Previous: xargs options, Up: Invoking xargs

8.4.2 Invoking the shell from xargs

Normally, xargs will exec the command you specified directly, without invoking a shell. This is normally the behaviour one would want. It's somewhat more efficient and avoids problems with shell metacharacters, for example. However, sometimes it is necessary to manipulate the environment of a command before it is run, in a way that xargs does not directly support.

Invoking a shell from xargs is a good way of performing such manipulations. However, some care must be taken to prevent problems, for example unwanted interpretation of shell metacharacters.

This command moves a set of files into an archive directory:

     find /foo -maxdepth 1 -atime +366 -exec mv {} /archive \;

However, this will only move one file at a time. We cannot in this case use -exec ... + because the matched file names are added at the end of the command line, while the destination directory would need to be specified last. We also can't use xargs in the obvious way for the same reason. One way of working around this problem is to make use of the special properties of GNU mv; it has a -t option that allows the target directory to be specified before the list of files to be moved. However, while this technique works for GNU mv, it doesn't solve the more general problem.

Here is a more general technique for solving this problem:

     find /foo -maxdepth 1 -atime +366 -print0 |
     xargs -r0 sh -c 'mv "$@" /archive' move

Here, a shell is being invoked. There are two shell instances to think about. The first is the shell which launches the xargs command (this might be the shell into which you are typing, for example). The second is the shell launched by xargs (in fact it will probably launch several, one after the other, depending on how many files need to be archived). We'll refer to this second shell as a subshell.

Our example uses the -c option of sh. Its argument is a shell command to be executed by the subshell. Along with the rest of that command, the $@ is enclosed by single quotes to make sure it is passed to the subshell without being expanded by the parent shell. It is also enclosed with double quotes so that the subshell will expand $@ correctly even if one of the file names contains a space or newline.

The subshell will use any non-option arguments as positional parameters (that is, in the expansion of $@). Because xargs launches the sh -c subshell with a list of files, those files will end up as the expansion of $@.

You may also notice the ‘move’ at the end of the command line. This is used as the value of $0 by the subshell. We include it because otherwise the name of the first file to be moved would be used instead. If that happened it would not be included in the subshell's expansion of $@, and so it wouldn't actually get moved.

Another reason to use the sh -c construct could be to perform redirection:

     find /usr/include -name '*.h' | xargs grep -wl mode_t |
     xargs -r sh -c 'exec emacs "$@" < /dev/tty' Emacs

Notice that we use the shell builtin exec here. That's simply because the subshell needs to do nothing once Emacs has been invoked. Therefore instead of keeping a sh process around for no reason, we just arrange for the subshell to exec Emacs, saving an extra process creation.

Sometimes, though, it can be helpful to keep the shell process around:

     find /foo -maxdepth 1 -atime +366 -print0 |
     xargs -r0 sh -c 'mv "$@" /archive || exit 255' move

Here, the shell will exit with status 255 if any mv failed. This causes xargs to stop immediately.