If you’ve written any amount of bash code, chances are you’ve come across the trap command. Trap allows you to capture signals and execute code when they occur. Signals are asynchronous notifications that are sent to the script when certain events occur. Most of these notifications are for events that you hope will never happen, such as invalid memory access or an incorrect system call. However, there are one or two events that it is reasonable to 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 the script exits; This can be used to make sure your script runs some cleanup on exit.
The signal(7) man page describes all available signals. The Wikipedia page for the signal (IPC) has a little more detail. As I mentioned, most of them are of little interest in scripts. The “SIGINT” signal is perhaps the only one that could be of interest in a script. SIGINT is generated when you type Ctrl-C on the keyboard to interrupt a running script. If you don’t want your script to stop like this, you can catch the signal and remind yourself to avoid interrupting the script. Although, as you’ll see, this is less useful than one might expect. The “SIGUSR1” signal is a “user” defined signal that you can use as you wish. It is never generated by the system.
However, the most common use of the trap command is to trap the psuedo signal generated by bash called EXIT. Let’s say, for example, you have a script that creates a temporary file. Instead of deleting it every place you exit your script, simply place a capture command at the beginning of
your script that deletes the file on exit:
Now, every time your script exits, it deletes its temporary file. The syntax of the trap command is “trap COMMAND SIGNALS…”, so unless the command you want to execute is a single word, the “command” part must be cited.
If your cleaning needs are complex, you don’t have to try to jam everything in a string with semicolons, just type in a function:
Note that if you send a kill -9 to your script, it will not run the EXIT trap before exiting.
The other possible thing you’d like to use the trap command for is to capture Ctrl-C so your script can’t be interrupted or maybe so you can ask if The user really wants to interrupt the process. As an example, I will use the following controller function, which warns the user
in the first two Ctrl-C and then comes out in the third: Use the following to test the driver:
If you run that and type Ctrl-C three times, you will get the following result
My first shot in the test script had sleep 10 as condition while:
But that didn’t work. After some thought, I realized that it’s because when the trap command returns, it doesn’t resume the “sleep” command at the point where it was interrupted, nor does it restart the “sleep” command, but instead returns to the next command after the command that was interrupted, which in this case is what 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 cannot, for example, use the capture command to capture the signal, print a warning, and then resume operation as if nothing happened. Rather, what you should do if you can’t interrupt something is to disable Ctrl-C handling while the command is running. You can also do this with the capture command by specifying an empty command to capture. You can also use the capture command to reset signal control to the default value by specifying a “-” as the command. So you could do something like this: So
unless your script has long moments where it’s just waiting, catching signals and actually doing something may not provide the experience you were hoping for
The last thing I want to see is catch user-defined signals in a script. Let’s say I want to monitor the system log and count the number of times sudo runs, and I want to run the script in the background and then send it a signal every time I want it to show the count:
What this does is pipe the output from journalctl (i.e. the system log) to the read command in the loop. Within the loop, the if-statement checks whether the line is a sudo command. If so, increment a counter. The code before the loop sets a trap for the SIGUSR1 signal, and when it is received, the “show_opens” functions print the number of sudo commands seen since the script was started. You can send the SIGUSR1 signal to the script with the command kill:
Unfortunately, once again, this did not work. The first issue 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 is suspended right after it starts.
After discovering the problem of the suspended background, I thought that all systems were “ready”, but it is not so, still nothing. The problem now is because the loop is taking input from a pipe. The original bash process has now run one thread for “journalctl” and another thread for “while read line…”. When bash executes a command, according to the man page:
traps captured by the shell are reset to values inherited from the shell parent
So when these threads are started, the SIGUSR1 trap is reset and no longer has an effect on the process. For this to work, we need to set the trap inside the loop to be part
of the thread: Note that I
use $BASHPID to get the thread process ($$ always returns the original shell process ID).
And now it works:
In the end, I
can’t say that I will probably catch SIGINT or any other signal 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.