The Bash Trap Command

 

If you've written any amount of bash code, you've likely come across the trap command. Trap allows you to catch signals and execute code when they occur. Signals are asynchronous notifications that are sent to your script when certain events occur. Most of these notifications are for events that you hope never happen, such as an invalid memory access or a bad system call. However, there are one or two events that you might reasonably want to deal with. There are also "user" events available that are never generated by the system that you can generate to signal your script. Bash also provides a psuedo-signal called "EXIT", which is executed when your script exits; this can be used to make sure that your script executes some cleanup on exit.

 

The man page for signal(7) describes all the available signals. The Wikipedia page for signal (IPC) has a bit more detail. As I mentioned, most of them are of little interest in scripts. The "SIGINT" signal is perhaps the only one that might be of interest in a script. SIGINT is generated when you type Ctrl-C at the keyboard to interrupt a running script. If you don't want your script to be stopped like this, you can trap the signal and remind yourself that interrupting the script is to be avoided. Although, as you'll see, this is less useful than one might hope. The "SIGUSR1" signal is a "user"-defined signal that you can use however you like. it is never generated by the system.

The most common use of the trap command though is to trap the bash-generated psuedo-signal named EXIT. Say, for example, that you have a script that creates a temporary file. Rather than deleting it at each place where you exit your script, you just put a trap command at the start of your script that deletes the file on exit:

tempfile=/tmp/tmpdata
trap "rm -f $tempfile" EXIT

Now whenever your script exits, it deletes your temporary file. The syntax for the trap command is "trap COMMAND SIGNALS...", so unless the command you want to execute is a single word, the "command" part should be quoted.

If your cleanup needs are complex, you don't have to try to jam it all into a string with semicolons, just write a function:

function cleanup()
{
    # ...
}

trap cleanup EXIT

Note that if you send a kill -9 to your script, it will not execute the EXIT trap before exiting.

The other possible thing that you might like to use the trap command for is to catch Ctrl-C so that your script can't be interrupted or perhaps so you can ask if the user really wants to interrupt the process. As an example, I'll use the following handler function, which warns the user on the first two Ctrl-Cs and then exits on the third:

ctrlc_count=0

function no_ctrlc()
{
    let ctrlc_count++
    echo
    if [[ $ctrlc_count == 1 ]]; then
        echo "Stop that."
    elif [[ $ctrlc_count == 2 ]]; then
        echo "Once more and I quit."
    else
        echo "That's it.  I quit."
        exit
    fi
}

Use the following to test the handler:

trap no_ctrlc SIGINT

while true
do
    echo Sleeping
    sleep 10
done

If you run that and type Ctrl-C three times, you'll get the following output:

$ bash noctrlc.sh
Sleeping
^C
Stop that.
Sleeping
^C
Once more and I quit.
Sleeping
^C
That's it.  I quit.
$

My first shot at the test script had the sleep 10 as the while condition:

trap no_ctrlc SIGINT

while sleep 10
do
    echo Sleeping
done

But that didn't work. After a bit of thought, I realized it's because when the trap command returns, it does not resume the "sleep" command at the point it was interrupted, nor does it restart the "sleep" command, rather it returns to the next command after the command that was interrupted, which in this case is whatever follows the while loop. So the loop ends and the script exits normally.

This is an important point: interrupted commands are not restarted. So if your script needs to do something important that shouldn't be interrupted, then you can't, for example, use the trap command to trap the signal, print a warning, and then resume the operation like nothing happened. Rather, what you need to do if you can't have something interrupted is disable Ctrl-C handling while the command executes. You can do this with the trap command too by specifying an empty command to trap. You can also use the trap command to reset signal handling to the default by specifying a "-" as the command. So you might do something like this:

# Run something important, no Ctrl-C allowed.
trap "" SIGINT
important_command

# Less important stuff from here on out, Ctrl-C allowed.
trap - SIGINT
not_so_important_command

So unless your script has long moments when it's just waiting, trapping signals and actually doing something may not provide the experience you hoped for.

The final thing I want to look at is trapping user-defined signals to a script. Say that I want to monitor the system log and count the number of times that sudo is run, and I want to run the script in the background and then send it a signal whenever I want it to display the count:

nopens=0
function show_opens()
{
    echo "Seen $nopens sudo session opens"
}
trap show_opens USR1

sudo journalctl -f | while read line
do
    if [[ $line =~ sudo.*session.*opened ]]; then
        let nopens++
    fi
done

What this does is pipes the output from journalctl (i.e., the system log) to the read command in the loop. Inside the loop, the if-statement checks to see if the line is a sudo command. If so, it increments a counter. The code before the loop sets a trap for the SIGUSR1 signal, and when it's received, the "show_opens" functions prints out the number of sudo commands seen since the script was started. You can send the SIGUSR1 signal to the script with the kill command:

$ bash bkgnd.sh &
[1] 1000
$ kill -SIGUSR1 1000

Unfortunately, once again, this failed to work. The first problem I discovered, which I recently mentioned in my post on Job Control is that if the sudo command needs to prompt for a password, the script becomes suspended just after starting.

Tip: Reset sudo's Password Timestamp

If you need to test something with sudo and want to make sure that everything is working both when sudo prompts for a password and when it does not prompt for a password, execute the command sudo -k to reset sudo's password timestamp. After executing sudo with the -k option, sudo will once again ask for a password the next time it's run, regardless of how recently you entered a password.

After figuring out the suspended background problem, I figured all systems were "go", but not so, still nothing. The problem now is because the loop is taking input from a pipe. The original bash process has now executed one sub-process for "journalctl" and another sub-process for "while read line ...". When bash executes a command, per the man page:

traps caught by the shell are reset to the values inherited from the shell's parent

So when these sub-processes are started, the SIGUSR1 trap is reset and no longer has an effect on the process. To get this to work, we need to set the trap inside the loop so that it is part of the sub-process:

nopens=0
function show_opens()
{
    echo "Seen $nopens sudo session opens"
}

sudo journalctl -f | while read line
do
    if [[ -z "$trap_set" ]]; then
        trap_set=1
        echo "Trap set in $BASHPID"
        trap show_opens USR1
    fi
    if [[ $line =~ sudo.*session.*opened ]]; then
        let nopens++
    fi
done

Note that I use $BASHPID to get the process of the sub-process ($$ always returns the process id of the original shell).

And now it works:

$ bash bkgnd.sh &
[1] 1000
Trap set in 1002
$ kill -SIGUSR1 1002
Seen 1 sudo session opens
$ sudo ls
...
$ kill -SIGUSR1 1002
Seen 2 sudo session opens

In the end, I can't say that I'll likely be trapping SIGINT or any other signals beyond EXIT on a regular basis, but I can say that I discovered some interesting subtleties about bash in the process of making these examples work.

# Copyright 2019 Mitch Frazier <mitch -at- linuxjournal.com>
#
# This software may be used and distributed according to the terms of the
# MIT License or the GNU General Public License version 2 (or any later version).

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.

Load Disqus comments