Using find(1) in command substitution and quote filenames with blanks - shell

I'd like to use find inside a command substitution, where the returned filenames contain whitespace. What option do I need so it correctly quotes the filenames? I tried -print0, but it will not work in the shell itself.
example:
command $(find . -type f) some other params
I also tried with -exec echo "{}" \;, but that was of no help either.
If I use set -x to display shell expansion and the actual command which is executed I get:
$ command `find -type f -printf \"%p\"\ ` some other params
++ find -type f -printf '"%p" '
+ command '"./file_with' 'blanks"' '"./another' 'file"' some other params
Where are the single quotation marks coming from and why are they applied to each "word"?

Put the find result in an array, and run command "${array[#]}" some other params.

Maybe the printf action is more amenable to being contained in a substitution (GNU find only, though):
command $(find . -type f -printf \"%P\"\ ) some other params
The %P placeholder is the filename minus the argument to find, so in cases other than find ., you'd probably want %p instead.

find /what/ever -name "what ever" -exec echo "\{\}" \;
works here (Ubuntu 10.04 default gterm with bash)
Just tried
find /bin -name ls -exec \{\} -lah \;
`find /bin -name ls -exec echo \{\} \;` -lah
MYCMD=`find /bin -name ls -exec echo \{\} \;` && $MYCMD -lah
MYCMD=$(`find /bin -name ls -exec echo \{\} \;` -lah) && echo $MYCMD
MYCMD=$(`find /bin -name ls` -lah) && echo $MYCMD
all work as expected

Related

find command - get base name only - NOT with basename command / NOT with printf

Is there any way to get the basename in the command find?
What I don't need:
find /dir1 -type f -printf "%f\n"
find /dir1 -type f -exec basename {} \;
Why you may ask? Because I need to continue using the found file. I basically want something like this:
find . -type f -exec find /home -type l -name "*{}*" \;
And it uses ./file1, not file1 as the agrument for -name.
Thanks for your help :)
If you've got Bash version 4.3 or later, try this Shellcheck-clean pure Bash code:
#! /bin/bash -p
shopt -s dotglob globstar nullglob
for path in ./**; do
[[ -L $path ]] && continue
[[ -f $path ]] || continue
base=${path##*/}
for path2 in /home/**/*"$base"*; do
[[ -L $path2 ]] && printf '%s\n' "$path2"
done
done
shopt -s ... enables some Bash settings that are required by the code:
dotglob enables globs to match files and directories that begin with .. find shows such files by default.
globstar enables the use of ** to match paths recursively through directory trees. globstar was introduced in Bash 4.0, but it was dangerous to use before Bash 4.3 (2014) because it followed symlinks when looking for matches.
nullglob makes globs expand to nothing when nothing matches (otherwise they expand to the glob pattern itself, which is almost never useful in programs).
See Removing part of a string (BashFAQ/100 (How do I do string manipulation in bash?)) for an explanation of ${path##*/}. That always works, even in some rare cases where $(basename "$path") doesn't.
See the accepted, and excellent, answer to Why is printf better than echo? for an explanation of why I used printf instead of echo to output the found paths.
This solution works correctly if you've got files that contain pattern characters (?, *, [, ], \) in their names.
Spawn a shell and make the second call to find from there
find /dir1 -type f -exec sh -c '
for p; do
find /dir2 -type l -name "*${p##*/}*"
done' sh {} +
If your files may contain special characters in their names (like [, ?, etc.), you may want to escape them like this to avoid false positives
find /dir1 -type f -exec sh -c '
for p; do
esc=$(printf "%sx\n" "${p##*/}" | sed "s/[?*[\]/\\\&/g")
esc=${esc%x}
find /dir2 -type l -name "*$esc*"
done' sh {} +
You'll have to forward it to another evaluator. There is no way to do that in find.
find . -type f -printf '%f\0' |
xargs -r0I{} find /home -type l -name '*{}*'
This answers your question about trying to merge the functionality of %f and -exec find and is based on your example but your example injects raw filenames as -name patterns so avoid that and look at other solutions instead.
Simply spawn a bash shell:
find /dir1 -type f -exec bash -c '
base=$(basename "$1")
echo "$base"
do_something_else "$base"
' bash {} \;
$1 in the bash part is each file filtered by find.

Find file and cd into it

I am attempting to find multiple files, then quit after the first match and then cd into this match, I have attempted:
find `pwd` -iname 'tensorflow' -type d -exec echo {} \; -quit | xargs -I{} cd {}
However, this does nothing and it won't enter into that directory.
There is no /usr/bin/cd, it's not an executable. You have to run it in current shell, not in subshell as part of pipeline.
Do not use backticks. Prefer $(...).
find pwd? Just find ., you are already in pwd.
-exec echo {} \;? Just -print it.
dir=$(find . -iname 'tensorflow' -type d -print -quit)
cd "$dir"

Unix find -exec: Why do the following behaviors differ?

The following works as intended:
$ find . -name .git -exec dirname '{}' \;
./google/guava
./JetBrains/intellij-community
./zsh-users/zsh-syntax-highlighting
But the following only returns dots:
$ find . -name .git -exec echo "$(dirname '{}')" \;
.
.
.
Why is that, and how can I use $(dirname '{}') in a find -exec command?
Please note, I am asking about UNIX find (OS X and FreeBSD, specifically), not GNU.
Reason for behaviour difference
Your shell is evaluating the $(dirname) before find, leading to this command being executed :
find . -name .git -exec echo . ;
Other ways to do this
You can of course use shell expansion inside find, by calling another shell yourself (or better, calling a script using the shell you want as shebang).
In other words:
find . -name .git -exec sh -c 'dirname {}' \;
Solution without dirname (POSIX, faster, one less subprocess to call) :
find . -name .git -exec sh -c 'path={}; echo "${path%/*}" ' \;
Combing /u/tripleee's answer (upvote him not me) with the find optimization :
find . -name .git -exec sh -c 'for f; do echo "${f%/*}"; done' _ {} \+
If you're using gnu find then you can ditch dirname and use printf:
find . -name .git -printf "%h\n"
The general answer is to run -exec sh (or -exec bash if you need Bash features).
find . -name .git -exec sh -c 'for f; do dirname "$f"; done' _ {} \+
dirname can easily be replaced with something simpler, but in the general case, this is a useful pattern for when you want a shell to process the results from find.
-exec ... \+ is running the shell on as many matches as possible, instead of executing a separate shell for each match. This optimization is not available in all versions of find.
If you have completely regular file names (no newlines in the results, etc) you might be better off with something like
find . -name .git | sed 's%/[^/]*$%%'
However, assuming that file names will be regular is a huge recurring source of bugs. Don't do that for a general-purpose tool.

Shell generic equivalent of Bash Substring replacement ${foo/a/b}

Is there a shell-independent equivalence of Bash substring replacement:
foo=Hello
echo ${foo/o/a} # will output "Hella"
Most of the time I can use bash so that is not a problem, however when combined with find -exec it does not work. For instance, to rename all .cpp files to .c, I'd like to use:
# does not work
find . -name '*.cpp' -exec mv {} {/.cpp$/.c}
For now, I'm using:
# does work, but longer
while read file; do
mv "$file" "${file/.cpp$/.c}";
done <<< $(find . -name '*.cpp')
Ideally a solution that could be used in scripts is better!
Using find and -exec you can do this:
find . -name '*.cpp' -exec bash -c 'f="$1"; mv "$f" "${f/.cpp/.c}"' - '{}' \;
However this will fork bash -c for each filename so using xargs or a for loop like this is better for performance reasons:
while IFS= read -d '' -r file; do
mv "$file" "${file/.cpp/.c}"
done < <(find . -name '*.cpp' -print0)
Btw, an alternative to using bash would be to use rename. If you have the cool version of the rename command, which is shipped along with perl you can do:
find -name '*.cpp' -exec rename 's/\.cpp$/.c/' {} +
The above example assumes that you have GNU findutils, having this you don't need to pass the current directory since it is the default. If you don't have GNU findutils, you need to explicitly pass it:
find . -name '*.cpp' -exec rename 's/\.cpp$/.c/' {} +

find . -exec echo {} \; = missing argument to `-exec'

Why doesn't this work? (echo is not the real command)
$ find . -type d -exec echo {} \;
find: missing argument to `-exec'
I managed to do that anyway like this:
$ for f in `find . -type d`; do echo $f; done
This work for me.
find . -type f -exec file '{}' \;
Braces are enclosed in single quote marks to protect them from interpretation as shell script punctuation.
The following line is from the EXAMPLES section of man find:
find . -type f -exec file '{}' \;
It looks to me like the {} part needs to be in single quotes.

Resources