Skip to content

Shell Trap with trap


Using $$ for temporary filenames seems convenient, but it has a real weakness: process IDs can be reused, so the filename isn’t guaranteed to be unique. mktemp solves the safe temporary-file creation problem, while trap solves the cleanup problem — together they give you a reliable way to create temporary resources and remove them when the script exits, whether it succeeds, fails, or is interrupted.

trap Basics

trap registers commands to run when a signal — or one of a few special pseudo-signals Bash provides — occurs:

trap 'echo "Script is exiting"' EXIT
echo "Doing work"
./script.sh
Doing work
Script is exiting

Notice the trap command is single-quoted. This matters : single quotes prevent expansion at the moment the trap line itself runs, so any variables referenced inside are expanded later, when the trap actually fires — not immediately, when it’s merely being registered. Getting this backwards is a genuine, common bug, worked through in full in this chapter’s Shell-Safety Considerations section.

EXIT — Running Cleanup No Matter How A Script Ends

EXIT is a pseudo-signal Bash provides that fires whenever the script terminates — normal completion, an explicit exit call, or even a failure that triggers set -e from the previous chapter. It’s the one trap you can generally count on running regardless of why the script stopped:

trap 'echo "Cleaning up"' EXIT
echo "About to fail"
exit 1
About to fail
Cleaning up

Cleanup still ran, even though the script exited with a failure status. This reliability is exactly what makes EXIT the right place to put resource cleanup.

mktemp — Generating Safe Temporary Files And Directories

mktemp creates a genuinely unique temporary file (or, with -d, directory), sidestepping the PID-reuse weakness earlier chapter flagged with the $$-based naming approach:

temp_file=$(mktemp)
echo "Created: $temp_file"

temp_dir=$(mktemp -d)
echo "Created: $temp_dir"
Created: /tmp/tmp.X7fK2pQr9s
Created: /tmp/tmp.b3nM8wLp1t

Each call generates a fresh, collision-resistant name — no risk of accidentally reusing a path from a previous, possibly-crashed run.

The Standard Pattern: mktemp + trap ... EXIT

Combined, these two form the idiomatic Bash pattern for any script that needs temporary storage:

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

echo "Working in $temp_dir"
echo "sample data" > "$temp_dir/data.txt"

Whatever happens for the rest of the script — it completes normally, it fails partway through, set -e kicks in and stops it early — the EXIT trap guarantees $temp_dir gets removed on the way out. This is worth adopting as a fixed habit anywhere a script creates a temporary file or directory: mktemp, immediately followed by a trap ... EXIT that cleans it up.

ERR — Reacting To Any Command Failure

ERR fires whenever a command fails, under essentially the same rules set -e uses to decide whether a failure “counts” — a command inside an if condition or on the left of &&/|| won’t trigger it, for the same reasons covered in the previous chapter:

trap 'echo "Error on line $LINENO"' ERR
false
echo "This still runs unless set -e is also active"
Error on line 2
This still runs unless set -e is also active

$LINENO — introduced here for the first time — holds the current line number, making it a genuinely useful diagnostic detail to include in an ERR handler, pointing you (or whoever’s reading the script’s output) directly at where things went wrong.

The Limitations Of ERR Traps

Two limitations are worth knowing up front. First, ERR shares every one of set -e’s documented exceptions from the previous chapter — if a failure wouldn’t trigger set -e, it won’t trigger an ERR trap either, since both mechanisms use the same underlying logic to decide what “counts” as a failure.

Second, and less obvious: an ERR trap does not automatically apply inside functions, unless set -o errtrace (equivalently, set -E) is also enabled:

set -o errtrace
trap 'echo "Error on line $LINENO"' ERR

fail_inside_function() {
    false
}

fail_inside_function

Without set -o errtrace, a failure inside fail_inside_function would silently not trigger the ERR trap at all — worth remembering for any script that relies on ERR for logging or alerting and also uses functions, which by this point in the course is essentially every non-trivial script you’d write.

INT And TERM — Handling Signals

INT (sent by Ctrl+C) and TERM (the default signal kill sends) are real operating-system signals, not Bash-specific pseudo-signals like EXIT and ERR. Trapping them lets a script respond to an interruption gracefully instead of dying wherever it happened to be:

cleanup() {
    echo "Cleaning up before exit"
}
trap cleanup EXIT

handle_interrupt() {
    echo "Interrupted — exiting gracefully"
    exit 1
}
trap handle_interrupt INT TERM

One command can trap multiple signals at once, as shown with INT TERM above. Structuring it this way — a signal handler that calls exit, with the actual cleanup logic living in a separate EXIT trap — avoids duplicating cleanup code across every signal you handle: since calling exit from inside the INT/TERM handler itself triggers the EXIT trap in turn, cleanup only ever needs to be written once.

Note

SIGKILL (kill -9) can never be trapped or handled by any process, by design at the operating-system level — it’s the one termination method nothing in this chapter can intercept. Rely on EXIT and the ordinary signals for graceful cleanup; there’s no way to guarantee cleanup against a kill -9.

Removing A Trap

trap - SIGNAL resets a signal back to its default (untrapped) behavior:

trap - EXIT

