5

I have a directory "Movies" containing subdirectories "Movie Name". Each subdirectory "Movie Name" contains a movie file and related image/nfo files etc.

I'm trying to copy all directories containing movie file of type ".avi" to an external usb device.

So if Directory/subdirectory A contains movie.avi, poster.jpg and movie.nfo, then I want to copy that directory and its contents to the external drive.

I've tried this:

find . -name "*.avi" -printf "%h\n" -exec cp {} /share/USBDisk1/Movies/ \;

But it only copies the file, not the directory and file contents.

5 Answers 5

6

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
3
  • The last one may copy a directory several times if its parent directory also contains avi files. May 14, 2013 at 3:18
  • @StephaneChazelas Going by the problem statement, the movie directories are direct subdirectories of the root anyway. If they aren't, then movie1/1.avi and movie1/movie2/2.avi should result in both movie1/1.avi and movie2.2.avi on the USB stick. I suppose the right thing to do would be to either not copy movie1's subdirectories, or arrange to create hard links or symlinks (except that that's unlikely to work as the share is probably FAT). May 14, 2013 at 6:13
  • This: find . -name "*.avi" -exec sh -c 'cp -Rp "${0%/*}" /share/USBDisk1/Movies/' {} \; is working a treat! It's a simple one time offload of files so this is the one for me. Thank you so much for your help. I don't think I would have ever gotten there!
    – Lowey
    May 14, 2013 at 11:38
1

Since you seem to be using GNU tools, you could do:

find . -name '*.avi' -printf '%h\0' |
  tr '\1/' '/\1' |
  LC_ALL=C sort -zu |
  tr '\1/' '/\1' |
  awk -vRS='\0' -vORS='\0' '
    NR>1 && substr($0, 1, length(l)) == l {next}
    {print; l=$0"/"}' |
  xargs -r0 cp -rt /share/USBDisk1/Movies/

The above is GNU specific:

  • for find (because of -printf)
  • for sort because of -z
  • for awk for the ability to handle NUL characters
  • for xargs (for the -r and -0 options, though some BSDs support them as well)
  • cp (for -t)

The idea is:

  • find outputs the dirnames of all the avi files.
  • we sort that list so that every directory is listed after its parent directory (for that we need the / character to sort before any other one which is why we swap it with \1).
  • we use sort -u so that each directory appears only once
  • we use a small awk script to remove every directory entry for which an ancestor has already been included (as we don't want to copy both ./a and ./a/b if both contain AVI files).
  • pass that list to xargs's stdin so it can pass it as arguments to cp -t target.

Note that it doesn't try to protect against potential clashes if some directories after the same name.

If you want to reproduce the directory structure on the target drive, you can replace xargs -r0 cp -rt /share/USBDisk1/Movies/ with

pax -0rw /share/USBDisk1/Movies/

(with a pax supporting the -0 option such as the one found on Debian or MirBSD at least).

Ideally, you'd like to be able to write it like:

find . -type d -has '*.avi' -prune -exec cp -rt /target {} +

Unfortunately, find has no such -has predicate. The closest you can achieve would be:

find . -type d -exec bash -c '
  shopt -s nullglob dotglob
  set -- "$1"/*.avi
  (( $# > 0 ))' sh {} \; \
  -prune -exec cp -r {} /share/USBDisk1/Movies/ \;

But that means calling one bash and redo the file listing within bash for each directory, so would probably be less efficient than the previous solution.

5
  • find . -depth does the sequencing you need
    – jthill
    May 13, 2013 at 20:13
  • @jhill, find -depth | tr '\0\n' '\n\0' | tac | tr '\0\n' '\n\0', you mean? Even then, you'd still need to sort for uniq or make the awk part more complex, so that would hardly help May 13, 2013 at 20:19
  • Why do the reversal? -depth lists directories after their contents.
    – jthill
    May 13, 2013 at 20:40
  • @jhill. yes and we need them before their content. Because if both a and a/b contain avi files, we want to copy a and not a/b (since we've already copied a/b as part of the a copy). May 13, 2013 at 20:57
  • need a -type f on the find I think
    – jthill
    May 14, 2013 at 1:25
1

I found this one which I think is much simpler and pretty much results to what you need:

find . -name "*.avi" -exec cp "{}" /share/USBDisk1/Movies/ \;

I think similar way you did but without the -printf as the "{}" captures the output from find.

Reference: https://stackoverflow.com/questions/16898462/moving-files-from-directory-and-all-subdirectories

Best,

0
foo () 
{ 
    local subdirs=() target="$1" dest="$2";
    while IFS= read -rd ''; do
        subdirs+=("$REPLY");
    done < <(find "$target" -type f -iname '*.avi' -exec bash -c 'printf "%s\0" "${@%/*}"' _ {} + | sort -zu);
    cp -rp -- "${subdirs[@]}" "$dest"
}

Usage: foo <source directory> <destination directory>

Which will also address the following issue, mentioned previously by Gilles:

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.

2
  • Note that -exec cmd ... {} + does not guarantee that cmd be called only once. You'd better do the sort -u on the output of find and use -printf '%h\n' to take the dirname (since you're using a GNUism already) which would save executing bash. It should also be noted that your code is bash specific (though it takes little to change for it to also work in ksh93 or zsh), assumes the file names don't contain newline characters and may copy a directory more than once if a directory and its parent both happen to contain AVI files. May 14, 2013 at 3:33
  • This will break on paths containing newlines.
    – Chris Down
    May 14, 2013 at 4:09
-2

You can use xargs and cp to make the work:

find . -name "*.avi" -printf "%h\n" | xargs cp -t /share/USBDisk1/Movies/ -r

2
  • If the movie consists of multiple .avi files (that e.g. each would fit on a CD) than this results in multiple copies of the each all files for that movie. To USB that is slow.
    – Anthon
    May 13, 2013 at 5:46
  • 2
    The indicated that the subdirectories have spaces in them, so this will confuse xargs
    – Anthon
    May 13, 2013 at 6:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .