You are on page 1of 6

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) (http://man7.org/linux/man-pages/man7/signal.7.html)


describes all the available signals. The Wikipedia page for signal (IPC)
(https://en.wikipedia.org/wiki/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
(https://www.linuxjournal.com/content/job-control-bash-feature-you-only-think-you-
dont-need) 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.

You might also like