1. Overview

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

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

2. The -exec Action

The find command has two main parts to it: 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 would define 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 would 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.

Imagine we would like to run the file command on the list of mp3 files we just found to determine their filetype, we could 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 includes:

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

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 is not 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 could export them and call bash -c with our requested function on our file.

An example is in order. 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’ll try to run the mp3info command on all of our files, -exec will complain 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 filenames 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 run both the basename as well as the file commands. In this case, to allow us to concatenate the commands, we spawn 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 show the usage 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’ll 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.

We might prefer using the semi-colon delimiter in one of the following cases:

  • The tool run by -exec does not 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 will take more time to get all the results

6. Conclusion

To conclude, we showed 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.

guest
0 Comments
Inline Feedbacks
View all comments