9

I want to use find to locate files, then copy those to a directory, so I tried:

find . -name '*.png' -exec cp {} /tmp/dest +

However, this fails with

find: missing argument to `-exec'

When I replace the + by a ; it works, but invokes cp for every file individually. How can I add a trailing argument (such as a destination directory) when using the + form of -exec?

Of course, in this case I can work around the apparent limitation by using cp -t (such as indicated in this post on Stack Overflow, but that solution is specific to cp. Instead, I might be using rsync, scp, or some other tool. Is there a general way to add arguments between {} and + in find -exec?

gerrit
  • 3,487
  • 1
    cp -t /tmp/dest {} + – nezabudka Oct 21 '20 at 12:29
  • 1
    @nezabudka I already mentioned that workaround in the question, it works for cp but may not work for other applications where I may need a similar syntax. – gerrit Oct 21 '20 at 12:31
  • 1
    As an aside consider xargs when the find will return multiple files https://danielmiessler.com/blog/linux-xargs-vs-exec/#:~:text=When%20you%20use%20%2Dexec%20to,which%20is%20often%20just%20once. – Stephen Boston Oct 21 '20 at 12:36
  • @StephenBoston That blog post specifically deals with the -exec ... \; usage of -exec, and does not consider -exec ... {} +, which would not execute the utility more than once per batch, just like xargs would. This question is about the -exec ... {} + usage and syntax specifically. – Kusalananda Oct 21 '20 at 12:38
  • Most commands that take a variable number of args specify they shall be given last. mv and cp were historical anomalies that took a directory last, and were fixed so they worked with xargs. I can't offhand think of any other such anomalies. – Paul_Pedant Oct 21 '20 at 13:55
  • @Paul_Pedant What if I want to copy with rsync rather than cp? For example, rsync src1 src2 src3 src4 user@host:dest/? rsync --help | grep target comes up empty. Same for scp. – gerrit Oct 21 '20 at 15:57
  • You send the filenames from find -print0 and pipe them into rsync --files-from=- --from0. They don't appear on the command line at all, so this fixes both the args order and the max-args limit. I found this at https://unix.stackexchange.com/questions/87018/find-and-rsync. – Paul_Pedant Oct 21 '20 at 16:25
  • I never got the hang of scp. I use ssh, variations based on ( cd /src && tar czf - . ) | ssh usr@host '( cd /dst && tar xzf - )'. – Paul_Pedant Oct 21 '20 at 16:50

1 Answers1

14

No, if you're using -exec ... {} +, there may be nothing between {} and + apart from whitespace. There is no way around that.

From the POSIX standard specification of the find command:

-exec utility_name [argument ...] ;
-exec utility_name [argument ...] {} +

The end of the primary expression shall be punctuated by a <semicolon> or by a <plus-sign>. Only a <plus-sign> that immediately follows an argument containing only the two characters {} shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be treated as special.

A more generic solution would possibly be

find ... -exec sh -c 'cp "$@" /tmp/dest' sh {} +

Here, an inline sh -c script is executed with batches of arguments from find. Inside the inline script, "$@" will be the list of passed arguments (individually quoted), which allows us to place them as we want them on the cp command line.

This allows us to use non-GNU cp (on e.g. macOS or other BSD systems, where there is no -t option) or any other utility where one may want to add other arguments to the end of the list of pathnames coming from find.

Related:


Nobody asked for this, but anyway...

With the destination directory in a variable, destdir:

destdir=/tmp/dest

find ... -exec sh -c 'destdir=$1; shift; cp "$@" "$destdir"' sh "$destdir" {} +

Note that the destdir in the shell calling find is a separate variable to the destdir in the sh -c script.

Or, with bash:

destdir=/tmp/dest

find ... -exec bash -c 'cp "${@:2}" "$1"' bash "$destdir" {} +

This is "slicing" the "$@" list to rearrange it appropriately for the cp command line, without extracting $1, the destination directory pathname, into a separate variable.

Kusalananda
  • 333,661