Linux Basic — trap Command

4 minute read

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)
  • $LINENO here refers to where the trap was set unless you pass it in; the $LINENO token 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.


#!/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 $tmpdir on 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

Updated: