find: Invoking the shell from xargs

1 
1 8.4.2 Invoking the shell from xargs
1 -----------------------------------
1 
1 Normally, 'xargs' will exec the command you specified directly, without
1 invoking a shell.  This is normally the behaviour one would want.  It's
1 somewhat more efficient and avoids problems with shell metacharacters,
1 for example.  However, sometimes it is necessary to manipulate the
1 environment of a command before it is run, in a way that 'xargs' does
1 not directly support.
1 
1    Invoking a shell from 'xargs' is a good way of performing such
1 manipulations.  However, some care must be taken to prevent problems,
1 for example unwanted interpretation of shell metacharacters.
1 
1    This command moves a set of files into an archive directory:
1 
1      find /foo -maxdepth 1 -atime +366 -exec mv {} /archive \;
1 
1    However, this will only move one file at a time.  We cannot in this
1 case use '-exec ... +' because the matched file names are added at the
1 end of the command line, while the destination directory would need to
1 be specified last.  We also can't use 'xargs' in the obvious way for the
1 same reason.  One way of working around this problem is to make use of
1 the special properties of GNU 'mv'; it has a '-t' option that allows the
1 target directory to be specified before the list of files to be moved.
1 However, while this technique works for GNU 'mv', it doesn't solve the
1 more general problem.
1 
1    Here is a more general technique for solving this problem:
1 
1      find /foo -maxdepth 1 -atime +366 -print0 |
1      xargs -r0 sh -c 'mv "$@" /archive' move
1 
1    Here, a shell is being invoked.  There are two shell instances to
1 think about.  The first is the shell which launches the 'xargs' command
1 (this might be the shell into which you are typing, for example).  The
1 second is the shell launched by 'xargs' (in fact it will probably launch
1 several, one after the other, depending on how many files need to be
1 archived).  We'll refer to this second shell as a subshell.
1 
1    Our example uses the '-c' option of 'sh'.  Its argument is a shell
1 command to be executed by the subshell.  Along with the rest of that
1 command, the $@ is enclosed by single quotes to make sure it is passed
1 to the subshell without being expanded by the parent shell.  It is also
1 enclosed with double quotes so that the subshell will expand '$@'
1 correctly even if one of the file names contains a space or newline.
1 
1    The subshell will use any non-option arguments as positional
1 parameters (that is, in the expansion of '$@').  Because 'xargs'
1 launches the 'sh -c' subshell with a list of files, those files will end
1 up as the expansion of '$@'.
1 
1    You may also notice the 'move' at the end of the command line.  This
1 is used as the value of '$0' by the subshell.  We include it because
1 otherwise the name of the first file to be moved would be used instead.
1 If that happened it would not be included in the subshell's expansion of
1 '$@', and so it wouldn't actually get moved.
1 
1    Another reason to use the 'sh -c' construct could be to perform
1 redirection:
1 
1      find /usr/include -name '*.h' | xargs grep -wl mode_t |
1      xargs -r sh -c 'exec emacs "$@" < /dev/tty' Emacs
1 
1    Notice that we use the shell builtin 'exec' here.  That's simply
1 because the subshell needs to do nothing once Emacs has been invoked.
1 Therefore instead of keeping a 'sh' process around for no reason, we
1 just arrange for the subshell to exec Emacs, saving an extra process
1 creation.
1 
1    Sometimes, though, it can be helpful to keep the shell process
1 around:
1 
1      find /foo -maxdepth 1 -atime +366 -print0 |
1      xargs -r0 sh -c 'mv "$@" /archive || exit 255' move
1 
1    Here, the shell will exit with status 255 if any 'mv' failed.  This
1 causes 'xargs' to stop immediately.
1