How does command substitution work with find? - macos

I have the following command
find . -name "*.tiff" -exec echo `basename -s .tiff {}` \;
I expect this to print all my .tiff-files without their file extensions. What I get is
./file1.tiff
./file2.tiff
...
The command,
find . -name "*.tiff" -exec basename -s .tiff {} \;
does yield
file1
file2
...
Is this not supposed to be the input of echo?

The content of the backticks is executed before the find command - yielding just the placeholder {}, which is used in the find command line - hence your result. You can always use set -x to examine what the shell is up to.

Use single-quote characters (') instead of backticks (`) - putting a command in backticks causes it to be executed and replaced by its output in your command.
Also, modify the command to get rid of the echo, like this:
find . -name "*.tiff" -exec 'basename -s .tiff {}' \;
This will execute basename on each found file.

Related

I used the'-print0' option to handle filenames with spaces, but I get an error

#!/bin/bash
find /home/data -name '*QQ*' -print0 -exec bash -c ' mv $1 ${0/\-QQ/-TT}' {} \;
I used the'-print0' option to handle filenames with spaces, but I get an error
/home/data/gone to sea.1080p-QQ.mp4mv: target 'sea.1080p-TT.mp4' is not a directory
Which part is wrong?
Thanks
You don't need -print0, since you're not piping the output to another program.
You just need to quote properly in the bash command.
find /home/data -name '*-QQ*' -exec bash -c 'mv "$1" "${1/\-QQ/-TT}"' {} {} \;
This should work as long as the filenames don't contain double quote or $ characters.
You could also avoid bash -c by using the rename command:
find /home/data -name '*-QQ*' -exec rename 's/-QQ/-TT/' {} +

Bash -c argument passing to find

This command works
find . -name \*.txt -print
and outputs two filenames
This command works
bash -c 'echo . $0 $1 -print' "-name" "\*.txt"
and outputs this result:
. -name *.txt -print
But this command
bash -c 'find . $0 $1 -print' "-name" "\*.txt"
does not give an error but does not output anything either.
Can anyone tell me what is happening here?
It looks like you're trying to use "\*.txt" to forestall glob-expansion so that the find command sees *.txt instead of e.g. foo.txt.
However, what it ends up seeing is \*.txt. No files match that pattern, so you see no output.
To make find see *.txt as its 3rd argument, you could do this:
bash -c 'find . $0 "$1" -print' "-name" "*.txt"
Edit: Are you really getting . -name *.txt -print as the output of the first command where you replaced find with echo? When I run that command, I get . -name \*.txt -print.
Well the suggestions from francesco work. But I am still confused by the behaviour here.
We know that putting unquoted wild cards in a find command will usually result in an error. To wit:
find . -name *.txt -print
find: paths must precede expression: HowTo-Word-Split.txt' find:
possible unquoted pattern after predicate-name'?
However putting the wild card in single quotes (or escaping it if it is only 1 char) will work like so:
find . -name \*.txt -print
which gives this output (on two separate lines)
> ./HowTo-Word-Split.txt
> ./bash-parms.txt
So in the bash -c version what I was thinking was this:
bash -c 'find . $0 $1 -print' "-name" "*.txt"
would result in the *.txt being expanded even before being passed in to the cmd string,
and using single quotes around it would result in trying to execute (after the arg substitution and the -c taking effect)
find . -name *.txt -print
which as I just demonstrated does not work.
However there seems to be some sort of magic associated with the -c switch as demonstrated by setting -x at the bash prompt, like so:
$ set -x
$ bash -c ' find . $0 "$1" -print' "-name" "*.txt"
+ bash -c ' find . $0 "$1" -print' -name '*.txt'
./HowTo-Word-Split.txt
./bash-parms.txt
Note that even though I used double quotes in the -c line, bash actually executed the find with single quotes put around the argument, thus making find work.
Problem solved. :)!

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

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.

Bash How reuse found files name?

I want to run a script in a directory in order to generate an svg file for each found dot file. Something like:
find . -name "*.dot" -exec dot -Tsvg \{} \;
This works fine but just output the result on stdout. usually I am using a redirection to generate the svg file. How can I get the dot file name to use it in the redirection like
find . -name "*.dot" -exec dot -Tsvg > "$dotfilename".svg \{} \;
The following:
for i in `find . -name "*.dot"`; do
dot -Tsvg $i > $i.svg
done
performs the find (in backticks) and loops over the results, executing dot for each one. The filename is in $i.
This uses backtick substitution and is a useful mechanism for capturing command output for subsequent use in another command.
To remove the extension and add another, use ${i%.*}.svg. See this SO answer for more info.
You don't need output redirection. Use -O to save to a file whose name is automatically created from the input file name and the output format.
find . -name "*.dot" -exec dot -Tsvg -O \{} \;
Just to point out that you can use {} multiple times in the argument to -exec:
find . -name "*.dot" -exec dot -Tsvg -o \{}.svg \{} \;
Where the first would produce "foo.svg" from "foo.dot", the second would produce "foo.dot.svg"
You can't do the redirection here. find does execute using execvp(3) or friends, not the shell. Instead, use shell globs, or make a script which you then can call from find
An example:
for i in ./*.dot
do
svg=${i%.dot}.svg
dot -Tsvg "$i" > "$svg"
done
The problem that you are having is that redirection is processed before the find command. You can work around this by spawning another bash process in the -exec call. This also makes it easy to remove the extension using parameter expansion.
find . -name "*.dot" -exec bash -c 'dot -Tsvg "$1" > "${1%.*}".svg' -- {} \;

Resources