Defer in Bash


I’ve been writing lots of shell scripts in BASH recently. Along the way, I learnt a new pattern to run a program and ensure it’s cleaned up before the script stops. The key command is:

TRAP(1)

NAME
       trap - perform an action when the shell receives a signal

SYNOPSIS
       trap [OPTIONS] [[ARG] REASON ... ]

Example

It’s pretty simple, just open up a terminal, put in the following statement, and then hit Ctrl-D to exit.

cleanup() {
    echo "Cleaning up"
}
trap cleanup EXIT

You will observe the message Cleaning up appearing on your screen. EXIT is a pseudo-signal in BASH. When you trap it, the specified command will be executed when the script is about to exit, regardless of the exit status.

bash-5.2$ cleanup() {
    echo "Cleaning up"
}
bash-5.2$ trap cleanup EXIT
bash-5.2$
exit
Cleaning up

Kill child process before exit

The following is a more common scenario.

  1. First, we set up a slice PIDS and use the cleanup function to handle an EXIT signal.
  2. We then fire up two programs, p1 and p2, in the background.
  3. And if something makes the shell script crash or the user puts a stop to it, the cleanup function will kick in and stop the background programs p1 and p2.
PIDS=()
cleanup() {
  echo "Cleaning up"
  for pid in ${PIDS[@]}; do
    echo "Killing ${pid}"
    sudo kill -9 ${pid}
  done
}
trap cleanup EXIT

start_p1() {
  ./bin/p1 &> p1.log &
  PID=$!
  PIDS+=(${PID})
  echo "p1 PID: ${PID}"
}

start_p2() {
  ./bin/p2 &> p2.log &
  PID=$!
  PIDS+=(${PID})
  echo "p2 PID: ${PID}"
}

start_p1
start_p2

# Do something with p1 and p2.
# If anything goes wrong and it exits early, `p1` and `p2` will be cleaned up.