Linux Basic — trap Command
trap Command
trap lets you run code when the script exits, when a command fails (under set -e), or when the script receives a signal like SIGINT. Together with strict mode, traps are the foundation of self-cleaning scripts.
Trap Syntax
trap 'COMMANDS' SIGNAL [SIGNAL ...]
The COMMANDS string is run by the shell whenever any listed signal/pseudo-signal fires.
The signals (and pseudo-signals) you’ll actually use:
| Name | Fires when |
|---|---|
EXIT |
the script is about to exit (any reason) |
ERR |
a command exits non-zero (under set -e) |
INT |
SIGINT — Ctrl-C |
TERM |
SIGTERM — kill default |
HUP |
SIGHUP — terminal closed |
DEBUG |
before every command (rarely useful) |
Pattern 1 — Cleanup Temp Files on EXIT
The single most useful trap:
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d)
trap 'rm -rf -- "$tmpdir"' EXIT
# … use $tmpdir freely; it's cleaned up on every exit path
cp data.csv "$tmpdir/"
process "$tmpdir/data.csv"
EXIT runs on:
- Normal end of script
- Explicit
exit - Errors with
set -e - SIGINT, SIGTERM, etc. (unless trapped separately)
So one trap … EXIT covers all cleanup. No need for trap … INT TERM ERR EXIT redundantly.
Always use -- in rm -rf to prevent paths starting with - from being misread as flags. And always quote $tmpdir to handle spaces.
Pattern 2 — Multiple Cleanups
Each trap … EXIT replaces the previous one. For multiple cleanups, write a single handler:
cleanup() {
rm -rf "$tmpdir"
[[ -n "$lockfile" ]] && rm -f "$lockfile"
kill "$bg_pid" 2>/dev/null || true
}
trap cleanup EXIT
Or use the additive idiom (works but ugly):
trap 'rm -rf "$tmpdir"' EXIT
trap "$(trap -p EXIT | sed 's/^trap -- //;s/ EXIT$//') ; rm -f \"\$lockfile\"" EXIT
The handler-function approach is much cleaner.
Pattern 3 — Report on Error (trap … ERR)
set -eE # -E makes ERR trap inheritable
on_error() {
local rc=$?
local line=$1
echo "ERROR: exit $rc at line $line" >&2
}
trap 'on_error $LINENO' ERR
ERR fires whenever a command would cause set -e to abort. Inside the handler:
$?is the failing exit code (capture as the very first thing)$LINENOhere refers to where the trap was set unless you pass it in; the$LINENOtoken in the trap string is evaluated when the trap fires, so it gives the failing line
Combined diagnostic header (great for ops scripts):
set -eEuo pipefail
err_report() {
local rc=$?
local line=$1
local cmd=$2
echo "----------------------" >&2
echo "ERROR (exit $rc) at $0:$line" >&2
echo " command: $cmd" >&2
echo "----------------------" >&2
}
trap 'err_report $LINENO "$BASH_COMMAND"' ERR
$BASH_COMMAND is the command that just ran (or is about to run). Inside an ERR trap it’s the command that failed.
Pattern 4 — Handle Ctrl-C Gracefully
Without a trap, Ctrl-C kills the script immediately — no EXIT trap on some shells, no cleanup. Trap INT explicitly:
trap 'echo "Interrupted — cleaning up"; exit 130' INT
Returning exit code 130 (= 128 + 2 = SIGINT) follows convention so callers can tell “the user cancelled me” apart from “I failed.”
Note: with trap … EXIT set, the EXIT trap will run on SIGINT too, so cleanup happens automatically — the INT trap is only needed if you want a custom message or different exit code.
Pattern 5 — Lock File With Auto-Cleanup
lockfile=/var/lock/mybackup.lock
if ! ( set -o noclobber; > "$lockfile" ) 2>/dev/null; then
echo "Already running" >&2
exit 1
fi
trap 'rm -f "$lockfile"' EXIT
# ... do the work ...
set -o noclobber + > is atomic — if the file already exists, the redirect fails. The trap guarantees the lock is cleared on any exit, including kills.
(For production locking, prefer flock — it’s race-free; covered in Practical Patterns-adjacent material in later chapters.)
Inspect Current Traps
trap # list all current traps
trap -p EXIT # print trap for EXIT
trap -p # print all
Useful when debugging “is my cleanup actually registered?”
Disable a Trap
trap - EXIT # remove the EXIT trap
trap '' EXIT # set to no-op (won't run, but is "trapped" — useful to ignore signals)
trap '' INT is a way to make a critical section ignore Ctrl-C (use sparingly).
Trap-in-Subshells Caveat
Traps are inherited by subshells, but child processes (like bash -c '…' or pipelines with subshells) get their own trap context that may not see your settings as expected. The simple rule: set traps in your main script and don’t rely on them inside (...) subshells unless you re-set them.
A Real Example — Full-Featured Script Skeleton
#!/usr/bin/env bash
set -eEuo pipefail
IFS=$'\n\t'
tmpdir=$(mktemp -d)
cleanup() {
rm -rf -- "$tmpdir"
}
trap cleanup EXIT
err_report() {
local rc=$? line=$1 cmd=$2
echo "ERROR (exit $rc) at $0:$line: $cmd" >&2
}
trap 'err_report $LINENO "$BASH_COMMAND"' ERR
trap 'echo "Interrupted"; exit 130' INT
# --- main script body ---
cp data.csv "$tmpdir/"
process "$tmpdir/data.csv"
echo "Done."
This skeleton gives you:
- Auto-cleanup of
$tmpdiron every exit path - A clear error report with line and command on any failure
- Graceful Ctrl-C handling with exit code 130
- Strict mode catching typos and silent failures
Copy this for every serious script.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
trap "rm -rf $tmpdir" EXIT (double quotes) |
trap 'rm -rf -- "$tmpdir"' EXIT |
$tmpdir expands NOW, not at trap time |
Setting multiple trap … EXIT and expecting them all to fire |
combine into one handler function | each replaces the last |
Using trap … ERR without set -E in functions |
set -eE (or set -E if not using -e) |
ERR doesn’t inherit into functions without -E |
Forgetting rm -f (will fail if file gone) |
rm -f or \|\| true |
second-run cleanup might fail |
Calling exit inside ERR trap without saving $? first |
save $? on the very first line |
exit’s own code overwrites it |
Next → Custom Error Functions