Best Practices

  • Always single-quote a trap’s command string (or reference a named function instead), so any variables inside are evaluated when the trap actually fires, not when it’s registered — the exact bug demonstrated in full below.
  • Use mktemp (or mktemp -d) paired immediately with a trap ... EXIT for any script that creates temporary files or directories — treat this as a fixed, default pattern.
  • Keep real cleanup logic in the EXIT trap alone, and have INT/TERM handlers simply log and call exit, letting that trigger EXIT in turn, rather than duplicating cleanup across multiple trap handlers.
  • Enable set -o errtrace if your script uses ERR traps and also defines functions — which, by this point in the course, is essentially always.

Shell-Safety Considerations

Here’s the double-quoting mistake mentioned earlier in this chapter, made concrete — and it’s a genuinely easy one to write by accident, since double quotes are the safer default essentially everywhere else in this course.

Naive (broken) version — the trap command is double-quoted, so $temp_dir is expanded immediately, at registration time, before it’s even been assigned a real value:

temp_dir=""
trap "rm -rf $temp_dir" EXIT
temp_dir=$(mktemp -d)
echo "Working in: $temp_dir"
ls "$temp_dir"
Working in: /tmp/tmp.qR8mN2xLp4

The script finishes, and the EXIT trap fires — but the trap’s command was baked in as rm -rf (with $temp_dir substituted as an empty string, since that’s what it held at the moment trap was called), not rm -rf /tmp/tmp.qR8mN2xLp4. The temp directory is never actually removed — it leaks silently, with no error or indication anything went wrong.

Corrected version — single-quote the trap command, so $temp_dir is looked up fresh when the trap actually fires:

temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT
echo "Working in: $temp_dir"

This time, by the time the EXIT trap fires, $temp_dir genuinely holds the real path — and since the trap’s own command string wasn’t expanded until that moment, it correctly removes the actual directory that was created. The rule to carry forward: trap commands should almost always be single-quoted, precisely so their variable references stay “live” until the moment the trap fires, rather than getting frozen at registration time.

Hands-On: Traps, Temp Resources, And Signal Handling

1. Set up a working directory.

mkdir -p ~/shell-course/ch19
cd ~/shell-course/ch19

2. Confirm EXIT fires on both normal and failed exits.

cat > exit-trap.sh << 'EOF'
#!/usr/bin/env bash
trap 'echo "Cleaning up"' EXIT
echo "About to fail"
exit 1
EOF
chmod +x exit-trap.sh
./exit-trap.sh

3. Use the standard mktemp + trap EXIT pattern, and confirm cleanup actually happened.

cat > temp-pattern.sh << 'EOF'
#!/usr/bin/env bash
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT
echo "Working in: $temp_dir"
echo "$temp_dir" > /tmp/shell-course-last-tempdir.txt
EOF
chmod +x temp-pattern.sh
./temp-pattern.sh

ls "$(cat /tmp/shell-course-last-tempdir.txt)" 2>&1

Confirm the second command reports the directory no longer exists — the trap removed it.

4. Reproduce the double-quoted trap bug, then fix it.

cat > trap-broken.sh << 'EOF'
#!/usr/bin/env bash
temp_dir=""
trap "rm -rf $temp_dir" EXIT
temp_dir=$(mktemp -d)
echo "Working in: $temp_dir"
echo "$temp_dir" > /tmp/shell-course-broken-tempdir.txt
EOF
chmod +x trap-broken.sh
./trap-broken.sh
ls "$(cat /tmp/shell-course-broken-tempdir.txt)"

cat > trap-fixed.sh << 'EOF'
#!/usr/bin/env bash
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT
echo "Working in: $temp_dir"
echo "$temp_dir" > /tmp/shell-course-fixed-tempdir.txt
EOF
chmod +x trap-fixed.sh
./trap-fixed.sh
ls "$(cat /tmp/shell-course-fixed-tempdir.txt)" 2>&1

Confirm the broken version’s directory still exists (leaked), while the fixed version’s directory is gone.

5. Handle a real signal.

cat > signal-demo.sh << 'EOF'
#!/usr/bin/env bash
trap 'echo "Caught signal, exiting gracefully"; exit 1' INT TERM
echo "Sleeping..."
sleep 30
EOF
chmod +x signal-demo.sh

./signal-demo.sh &
pid=$!
sleep 1
kill -TERM "$pid"
wait "$pid"
Sleeping...
Caught signal, exiting gracefully

6. Clean up.

cd ~
rm -rf ~/shell-course/ch19
rm -rf "$(cat /tmp/shell-course-broken-tempdir.txt)"
rm -f /tmp/shell-course-last-tempdir.txt /tmp/shell-course-broken-tempdir.txt /tmp/shell-course-fixed-tempdir.txt

You’ve now used mktemp to solve the PID-reuse weakness flagged in previous chapters, paired it with trap ... EXIT as the standard cleanup pattern, confirmed the real difference single vs. double quoting makes in a trap command, and handled an actual SIGTERM sent to a running background script. That completes this section on control flow and structure. The next section moves into safety, debugging, and real-world patterns — starting with a consolidated style guide bringing together every safety practice this course has covered so far.

Last updated on