Commands That Launch Other Commands

Steve Zeil

Last modified: Aug 29, 2023
Contents:

Redirection allows you to alter the behavior of individual commands by changing where they get their input and where they store their output. Pipes allow you to combine commands in interesting ways.

Next we will look at other ways to combine selective inputs and combinations of commands. In particular, we will look at two commands, xargs and find, that are interesting in part because they allow you to issue other commands from them. First, though, we will look at a technique by which the output from one command can be supplied as part of the parameter list to a second command.

1 Backticks

Earlier, we looked at different forms of quoting in shell commands. We used double-quotes (") and single quotes (') to tell the shell to suppress its usual treatment of special characters, making them “normal” characters instead. In essence, quoting suppresses the normal activity of the command shell.

If, however, we write something between a pair of backticks (`, often found just to the left of the “1” key), almost the opposite happens. Instead of treating the enclosed characters as plain text, the shell treats them as a command, runs that command, and replaces the whole `-surrounded string by the output of that command.

Example 1: Try This

Give the following commands, noting the difference between the effects of the forward and backward quotes.

date
echo Today is date.
echo Today is 'date'.
echo Today is `date`.

As an example of where this might be useful, suppose that you had been working on a large programming project, and now wanted to start another, similar project. Let’s assume that the old project is in a directory named oldProject and that we have just created a directory newProject to hold the new one. Now, you might start by copying the files from the old project to the new one:

cp oldProject/* newProject/

but that would copy not only your program’s source code but also files that you don’t want carried over to the new project, the *.o files and executables produced when you compiled the old project.

Now, if we could produce a listing of the files that we wanted, we could use backticks to feed that list into the appropriate spot in a cp command. So, let’s build that list. If we start with

ls oldProject/*

we get a list of all the files in the old project. Let’s filter that by removing the .o files:

ls oldProject/* | grep -v '\.o'

The backslash in front of the period is needed because, normally, a period in a regular expression means “match any single character”, and we actually want to match only periods. The -v option tells grep to select every line that does not match our pattern. So this should remove all .o files from the list if old project files.

Now, executables in Unix usually have no extension at all. So’ we’d like to remove from our list any files that have no periods in them at all:

ls oldProject/* | grep -v '\.o' | grep '\.'

That gives us the list of files we want to copy, so we can now feed that into a cp command:

cp `ls oldProject/* | grep -v '\.o' | grep '\.'` newProject/
Example 2: Try This

Let’s set up a simple version of the example that we just went through. Give these commands:

cd ~/playing
mkdir oldProject newProject
cd oldProject
date > a.h
date > a.cpp
date > b.h
date > b.cpp
date > a.o
date > b.o
date > c.o
date > exec
ls
cd ..

Now, we’ve set up a dummy project. Let’s see how the example works:

ls oldProject/*
ls oldProject/* | grep -v '\.o'
ls oldProject/* | grep -v '\.o' | grep '\.'
cp `ls oldProject/* | grep -v '\.o' | grep '\.'` newProject/
ls newProject

2 xargs

xargs reads a list of file names from the standard input and fills those file names in to a command of your choosing. In its simplest form, you can use xargs like this:

xargs partial-command

In this form xargs will read a list of file names (paths) from the standard input and will simply tack them on to the end of the partial command.

Where do the file names come from? You’re not likely to type them directly at the keyboard. (If you were going to do that, you would probably just have typed the whole command.) So, usually, you will run this with a list of files names that you have collected into a text file, or you may pipe into xargs the output of a command that lists the files you want.

Example 3: Try This:
ls /bin
ls /bin | xargs echo Issuing a command on

Most command shells will have limits on just how long a single command can get, and xargs tries to be smart about the way it constructs commands. If the standard input contains a very large number of files, xargs will break the list up into pieces. Look at the output above. Can you see evidence that xargs has split the list into separate commands?

You can control the maximum number of files that xargs will pack into one command using the -n flag.

Example 4: Try This:
ls /bin | xargs -n 4 echo Issuing a command on

The most common reason for doing this is because not all Unix commands work on arbitrarily long lists of files. Some work only on a single file, making -n 1 a useful option.

In this basic form, xargs tacks the file names onto the end of the generated command. But sometimes you might want the filenames placed into the middle of the command. The -i option permits that. If xargsis executed with a -i flag, then it looks in your partial command for the characters “{}” and places your files there (one at a time, as if you had said “-n 1”.

Example 5: Try This:
ls /bin | xargs -i echo Hello {} world

As an example of using xargs, suppose that you have a directory with a large number of data files. You want to copy those files into a new directory. However, you have edited many of these files, so the original directory is littered with backup files left by the editors. You would prefer not to copy those backups. The backups can be recognized because some end in “.bak” and some end in “~”.

Now, normally, you would copy files from one directory to another like this:

cp oldDirectory/* newDirectory/

But there is not obvious way to rewrite the wildcard pattern * to exclude the backup files (wildcards are great for including things, not so great for excluding them).

But a list of file names is nothing more than text, and we have some powerful tools like grep for editing and selecting text.

Example 6: Try This:
mkdir ~/xargs
cd ~cs252/Assignments/xargs
ls
ls | grep -v '\.bak$'
ls | grep -v '\.bak$' | grep -v '~$'
ls | grep -v '\.bak$' | grep -v '~$' | xargs -i cp {} ~/xargs
ls ~/xargs

3 find

Wildcards give us a way to describe a number of files at once. But wildcards have a limitation. They can only describe one “level” of directories at a time. You can write a wildcard expression to look at a variety of files in one directory, or at a variety of files in one or more subdirectories of that directory, or in one or more sub-subdirectories of that one. But you cannot write a wildcard expression that will simultaneously describe files in a directory and in its subdirectories.

Some commands will try to help with this. Both cp and rm, for example, offer a -r flag (“r” for “recursive”) that will descend into an arbitrary number of levels of subdirectories, but these are all-or-nothing selections. You can’t be very selective about what files get processed this way.

This is where find come in. find is the Swiss army knife of Unix commands. It provides all kinds of ways to select files, no matter how deep they are in your directory structure. It provides a variety of things it can do with selected files, or it can fill their names into an arbitrary command in a manner similar to xargs -i.

The general form of a find command is

find list-of-files-and-directories list-of-actions

find looks at each file and directory given in the list. For directories, it also looks at all files and directories inside those, descending as far as it can from directory to directory.

The actions in the command are all given a flags (beginning with “-”). Some actions will “do something” to a file. Others are used to select which files will be passed on to the later commands in the list.

Example 7: Try This:
ls /usr/include
find /usr/include

The most common selection action is -name, which is given a wildcard expression to match file names against.

Example 8: Try This:
ls /usr/include/w*.h
ls /usr/include/*/w*.h
find /usr/include -name 'w*.h'

The wildcard expression for -name must be quoted, because you don’t want the command shell to expand it before it launches find.

Other useful ways to select files include -type, which chooses different “types” of files. Directories are type ‘d’ and ordinary files are type ‘f’.

Example 9: Try This:
find /usr/include -type d -name 'u*'
find /usr/include -type d -name 'u*' -ls

Note that all the files listed are directories.


find /usr/include -type f -name 'u*'

You can also select files based on how long ago they were modified.

Example 10: Try This:
find ~ -mtime +7
find ~ -mtime -7

One of these lists only files you have modified within the past 7 days. The other lists files whose last modification is more than 7 days in the past.

Not sure which is which? Try

find ~ -mtime +7 | xargs ls -ld
find ~ -mtime -7 | xargs ls -ld

What can find do to files it selects? The simplest possibility is to simply print the file name, which is done by the action “-print”. In fact, that’s the behavior we have seen in all the examples so far, because it’s the default if you don’t do anything else to the selected files. Sometimes, though, you use -print because you want to print a file name and do something else to a file.

You can get a bit more information than just the name:

Example 11: Try This:
find ~ -mtime +7 -ls

The most powerful use of find comes form the -exec action, which allows you to specify an arbitrary Unix command that you want applied to selected files. The command is terminated by a quoted semi-colon (“\;”) and should contain the characters “{}” at the point where you want to insert the file name.

Example 12: Try This:
cp ~cs252/Assignments/xargs/* ~/xargs
ls ~/xargs
cp ~cs252/Assignments/xargs/* ~/xargs
ls ~/xargs
find ~/xargs -name '*.bak' -print -exec rm {} \;
ls ~/xargs

Notice the use if -print to print the file name before passing it on to the command in the -exec action.

Closely related is -ok, which asks for permission before applying a command.

Example 13: Try This:
ls ~/xargs
find ~/xargs -name '*~' -ok rm {} \;
ls ~/xargs

Actually, because many commands, such as grep, can be used to test files for certain properties, -exec can actually be used to select files as well as to operate on them.

Example 14: Try This:
find /usr/include -type f -exec grep --quiet math {} \; -ls

There are many other possible actions as well. See the man page for details.