Bash Signal Handler and Traps

I would like to launch ssh-agent and add my private key at the beginning of my shell script and stop it before leaving the shell script. How do I guarantee that ssh-agent will be shutted down properly? What will happen if the user press Ctrl-C or even kill the shell script?

The answer to this question is the trap built-in utility from the shell command language. It allows the programmer to specify:

  1. Signal handlers
  2. Deferred tasks -- Defer the tasks that should be executed before leaving the shell script (no matter it is interrupted or exitted normally.)

In this post, I would like to give a brief introduction to trap built-in.

Motivating Example

The syntax for trap built-in is:

trap [action] [condition]

The action will be executed (via eval) when the specified condition occurred.

The condition can be:

  • EXIT -- The action will be executed whenever the shell script stops (no matter it is interrupted or leaving normally.)
  • INT -- The action will be executed whenever the user pressed Ctrl-C or the process received SIGINT signal.
  • Other signal names defined in <signal.h>.

For example, int.sh contains following code:

#!/bin/bash
trap "echo \"User abort\"; exit 1" INT
sleep 10

Run the script and press Ctrl-C:

$ chmod +x int.sh
$ ./int.sh
# ... press ctrl-c ...
^CUser abort

Compare with another case: run the script and wait for 10 seconds:

$ ./int.sh
# ... wait for 10 seconds (no output) ...

Let's look at another example. The following is the code listing of defer.sh:

#!/bin/bash
trap "echo \"EXIT\"" EXIT    # <-- Changed to EXIT
sleep 10

We can notice that echo "EXIT" will always be executed:

$ ./defer.sh
# ... press ctrl-c ...
^CEXIT

$ ./defer.sh
# ... wait for 10 seconds ...
EXIT

To wrap up:

  1. To perform a task when an interrupt occurs, register a trap with INT.
  2. To perform a cleanup task, register a trap with EXIT.

SSH Agent

OK. Now, let's work on a real world example: start and stop a SSH agent. To start a SSH agent and export the environment variables, we have to run:

$ eval $(ssh-agent -s)
Agent pid 10176

On the other hand, to stop a SSH agent:

$ eval $(ssh-agent -k)
Agent pid 10176 killed

We can combine them together in the shell script:

#!/bin/bash

kill_ssh_agent () {
  eval $(ssh-agent -k)
}

# Register a trap to stop the ssh agent.
trap kill_ssh_agent EXIT

# Start the ssh agent.
eval $(ssh-agent -s)

# Load private key to ssh agent.
ssh-add ~/.ssh/id_rsa

# ... other commands to run ...

Try to run the script:

$ ./run.sh
Agent pid 10248
Enter passphrase for /home/username/.ssh/id_rsa:
Identity added: /home/username/.ssh/id_rsa (/home/username/.ssh/id_rsa)
Agent pid 10248 killed

Great! Everything works as expected.

Conclusion

To sum up, we introduced the basic usage of trap built-in in this post. We mentioned two different trap conditions: INT and EXIT. The former will trigger the action when the shell script is interrupted, and the later will trigger the action before leaving the shell script. At last, we gave a real world example on ssh-agent.

Hope you enjoy this post. See you then.

Reference