Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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.

share|improve this question
add comment

4 Answers

up vote 2 down vote accepted

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
share|improve this answer
    
The last one may copy a directory several times if its parent directory also contains avi files. –  Stephane Chazelas May 14 '13 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). –  Gilles May 14 '13 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 '13 at 11:38
add comment

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.

share|improve this answer
    
find . -depth does the sequencing you need –  jthill May 13 '13 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 –  Stephane Chazelas May 13 '13 at 20:19
    
Why do the reversal? -depth lists directories after their contents. –  jthill May 13 '13 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). –  Stephane Chazelas May 13 '13 at 20:57
    
need a -type f on the find I think –  jthill May 14 '13 at 1:25
add comment
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.

share|improve this answer
    
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. –  Stephane Chazelas May 14 '13 at 3:33
    
This will break on paths containing newlines. –  Chris Down May 14 '13 at 4:09
    
Thank you guys. –  Rany Albeg Wein May 14 '13 at 12:08
add comment

You can use xargs and cp to make the work:

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

share|improve this answer
    
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 '13 at 5:46
2  
The indicated that the subdirectories have spaces in them, so this will confuse xargs –  Anthon May 13 '13 at 6:03
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.