1. Overview

In this tutorial, we’ll explore the -exec argument of the Linux find command. This argument extends find‘s capabilities, and makes it the swiss-army knife that it’s known to be.

We’ll discuss the use of -exec to execute commands and shell functions, as well as how to control them to improve the efficacy of their execution.

2. The -exec Action

The find command is comprised of two main parts, the expression and the action.

When we initially use find, we usually start with the expression part. This is the part that allows us to specify a filter that defines which files to select.

A classic example would be:

$ find Music/ -name *.mp3
Music/Gustav Mahler/01 - Das Trinklied vom Jammer der Erde.mp3
Music/Gustav Mahler/02 - Der Einsame im Herbst.mp3
Music/Gustav Mahler/03 - Von der Jugend.mp3
Music/Gustav Mahler/04 - Von der Schönheit.mp3
Music/Gustav Mahler/05 - Der Trunkene im Frühling.mp3
Music/Gustav Mahler/06 - Der Abschied.mp3

This command will result in a list of mp3 files in the Music directory and all its subdirectories.

The action part in this example is the default action, -print. This action prints the resulting paths with newline characters in between. It’ll run if no other action is specified.

In contrast, the -exec action allows us to execute commands on the resulting paths.

Let’s say we want to run the file command on the list of mp3 files we just found to determine their filetype. We can achieve this by running the following command:

$ find Music/ -name *.mp3 -exec file {} \;
Music/Gustav Mahler/01 - Das Trinklied vom Jammer der Erde.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Music/Gustav Mahler/02 - Der Einsame im Herbst.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Music/Gustav Mahler/03 - Von der Jugend.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Music/Gustav Mahler/04 - Von der Schönheit.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Music/Gustav Mahler/05 - Der Trunkene im Frühling.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
Music/Gustav Mahler/06 - Der Abschied.mp3:
  Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo

Let’s dissect the arguments passed to the -exec flag, which include:

  1. A command: file
  2. A placeholder: {}
  3. A command delimiter: \;

Now we’ll walk through each of these three parts in-depth.

3. The Command

Any command that can be executed by our shell is acceptable here.

We should note that this isn’t our shell executing the command, rather we’re using Linux’s exec directly to execute the command. This means that any shell expansion won’t work here, as we don’t have a shell. Another effect is the unavailability of shell functions or aliases.

As a workaround for our missing shell functions, we can export them and call bash -c with our requested function on our file.

To see this in action, we’ll continue with our directory of Mahler’s mp3 files. Let’s create a shell function that shows the track name and some details about the quality:

function mp3info() {
    TRACK_NAME=$(basename "$1")
    FILE_DATA=$(file "$1" | awk -F, '{$1=$2=$3=$4=""; print $0 }')
    echo "${TRACK_NAME%.mp3} : $FILE_DATA"
}

If we try to run the mp3info command on all of our files, -exec will complain that it doesn’t know about mp3info:

find . -name "*.mp3" -exec mp3info {} \;
find: ‘mp3info’: No such file or directory

As mentioned earlier, to fix this, we’ll need to export our shell function and run it as part of a spawned shell:

$ export -f mp3info
$ find . -name "*.mp3" -exec bash -c "mp3info \"{}\"" \;
01 - Das Trinklied vom Jammer der Erde :      128 kbps  44.1 kHz  Stereo
02 - Der Einsame im Herbst :      128 kbps  44.1 kHz  Stereo
03 - Von der Jugend :      128 kbps  44.1 kHz  Stereo
04 - Von der Schönheit :      128 kbps  44.1 kHz  Stereo
05 - Der Trunkene im Frühling :      128 kbps  44.1 kHz  Stereo
06 - Der Abschied :      128 kbps  44.1 kHz  Stereo

Note that because some of our file names hold spaces, we need to quote the results placeholder.

4. The Results Placeholder

The results placeholder is denoted by two curly braces {}.

We can use the placeholder multiple times if necessary:

find . -name "*.mp3" -exec bash -c "basename \"{}\" && file \"{}\" | awk -F: '{\$1=\"\"; print \$0 }'" \;
01 - Das Trinklied vom Jammer der Erde.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
02 - Der Einsame im Herbst.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
03 - Von der Jugend.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
04 - Von der Schönheit.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
05 - Der Trunkene im Frühling.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo
06 - Der Abschied.mp3
  Audio file with ID3 version 2.4.0, contains MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, Stereo

In the above example, we ran both the basename, as well as the file commands. To allow us to concatenate the commands, we spawned a separate shell, as explained above.

5. The Delimiter

We need to provide the find command with a delimiter so it’ll know where our -exec arguments stop.

Two types of delimiters can be provided to the -exec argument: the semi-colon(;) or the plus sign (+).

As we don’t want our shell to interpret the semi-colon, we need to escape it (\;).

The delimiter determines the way find handles the expression results. If we use the semi-colon (;), the -exec command will be repeated for each result separately. On the other hand, if we use the plus sign (+), all of the expressions’ results will be concatenated and passed as a whole to the -exec command, which will run only once.

Let’s see the use of the plus sign with another example:

$ find . -name "*.mp3" -exec echo {} +
./Gustav Mahler/01 - Das Trinklied vom Jammer der Erde.mp3 ./Gustav Mahler/02 -
  Der Einsame im Herbst.mp3 ./Gustav Mahler/03 - Von der Jugend.mp3 ./Gustav Mahler/04 -
  Von der Schönheit.mp3 ./Gustav Mahler/05 - Der Trunkene im Frühling.mp3
  ./Gustav Mahler/06 - Der Abschied.mp3

When running echo, a newline is generated for every echo call, but since we used the plus-delimiter, only a single echo call was made. Let’s compare this result to the semi-colon version:

$ find . -name "*.mp3" -exec echo {} \;
./Gustav Mahler/01 - Das Trinklied vom Jammer der Erde.mp3
./Gustav Mahler/02 - Der Einsame im Herbst.mp3
./Gustav Mahler/03 - Von der Jugend.mp3
./Gustav Mahler/04 - Von der Schönheit.mp3
./Gustav Mahler/05 - Der Trunkene im Frühling.mp3
./Gustav Mahler/06 - Der Abschied.mp3

From a performance point of view, we usually prefer to use the plus-sign delimiter, as running separate processes for each file can incur a serious penalty in both RAM and processing time.

However, we may prefer using the semi-colon delimiter in one of the following cases:

  • The tool run by -exec doesn’t accept multiple files as an argument.
  • Running the tool on so many files at once might use up too much memory.
  • We want to start getting some results as soon as possible, even though it’ll take more time to get all the results.

6. Conclusion

In this article, we learned how to use the -exec argument when running the find command in Linux. We explained the separate parts of this argument, and how we can use them to run our commands efficiently.


« 上一篇: Bash脚本中IFS的含义
» 下一篇: 使用Vim寄存器