Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Using trap to Run a Command after Your Shell Script Exits

blog/cards/using-trap-to-run-a-command-after-your-shell-script-exits.jpg

This can be handy to delete temp files that your script creates or to always write a log file out if the script works or fails.

Quick Jump: Demo Video

For example, let’s say you have a pretty involved shell script that runs 5 different functions. Each of these functions have commands that might fail, such as using curl to get an HTTP response from a site or maybe gathering stats about a file that may or may not exist.

You might want to ensure that your script always writes a log entry but it would be really tedious to wrap each command that might fail with an if condition to call your log function. That’s a lot of boilerplate of using set +e && RUN_COMMAND && set -e and then potentially calling your log function with different arguments.

With trap you can solve this is in a very clean way, here’s an example script. First we’ll see what the code looks like with comments and then we’ll break down what trap is doing:

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

# Default values for our log entry.
PATH_INPUT="${1:-}"
PATH_EXISTS="false"
PATH_FILE_COUNT="-1"

# EXIT is special, it captures just about any way a program can exit.
trap "log" EXIT

# We can trap specific signals too, in this case INT == CTRL+C.
trap "echo 'I HEARD YOU LIKE CTRL+C'" INT

# This will not work, this is only shown here to go over why it doesn't work:
#
# Unix systems blocks being able to trap KILL (9) which makes sense. If a
# script could block it, a bad actor could prevent their script from exiting
# even if you forcefully kill it in the harshest way possible.
trap "echo 'WE CHEATED DEATH?'" KILL

# For this example we're logging in JSON format. A multi-line string is nice
# to keep things readable.
log() {
  cat << EOF
{
  "timestamp": "$(date +'%Y-%m-%dT%H:%M:%S')",
  "path": "${PATH_INPUT}",
  "path_exists": ${PATH_EXISTS},
  "path_file_count": ${PATH_FILE_COUNT}
}
EOF
}

# This command may or may not work depending on if the path exists, but since we
# have set -o errexit enabled we can be sure it exists if the `ls` command
# worked because the script would have halted if `ls` failed.
list_contents() {
  ls -la "${PATH_INPUT}"

  PATH_EXISTS="true"
}

# Likewise, PATH_FILE_COUNT will only be set with a value >= 0 when the path exists.
count_files() {
  PATH_FILE_COUNT="$(find "${PATH_INPUT}" -type f | wc -l)"
}

# Calling our functions.
list_contents
count_files

# The script will remain running forever until you exit it somehow. This lets
# us demonstrate trapping CTRL+C or other signals.
sleep infinity &
wait

So, the idea with trap is you can pass it a command to evaluate along with 1 or more signals to react to. I didn’t demo it in the script but you can pass in multiple signals per command such as trap "whoami" INT TERM. You can also chain commands together like trap "whoami && echo 'COOL STORY'" EXIT.

If you’re running bash you can run trap -l to get a full list of signals. If you’re running zsh you can run bash -c "trap -l" to get the list. The command is a bit different in both shells.

Overall we want to put our traps before any commands that might fail so they are defined. If we put the traps on the bottom of the script then set -e will halt the script before the traps are evaluated. That’s why I defined them near the top of the file.

I like this pattern because each function doesn’t need to worry about calling log and it’s a safe bet no matter what the script does it’ll get called.

Of course an exception here is KILL (9). There’s not much we can do here to guarantee something runs after this signal happens. In case you’re curious, if you do run shellcheck on the script it will warn you that KILL is untrappable.

This is a good reminder that you can’t always depend on something happening and you should at least think through scenarios on how your script can clean up after itself during an edge case such as the power being cut off on the machine with no warning or the system owner running a kill -9 <PID> on your script.

We only demo’d writing log entries but you can use trap in the same exact way for deleting temp files that your script uses or anything you want such as broadcasting the status of something to Slack in a pretty resilient way.

Personally I find myself using EXIT the most but I’m sure there’s a time and place for using more specific signals.

The video below goes into more detail and demos the script in various states.

Demo Video

Timestamps

  • 0:25 – A few use cases
  • 1:41 – Running the script without trap
  • 3:53 – Trapping all exit signals
  • 5:23 – Trapping CTRL+C (SIGINT) specifically
  • 6:42 – SIGKILL (kill -9) is untrappable on Unix / Linux
  • 8:18 – Trapping SIGTERM
  • 9:03 – Going over coding patterns on how to use trap
  • 13:35 – List all of the possible signals with trap

References

What was the last thing you used trap for? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments