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