Scripts that create temp files, start services, or change state have a duty: clean up on every exit path — success, error, Ctrl+C, or kill. bash's trap builtin is how you keep that promise, and one line covers almost every case.

The one-line version #

The cleanup trap every script deserves
#!/usr/bin/env bash
set -euo pipefail

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

  # ... work with $tmpdir, safe on every exit path ...

trap 'commands' SIGNAL registers commands to run when that signal arrives. EXIT fires when the script ends for any reason — normal exit, an error under strict mode, Ctrl+C, kill. One line, total coverage: that is why this pattern is the answer nine times out of ten.

Signals you will actually trap #

SignalArrives whenTypical use
EXIT (0)Script ends, any wayCleanup — the default
INT (2)Ctrl+CGraceful stop + cleanup
TERM (15)kill defaultSame, for service-like scripts
HUP (1)Terminal closedSame family

EXIT covers INT and TERM implicitly — when the script dies from a signal, the EXIT trap still runs. You only trap INT/TERM explicitly to add behaviour beyond cleanup, like printing "interrupted" or shutting down a child process first:

Stop a background process on the way out
srv_pid=$(start_server)
trap 'kill "$srv_pid" 2>/dev/null' EXIT

The details that matter #

  • Single quotes in the trap body delay expansion: 'rm -rf "$tmpdir"' reads $tmpdir at trap-fire time, when it holds the right value. Double quotes expand immediately — sometimes what you want, usually not.
  • Traps run before the script finishes exiting, so variables are still in scope.
  • trap - EXIT removes a trap — useful for scoped sections: set it, do the risky work, clear it.
  • A trap can change the exit code by exiting itself — leave it alone unless you mean to.

The rollback shape #

Undo on failure, commit on success
acquire_lock
trap 'release_lock; echo "aborted" >&2' EXIT

if ! upload; then
  exit 1          # trap releases the lock
fi
trap - EXIT        # success: keep the lock deliberately

That set-trap/clear-trap bracket is the general pattern: provisional state, made permanent only after every step survived.

Temp files via mktemp + EXIT trap is the minimal safe recipe; the signals vocabulary behind INT and TERM lives in the process notes. The scripting series continues.