Use current filename ("{}") multiple times in "find -exec"? - shell

Many sources say that every instance of {} will be replaced with the filename found through find, but when I try to run the following, I only get one text file and its name is ".txt"
find /directory -name "*pattern*" -exec cut -f8 {} > {}.txt \;
The goal was to create a text file with only the eighth column from each file found, and each text file will be named after its parent file. Something about that second set of {} is not replacing with the filename of each found file.

Try:
find /directory -name "*pattern*" -exec sh -c 'cut -f8 {} > {}.txt' \;
But be aware that some versions of find require {} to be a distinct argument, and will not expand {} to a filename otherwise. You can work around that with:
find /directory -name "*pattern*" -exec sh -c 'cut -f8 $0 > $0.txt' {} \;
(this alternate command will put the output file in the subdirectory which contains the matched file. If desired, you could avoid that by redirecting to ${0#*/}
The issue is that find is not doing the redirection, the shell is. Your command is exactly equivalent to:
# Sample of INCORRECT code
find /directory -name "*pattern*" -exec cut -f8 {} \; > {}.txt
Note the following from the standard:
If more than one argument containing only the two characters "{}" is present, the behavior is unspecified.
If a utility_name or argument string contains the two characters "{}" , but not just the two characters "{}" , it is implementation-defined whether find replaces those two characters or uses the string without change.

To deal with the caveats that William Pursell mentioned in his answer, use the following:
find /directory -name "*pattern*" -exec sh -c 'cut -f8 "$1" > "$1.txt"' x {} \;
When you use sh -c, it gets the positional parameters from arguments following the string to execute. The extra x fills in $0, and the substituted filename will become $1.
The double quotes allow this to work properly with filenames containing spaces and other special characters.

find /directory -name "*pattern*" | xargs awk '{z=FILENAME".txt";print $8>z}'

Related

Shell command to list file names where the matching is found

I am trying to search through a list of binary files to find some keywords on Mac.
The following works to list out all the matches, but it doesn't show me the list of files where it is being found:
find . -type f -exec strings {} \;|grep "Bv9qtsZRgspQliITY4"
Is there any trick to do this?
Using -exec with a wee ‘script’:
find . -type f \
-exec sh -c 'strings "$1" | grep -q "Bv9qtsZRgspQliITY4"' -- {} \; \
-print
The above will print the paths of all the matching files. If you also want to print the matches you can use:
find . -type f \
-exec sh -c 'strings "$1" | grep "Bv9qtsZRgspQliITY4"' -- {} \; \
-print
This will, however, print the paths after the matches. If this is not desirable, then you can use:
find . -type f \
-print \
-exec sh -c 'strings "$1" | grep "Bv9qtsZRgspQliITY4"' -- {} \;
This, on the other hand, will print all paths, even non-matching ones. To print only matching paths and their matches:
find . -type f \
-exec sh -c 'strings "$1" | grep -q "Bv9qtsZRgspQliITY4"' -- {} \; \
-print \
-exec grep "Bv9qtsZRgspQliITY4" {} \;
This will run grep twice on matching files, which will make it slower. If this is a problem the matches can be stored in a variable, and if there are any the path printed first and then the matches. This is left as an exercise to the reader.*
* Let me know if I should post it here.
Try grep -rl "Bv9qtsZRgspQliITY4" ..
Explanation of options:
-r: search recursively
-l: don't print the contents of the file, just print the filename.
Optionally, you might want to use -i to search case-insensitively.
The problem with your idea is that you're piping the output of strings into grep. The filename is only passed to strings, meaning that nothing that comes after strings knows the filename.
I'm not quite sure about portability, but if you are using GNU's version of grep, then you can use --files-with-matches
-l, --files-with-matches print only names of FILEs containing matches
Then you can use something like this:
grep --recursive --files-with-matches "Bv9qtsZRgspQliITY4" *
Well, if it's only to print names of files don't use find but grep.
grep -ar . -e 'soloman' ./testo.txt:1:soloman
-a : Search in binary files
-r : recursive
And keep it simple.
If you don't want to see the words matched in your output simply add -l, --files-with-matches:
user#DESKTOP-RR909JI ~/projects/search
$ grep -arl . -e 'soloman'
./testo.txt
You can use
# this will list all the files containing given text in current directory
# i to ignore case
# l to list files with matches
# R read and process all files in that directory, recursively, following all symbolic links
grep -iRl "your-text-to-find" ./
# for case sensitive search
grep -Rl "your-text-to-find" ./

Parameter expansion not working in "find -exec"

I'm writing a bash script as above, but the parameter expansion is not working with the EXC variable.
#!/bin/bash
EXC="--exclude='*.js' --exclude='*.sh'"
find /path -exec grep ${EXC} "xxx" {} \; >> result.txt
Options in the EXC variable are not used by the grep call as it still parse JavaScript files...
Also tried
find /path -exec grep $EXC "xxx" {} \; >> result.txt
You can filter find results with ! (not) operator combined with -name option. To exclude .js and .sh files:
find . -type f ! -name '*.js' ! -name '*.sh' -exec grep xxx {} \;
The problem is that the single quotes are not removed from the parameter expansion, so grep is receiving '*.js' as the pattern, not *.js as you want. You need to use an array to hold the arguments:
exc=( "--exclude=*.js" "--exclude=*.sh" ) # No single quotes needed
find /path -exec grep "${exc[#]}" "xxx" {} \; >> result.txt
The quoted parameter expansion prevents *.js and *.sh from being expanded by the shell in the same way that quoting them when used literally does. --exclude='*.js' and '--exclude=*.js' would both result in the same argument being passed to grep, as would the minimalist --exclude=\*.js. You could also define your array as
exc=( --exclude "*.js" --exclude "*.sh" )
since the long option and its argument can be specified as one word containing = or as two separate words.
It still parses or it's only applied by find also on files that you are trying to exclude?
Anyway, why not use only grep -r instead of find plus grep?
grep -r --exclude='*.js' --exclude='*.sh' "xxx" /path
The problem is that i'm using a variable to hold some arguments, and parameter expansion does not replace the ${} thing by its value in the command line...
You can launch a subshell wich will perform what you want
#!/bin/bash
EXC="--exclude='*.js' --exclude='*.sh'"
find /path -exec bash -c "grep ${EXC} 'xxx' {}" \; >> result.txt
these could help you;
#!/bin/bash
EXC="--exclude='*.js' --exclude='*.sh'"
find /path -exec echo "grep ${EXC} 'xxx' {}" \; | bash > result.txt
or
#!/bin/bash
EXC="*js,*sh"
find /path -exec echo "grep --exclude={${EXC}} 'xxx' {}" \; | bash > result.txt

Interpret bash commands

I checked some resources, but still hard to find a clue to interpret the codes.
$ find . -iname "*.dwp" -exec bash -c 'mv "$0" "${0%\.dwp}.html"' {} \;
$ find . -name ".DS_Store" -exec rm {} \;
To be more specific, what's the difference between -iname and -name? And what does "-c" and "%" symbolize?
Can you interpret the two commands a bit for me?
The first one:
-iname "*.dwp", indicate to the find command to find files whose name matches the pattern *.dwp, ignore case, e.g.: ./a.dwp
-exec expression {} \; part, execute the command bash -c 'mv "$0" "${0%\.dwp}.html"' {}. {} will be replaced by the path of each file. The expression is terminated by a semicolon. If there is a file a.dwp in the current directory, bash -c 'mv "$0" "${0%\.dwp}.html"' a.dwp will execute.
bash -c 'mv "$0" "${0%\.dwp}.html"' {}:
-c means read command from string, do not start an interactive shell.
$0 is the argument of the command, a.dwp in this example.
${0%\.dwp}.html is string manipulation, % removes the shortest match from the end, so for a.dwp, remove .dwp from end to get the file name a without extension.
So the command is mv a.dwp a.html.
The second one is very simple if you understand first one.

What's the difference between `\;` and `+` at the end of a find command?

These are bash commands that are used to convert tabs to spaces.
Here's the link to the original stackoverflow post.
This one uses \; at the end of the command
find /path/to/directory -type f -iname '*.js' -exec sed -ie 's|\t| |g' '{}' \;
This one uses + instead of \;.
find /path/to/directory -type f -iname '*.js' -exec sed -ie 's|\t| |g' '{}' '+'
What exactly is the difference between the two?
The \; or + is not related to bash. It's an argument to the find command, specifically to find's -exec option.
find -exec uses {} to pass the current file name to the specified command, and \; to mark the end of the the command's arguments. The \ is needed because ; by itself is special to bash; by typing \;, you can pass a literal ; character as an argument. (You can also type ';' or ";".)
The + symbol (no \ needed because + is not special to bash) causes find to invoke the specified command with multiple arguments rather than just once, in a manner similar to xargs.
For example, suppose the current directory contains 2 files named abc and xyz. If you type:
find . -type f -exec echo {} \;
it invokes the echo command twice, producing this output:
./abc
./xyz
If you instead type:
find . -type f -exec echo {} +
then find invokes echo just once, with the following output:
./xyz ./abc
For more information, type info find or man find (if the documentation is installed on your system), or you can read the manual online at http://www.gnu.org/software/findutils/manual/html_node/find_html/

Edit a find -exec echo command to include a grep for a string

So I have the following command which looks for a series of files and appends three lines to the end of everything found. Works as expected.
find /directory/ -name "file.php" -type f -exec sh -c "echo -e 'string1\string2\nstring3\n' >> {}" \;
What I need to do is also look for any instance of string1, string2, or string3 in the find ouput of file.php prior to echoing/appending the lines so I don't append a file unnecessarily. (This is being run in a crontab)
Using | grep -v "string" after the find breaks the -exec command.
How would I go about accomplishing my goal?
Thanks in advance!
That -exec command isn't safe for strings with spaces.
You want something like this instead (assuming finding any of the strings is reason not to add any of the strings).
find /directory/ -name "file.php" -type f -exec sh -c "grep -q 'string1|string2|string3' \"\$1\" || echo -e 'string1\nstring2\nstring3\n' >> \"\$1\"" - {} \;
To explain the safety issue.
find places {} in the command it runs as a single argument but when you splat that into a double-quoted string you lose that benefit.
So instead of doing that you pass the file as an argument to the shell and then use the positional arguments in the shell command with quotes.
The command above simply chains the echo to a failure from grep to accomplish the goal.

Resources