Ubuntu – How to use the exec command inside a for loop after a find command

bashcommand linefind

I've been trying to get this working for a while now. I want to search for a list of files and then copy them all to a certain path.

If I run the command separately, then it works like so:

find <path> -name 'file1' -exec cp '{}' <path2> \;

But I've been unable to run it inside a for loop.

#this works
for f in file1 file2 file3...; do find <path> -name "$f" \; done;
#none of these work (this code tries to find and copy the files named file and list from the path)
for f in file1 file2 file3...; do find <path> -name "$f" -exec cp '{}' <path2> \; done;
for f in file1 file2 file3...; do find <path> -name "$f" -exec cp {} <path2> \; done;

I've tried a few other stuff that weren't likely to work. The first line in the code quote just gets stuck and the others don't copy anything even though they don't get stuck.

I haven't been able to run anything with exec inside a for loop after a search and at this point I'm not sure what to do.

I have solved the immediate problem by searching the files and logging the results to another file and then running a copy inside a for loop separately but I'd still like to know how to do this.

Best Answer

While the other answers are correct I want to offer a different approach that doesn't require multiple invocations of find to scan the same directory structure repeatedly. The basic idea is to use find to

  1. generate a list of files that match the common criteria,
  2. apply a custom filter to that list, and
  3. perform some action, e. g. cp, on the entries of the filtered list.

Implementation 1

(requires Bash to read null-byte-delimited records)

find /some/path -type f -print0 |
while read -rd '' f; do
  case "${f##*/}" in
    file1|file2|file3)
      printf '%s\0' "$f";;
  esac
done |
xargs -r0 -- cp -vt /some/other/path --

Each pipe corresponds to the beginning of the next step of the three steps described above.

The case statement has the advantage of allowing globbing matches. Alternatively you could use Bash's conditional expressions:

if [[ "${f##*/}" == file1 || "${f##*/}" == file2 || "${f##*/}" == file3 ]]; then
  printf '%s\0' "$f"
fi

Implementation 2

If the list of file names to match is a bit longer and cannot be replaced with a small set of globbing patterns, or if the list of file names to match is not known at the time of writing, you can resort to an array that holds the list of file names of interest:

FILE_NAMES=( "file1" "file2" "file3" ... )

find /some/path -type f -print0 |
while read -rd '' f; do
  for needle in "${FILE_NAMES[@]}"; do
    if [ "${f##*/}" = "$needle" ]; then
      printf '%s\0' "$f"
    fi
  done
done |
xargs -r0 -- cp -vt /some/other/path --

Implementation 3

As a variation we can use an associative array which hopefully has faster look-up times than plain "list" arrays:

declare -A FILE_NAMES=( ["file1"]= ["file2"]= ["file3"]= ... )  # Note the superscripts with []=

find /some/path -type f -print0 |
while read -rd '' f; do
  if [ -v FILE_NAMES["${f##*/}"] ]; then
    printf '%s\0' "$f"
  fi
done |
xargs -r0 -- cp -vt /some/other/path --
Related Question