Shell builtins · bash builtin
trap
Run a command when the shell receives a signal or exits, typically to clean up temporary files.
trap [-lp] [[ACTION] SIGNAL...]
trap 'commands' SIGNAL... registers code for the shell to run when a signal arrives
(INT from Ctrl-C, TERM from kill, HUP when the terminal closes) or at one of bash's
pseudo-signals: EXIT when the shell exits for any reason, ERR when a command fails, DEBUG
before every command and RETURN when a function returns.
The everyday pattern is cleanup: create a temporary file or directory, then trap 'rm -rf
"$tmp"' EXIT immediately, so it is removed whether the script finishes, fails under set
-e, or is interrupted. An EXIT trap is the one place to put "always do this at the end".
An action of '' (empty) makes the shell ignore the signal; - restores the default.
SIGKILL and SIGSTOP cannot be trapped. Traps are not inherited by subshells and
functions in the usual way (see the ERR example and set -E). Traps and signals covers
timing and signal-handling details in depth.
Examples
Clean up a temporary file whatever happens
#!/usr/bin/env bash
set -e
tmp=$(mktemp)
trap 'rm -f "$tmp"; echo "cleanup: removed $tmp"' EXIT
echo "working in $tmp"
sort data/fruits.txt > "$tmp"
grep -q kiwi "$tmp"
echo "this is not reached"
bash job.sh
echo "exit status: $?"
working in /tmp/tmp.R9fwTY8XGP cleanup: removed /tmp/tmp.R9fwTY8XGP exit status: 1
grep fails, set -e ends the script, and the EXIT trap still runs and deletes the
file. The script's exit status (1, from grep) passes through unchanged.
See the exit code inside an EXIT trap
#!/usr/bin/env bash
on_exit() {
local rc=$?
if (( rc == 0 )); then echo "finished OK"; else echo "FAILED with status $rc"; fi
}
trap on_exit EXIT
ls "$1" > /dev/null
bash report.sh data
bash report.sh nope; echo "caller sees $?"
finished OK ls: cannot access 'nope': No such file or directory FAILED with status 2 caller sees 2
At the start of the trap, $? is the status the shell is about to exit with. Save it
first thing, before any other command overwrites it. The trap does not change the exit
status unless it calls exit itself.
Handle Ctrl-C (SIGINT)
trap 'echo "caught SIGINT, stopping cleanly"; exit 130' INT
echo "working..."
kill -INT $$
echo "not reached"
working... caught SIGINT, stopping cleanly
kill -INT $$ sends the shell the same signal Ctrl-C would. The trap runs, prints its
message and exits with 130, the conventional status for "killed by SIGINT" (128 + 2).
Ignore a signal
bash -c '
trap "" TERM
kill -TERM $$
echo "still alive after SIGTERM"
trap - TERM
kill -TERM $$
echo "not reached"
'
echo "exit status: $?"
still alive after SIGTERM Terminated exit status: 143
An empty action makes the inner shell ignore the signal. trap - TERM restores the
default, after which the same signal kills it; the outer shell prints Terminated and reports status 143, the
convention for "killed by signal 15" (128 + 15).
Report the failing line with an ERR trap
trap 'echo "error: status $? at line $LINENO: $BASH_COMMAND" >&2' ERR
grep -q kiwi data/fruits.txt
ls data > /dev/null
false
echo "the ERR trap does not stop the shell"
error: status 1 at line 2: grep -q kiwi data/fruits.txt error: status 1 at line 4: false the ERR trap does not stop the shell
The ERR trap runs after every command that fails, under the same rules as set -e.
$LINENO and $BASH_COMMAND say where. On its own it only reports; combine it with
set -e to also stop.
Make functions inherit the ERR trap with set -E
trap 'echo "ERR trap fired at line $LINENO"' ERR
check() {
false
echo "check carries on after false"
return 3
}
check
echo "--- with set -E:"
set -E
check
echo "done"
check carries on after false ERR trap fired at line 7 --- with set -E: ERR trap fired at line 3 check carries on after false ERR trap fired at line 10 done
Without -E, the ERR trap is not inherited by functions: the failing false on line 3
goes unreported and only the failed call on line 7 triggers it. With set -E (errtrace)
the trap fires inside the function too (line 3), pointing at the real culprit, and again
for the failed call on line 10.
Show active traps with -p
trap 'rm -f /tmp/lock' EXIT
trap 'echo bye' INT TERM
trap -p
trap -- 'rm -f /tmp/lock' EXIT trap -- 'echo bye' SIGINT trap -- 'echo bye' SIGTERM
trap -p prints every trap as a command that would recreate it. trap -p EXIT shows
just one. Note that one trap call can set the same action for several signals.
A second trap on the same signal replaces the first
trap 'echo "first cleanup"' EXIT
trap 'echo "second cleanup"' EXIT
echo "exiting"
exiting second cleanup
Only one action per signal exists; setting another overwrites it. To run several
cleanups, put them in one function, or append by reading the current trap with
trap -p.
Single quotes delay expansion until the trap runs
file=first.tmp
trap "echo double quotes saw: $file" EXIT
file=second.tmp
trap -p EXIT
trap 'echo single quotes see: $file' EXIT
trap -- 'echo double quotes saw: first.tmp' EXIT single quotes see: second.tmp
With double quotes $file is expanded when the trap is set, freezing first.tmp into
it, as trap -p shows. With single quotes the variable is read when the trap fires, so
it sees the latest value.
List signal names and numbers
trap -l | head -n 4
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
-l prints the signal numbers and names the shell knows (the first lines shown here).
Names can be written with or without the SIG prefix.
Common pitfalls
Setting the cleanup trap after creating many resources: if the script dies in between, they leak. Set the trap right after creating the first temporary file.
Double quotes in the trap action expand variables when the trap is set, not when it runs. Use single quotes.
Reading
$?after another command in the trap handler: save it on the first line (local rc=$?).kill -9(SIGKILL) cannot be trapped, so no cleanup runs. SendTERMfirst.A trap set inside
$(...)or a( ... )subshell does not affect the parent shell, and EXIT traps of the parent are reset in subshells.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
trapwith signals andEXIT(or0) is POSIX.ERR,DEBUG,RETURN,-pand-lare bash (and partly ksh/zsh) features; dash only understands real signals andEXIT.
Options worth knowing
A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.
| Option | What it does |
|---|---|
-p | Print the current traps (all, or those for the named signals) in reusable form. |
-l | List signal names and their numbers. |
Related
killSend a signal to processes by PID or job number; by default TERM, which asks a process to exit.setTurn shell options such as -e, -u, -x and pipefail on or off, and replace the positional parameters.execReplace the shell with another program, or, with only redirections, change where the shell's own input and output go.mktempCreate a temporary file or directory with a unique, unpredictable name and print that name.- Traps and signalsRunning cleanup code with trap on EXIT, handling INT and TERM, the ERR pseudo-signal, and the timing and scoping rules that trip people up.
- Strict mode and its caveatsWhat set -euo pipefail actually does, the cases where -e silently does nothing, the traps in -u and pipefail, and a balanced way to use it.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.
- Subshells and command groupingThe difference between ( ... ) and { ...; }, what a subshell inherits and what it cannot change, which constructs create one, and what that costs.