First, why your attempt doesn't work: -printf "%h\n"
prints the directory part of the .avi
file name. That doesn't affect anything in the subsequent -exec
action — {}
doesn't mean “whatever the last printf
command printed”, it means “the path to the found file”.
If you want to use the directory part of the file name in that cp
command, you need to modify the file name before passing it to cp
. The find
command doesn't have this capability, but a shell does, so make find
invoke a shell which invokes cp
.
find . -name "*.avi" -exec sh -c 'cp -Rp "${0%/*}" /share/USBDisk1/Movies/' {} \;
Note that you'll need to pass -r
to cp
since you're copying a directory. You should probably preserve metadata such as the files' modification time, too, with -p
.
You may usefully replace cp -Rp
by rsync -a
. That way, if you've already copied a movie directory, it won't be copied again (unless its contents have changed).
Your command has a defect that may or may not affect you: if a directory contains multiple .avi
files, it will be copied multiple times. It would be better to look for directories, and copy them if they contain a .avi
file, rather than look for .avi
files. If you use rsync
instead of cp
, the files won't be copied again, it's just a bit more work for rsync
to verify the existence of the files over and over.
If all the movie directories are immediately under the toplevel directory, you don't need find
, a simple loop over a wildcard pattern suffices.
for d in ./*/; do
set -- "$d/"*.avi
if [ -e "$1" ]; then
# there is at least one .avi file in $d
cp -rp -- "$d" /share/USBDisk1/Movies/
fi
done
If the movie directories may be nested (e.g. Movies/science fiction/Ridley Scott/Blade Runner
), you don't need find
, a simple loop over a wildcard pattern suffices. You do need to be running ksh93 or bash ≥4 or zsh, and in ksh93 you need to run set -o globstar
first, and in bash you need to run shopt -s globstar
first. The wildcard pattern **/
matches the current directory and all its subdirectories recursively (bash also traverses symbolic links to subdirectories).
for d in ./**/; do
set -- "$d/"*.avi
if [ -e "$1" ]; then
cp -rp -- "$d" /share/USBDisk1/Movies/
fi
done
Solution Using Parallel
You could use GNU Parallel for a compact, faster solution.
find . -type d -print0 | parallel -0 cd {}'&&' <command-name>
This will work absolutely fine, even for directory names containing spaces and newlines. What parallel
does here is that it takes the output from find
, which is every directory and then feeds it to cd using {}
. Then if changing the directory is successful, a separate command is run under this directory.
Regular Solution using while loop
find "$PWD" -type d | while read -r line; do cd "$line" && <command-name>; done;
Note that $PWD
is used here because that variable contains the absolute path of current directory from where the command is being run. If you don't use absolute path, then cd
might throw error in the while loop.
This is an easier solution. It will work most of the time except when directory names have weird characters in them like newlines (see comments).
Best Answer
GNU find supports the
-printf
predicate, which supports the%f
format specifier for outputting on the filename.