I find myself constantly looking up the syntax of
find . -name "FILENAME" -exec rm {} \;
mainly because I don't see how exactly the -exec
part works. What is the meaning of the braces, the backslash and the semicolon? Are there other use cases for that syntax?
Best Answer
This answer comes in the following parts:
-exec
-exec
in combination withsh -c
-exec ... {} +
-execdir
Basic usage of
-exec
The
-exec
option takes an external utility with optional arguments as its argument and executes it.If the string
{}
is present anywhere in the given command, each instance of it will be replaced by the pathname currently being processed (e.g../some/path/FILENAME
). In most shells, the two characters{}
does not need to be quoted.The command needs to be terminated with a
;
forfind
to know where it ends (as there may be further options afterwards). To protect the;
from the shell, it needs to be quoted as\;
or';'
, otherwise the shell will see it as the end of thefind
command.Example (the
\
at the end of the first two lines are just for line continuations):This will find all regular files (
-type f
) whose names matches the pattern*.txt
in or below the current directory. It will then test whether the stringhello
occurs in any of the found files usinggrep -q
(which does not produce any output, just an exit status). For those files that contain the string,cat
will be executed to output the contents of the file to the terminal.Each
-exec
also acts like a "test" on the pathnames found byfind
, just like-type
and-name
does. If the command returns a zero exit status (signifying "success"), the next part of thefind
command is considered, otherwise thefind
command continues with the next pathname. This is used in the example above to find files that contain the stringhello
, but to ignore all other files.The above example illustrates the two most common use cases of
-exec
:find
command).Using
-exec
in combination withsh -c
The command that
-exec
can execute is limited to an external utility with optional arguments. To use shell built-ins, functions, conditionals, pipelines, redirections etc. directly with-exec
is not possible, unless wrapped in something like ash -c
child shell.If
bash
features are required, then usebash -c
in place ofsh -c
.sh -c
runs/bin/sh
with a script given on the command line, followed by optional command line arguments to that script.A simple example of using
sh -c
by itself, withoutfind
:This passes two arguments to the child shell script. These will be placed in
$0
and$1
for the script to use.The string
sh
. This will be available as$0
inside the script, and if the internal shell outputs an error message, it will prefix it with this string.The argument
apples
is available as$1
in the script, and had there been more arguments, then these would have been available as$2
,$3
etc. They would also be available in the list"$@"
(except for$0
which would not be part of"$@"
).This is useful in combination with
-exec
as it allows us to make arbitrarily complex scripts that acts on the pathnames found byfind
.Example: Find all regular files that have a certain filename suffix, and change that filename suffix to some other suffix, where the suffixes are kept in variables:
Inside the internal script,
$1
would be the stringtext
,$2
would be the stringtxt
and$3
would be whatever pathnamefind
has found for us. The parameter expansion${3%.$1}
would take the pathname and remove the suffix.text
from it.Or, using
dirname
/basename
:or, with added variables in the internal script:
Note that in this last variation, the variables
from
andto
in the child shell are distinct from the variables with the same names in the external script.The above is the correct way of calling an arbitrary complex script from
-exec
withfind
. Usingfind
in a loop likeis error prone and inelegant (personal opinion). It is splitting filenames on whitespaces, invoking filename globbing, and also forces the shell to expand the complete result of
find
before even running the first iteration of the loop.See also:
Using
-exec ... {} +
The
;
at the end may be replaced by+
. This causesfind
to execute the given command with as many arguments (found pathnames) as possible rather than once for each found pathname. The string{}
has to occur just before the+
for this to work.Here,
find
will collect the resulting pathnames and executecat
on as many of them as possible at once.Likewise here,
mv
will be executed as few times as possible. This last example requires GNUmv
from coreutils (which supports the-t
option).Using
-exec sh -c ... {} +
is also an efficient way to loop over a set of pathnames with an arbitrarily complex script.The basics is the same as when using
-exec sh -c ... {} ';'
, but the script now takes a much longer list of arguments. These can be looped over by looping over"$@"
inside the script.Our example from the last section that changes filename suffixes:
Using
-execdir
There is also
-execdir
(implemented by mostfind
variants, but not a standard option).This works like
-exec
with the difference that the given shell command is executed with the directory of the found pathname as its current working directory and that{}
will contain the basename of the found pathname without its path (but GNUfind
will still prefix the basename with./
, while BSDfind
orsfind
won't).Example:
This will move each found
*.txt
-file to a pre-existingdone-texts
subdirectory in the same directory as where the file was found. The file will also be renamed by adding the suffix.done
to it.--
, to mark the end of options is needed here in thosefind
implementations that don't prefix the basename with./
. The quotes around the argument that contains{}
not as a whole are needed if your shell is(t)csh
. Also note that not allfind
implementations will expand that{}
there (sfind
won't).This would be a bit trickier to do with
-exec
as we would have to get the basename of the found file out of{}
to form the new name of the file. We also need the directory name from{}
to locate thedone-texts
directory properly.With
-execdir
, some things like these becomes easier.The corresponding operation using
-exec
instead of-execdir
would have to employ a child shell:or,