Find Command – How ‘find … -exec sh -c’ Works

findscriptingshell-script

@StephaneChazelas posted the following solution to this Q&A: Having some trouble using “find -exec {} +”.

$ find . -iname "*.extension" -exec sh -c '
  exec <command> "$@" <additional parameters>' sh {} +

What exactly is going on here? Specifically what does the last sh {} do? It seems as though it's just there to pacify find's -exec command so that it has something to do, a NOOP.

I could just as easily put echo {} there and it appears to work just fine.

Best Answer

The syntax is:

find ... -exec cmd {} +

find will find a number of files based on the criteria in ... and run cmd with that list of file paths as arguments, as many as possible without surpassing the limit on the size of the arguments to a command.

If needed it may split the list of files and call cmd several times. For instance, it may end up calling:

cmd ./file1 ./file2 ... ./file3000
cmd ./file3001 ./file3002 ... ./file4321

A limitation with that is that {} has to be last. You can't for instance write:

find ... -exec cmd {} other args +

like you could with ';' instead of '+'.

You can write:

find ... -exec echo foo {} +

but not:

find ... -exec echo {} foo +

So, if you do need to add some extra arguments to cmd after the list of files, you have to resort to calling a shell. (Other reasons why you'd need to call a shell would be any time you need to use a shell feature like redirections, pipes, some string expansions....)

In sh -c 'inline-script' x a b c, for the inline-script, $0 is x, $1 is a, $2 is b... so "$@" is the list of those 3 arguments: a, b and c. So in:

find ... -exec sh -c 'cmd "$@" other arg' find-sh {} +

For the inline script, $0 (which is used for instance when displaying error messages) is set to find-sh and "$@" is the list of files (what find expands {} to).

By using the exec special builtin of the shell:

find ... -exec sh -c 'exec cmd "$@" other arg' find-sh {} +

We tell the shell not to fork an extra process to run cmd, but instead to run it in the same process (replacing the running shell process with that command). Some shells like bash, zsh and some implementations of ksh do that implicitly for the last command in a script.

Related Question