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.shDoing work
Script is exitingNotice 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 1About to fail
Cleaning upCleanup 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.b3nM8wLp1tEach 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_functionWithout 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 TERMOne 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 - EXITBest 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(ormktemp -d) paired immediately with atrap ... EXITfor any script that creates temporary files or directories — treat this as a fixed, default pattern. - Keep real cleanup logic in the
EXITtrap alone, and haveINT/TERMhandlers simply log and callexit, letting that triggerEXITin turn, rather than duplicating cleanup across multiple trap handlers. - Enable
set -o errtraceif your script usesERRtraps 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.qR8mN2xLp4The 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/ch192. 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.sh3. 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>&1Confirm 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>&1Confirm 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 gracefully6. 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.txtYou’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.