Strict Mode, Traps, Cleanup, and Temporary Resources
Design Bash automation that fails deliberately, reports useful context, releases temporary resources, and does not mistake a collection of shell options for a complete reliability strategy.
Learning objectives
By the end of this lesson
- Explain what
errexit,nounset,pipefail, anderrtracechange—and where they do not apply. - Build
EXIT,ERR,INT, andTERMtraps without losing the original status. - Create temporary files and directories securely with
mktemp. - Separate cleanup from rollback and make both idempotent.
- Construct a failure-injection lab that proves cleanup behavior.
1. “Strict mode” is an option policy, not a safety theorem
A common Bash preamble is set -Eeuo pipefail. It can expose mistakes earlier, but each option has context-dependent semantics. Reliable scripts treat these options as one layer inside an explicit error contract: commands with expected nonzero statuses are handled intentionally, critical mutations are checked directly, cleanup is registered before risky work, and tests exercise both success and failure paths.
flowchart TD
S["Start automation"] --> O["Apply documented shell options"]
O --> R["Register cleanup before allocating resources"]
R --> V["Validate arguments and dependencies"]
V --> W["Perform one controlled unit of work"]
W --> C{"Command status expected?"}
C -- yes --> H["Handle status explicitly"]
C -- no --> F["Capture context and fail"]
H --> N{"More work?"}
N -- yes --> W
N -- no --> X["EXIT trap performs idempotent cleanup"]
F --> X
X --> E["Return original exit status"]set -e has exceptions in tests, loops, command lists, pipelines, subshells, and command substitutions. A refactor can change whether a failure terminates the script. Important failure behavior should remain visible in if, case, or explicit status checks.
2. Understand the options independently
-e / errexitMay exit after an unhandled nonzero command statusUniversal fail-fast behavior in every shell context-u / nounsetErrors on many expansions of unset parametersValidation that a set value is non-empty or semantically validpipefailMakes a pipeline fail when a component failsIdentification or recovery from the failing component-E / errtracePropagates ERR traps into functions, substitutions, and subshellsExecution of ERR in contexts where errexit is suppressed#!/usr/bin/env bash
set -Eeuo pipefail
# Expected negative results belong inside explicit control flow.
if grep -q '^enabled=true$' service.conf; then
printf 'feature is enabled\n'
else
status=$?
if (( status == 1 )); then
printf 'feature is not enabled\n'
else
printf 'grep failed with status %d\n' "$status" >&2
exit "$status"
fi
fi
# nounset does not replace semantic validation.
: "${DEPLOY_ENV:?DEPLOY_ENV is required}"
[[ $DEPLOY_ENV =~ ^(dev|staging|prod)$ ]] || {
printf 'Unsupported DEPLOY_ENV: %s\n' "$DEPLOY_ENV" >&2
exit 2
}Use set -o and shopt -p to capture the effective shell policy during diagnostics. For reusable libraries, avoid silently changing the caller’s global options; either document the requirement or run sensitive code in a controlled subshell.
3. Traps must preserve status and avoid recursive failure
An EXIT trap runs when the shell exits normally or because of an error. Capture $? as the first operation, disable recursive traps when necessary, perform best-effort cleanup, and return the original status. ERR is useful for diagnostics, but it is not a universal exception handler. Signal traps should translate interruption into a clean exit while allowing the EXIT trap to perform cleanup once.
#!/usr/bin/env bash
set -Eeuo pipefail
work_dir=''
cleanup() {
local status=$?
trap - EXIT ERR INT TERM
if [[ -n $work_dir && -d $work_dir ]]; then
rm -rf -- "$work_dir" || \
printf 'warning: cleanup failed for %s\n' "$work_dir" >&2
fi
exit "$status"
}
on_error() {
local status=$?
printf 'error: status=%d source=%s line=%s command=%q\n' \
"$status" "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" \
"${BASH_LINENO[0]:-unknown}" "$BASH_COMMAND" >&2
return "$status"
}
trap cleanup EXIT
trap on_error ERR
trap 'exit 130' INT
trap 'exit 143' TERM
work_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/devops-academy.XXXXXXXX")
printf 'workspace=%s\n' "$work_dir"Keep trap handlers conservative. Calling complex functions, network clients, package managers, or commands that depend on partially initialized state can obscure the original failure. Diagnostics should be concise and cleanup should be repeatable.
4. Temporary resources require secure creation and ownership
Predictable names such as /tmp/report.$$ are unsafe because another process can pre-create or redirect them. mktemp creates a unique file or directory atomically. Prefer a private temporary directory, place all related artifacts inside it, set a restrictive umask, and register cleanup immediately after creation.
old_umask=$(umask)
umask 077
tmp_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/release-check.XXXXXXXX")
trap 'status=$?; rm -rf -- "$tmp_dir"; umask "$old_umask"; exit "$status"' EXIT
manifest="$tmp_dir/manifest.json"
log_file="$tmp_dir/run.log"
printf '{"state":"staged"}\n' > "$manifest"
printf 'started_at=%s\n' "$(date --iso-8601=seconds)" > "$log_file"
stat --format='mode=%a owner=%U group=%G path=%n' "$tmp_dir" "$manifest" "$log_file"Removing a temporary directory restores scratch state. Undoing a package upgrade, database write, firewall change, or deployment requires a separate rollback design. Never imply transactional guarantees merely because a trap deletes temporary files.
5. Scale cleanup with a small registered-action stack
One fixed cleanup function is sufficient for many scripts. When resources are created conditionally, register cleanup actions only after each resource exists. Avoid eval; store functions and arguments in arrays or use narrowly defined cleanup functions.
declare -a cleanup_paths=()
register_path() {
cleanup_paths+=("$1")
}
cleanup_paths_now() {
local status=$? index
trap - EXIT INT TERM
for (( index=${#cleanup_paths[@]}-1; index>=0; index-- )); do
rm -rf -- "${cleanup_paths[index]}" || true
done
exit "$status"
}
trap cleanup_paths_now EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
stage_dir=$(mktemp -d)
register_path "$stage_dir"
cache_dir=$(mktemp -d)
register_path "$cache_dir"
printf 'stage=%s cache=%s\n' "$stage_dir" "$cache_dir"Reverse-order cleanup mirrors resource acquisition: dependent resources are removed before the resources they depend on. Keep the registered type narrow; arbitrary command strings create quoting and injection hazards.
6. Hands-on lab: prove cleanup under injected failure
The lab creates a private workspace, records its path outside that workspace, injects either success or failure, and verifies that the directory is removed in both cases.
lab="$HOME/devops-academy/linux/chapter15/lesson01"
rm -rf "$lab"
mkdir -p "$lab"
cd "$lab"
cat > cleanup-demo.sh <<'SCRIPT'
#!/usr/bin/env bash
set -Eeuo pipefail
mode=${1:-success}
record_file=${2:?record file required}
work_dir=''
cleanup() {
local status=$?
trap - EXIT ERR INT TERM
if [[ -n $work_dir ]]; then
rm -rf -- "$work_dir" || true
fi
exit "$status"
}
on_error() {
local status=$?
printf 'failed status=%d line=%s command=%q\n' \
"$status" "${BASH_LINENO[0]:-?}" "$BASH_COMMAND" >&2
return "$status"
}
trap cleanup EXIT
trap on_error ERR
trap 'exit 130' INT
trap 'exit 143' TERM
umask 077
work_dir=$(mktemp -d -- "${TMPDIR:-/tmp}/cleanup-demo.XXXXXXXX")
printf '%s\n' "$work_dir" > "$record_file"
printf 'payload\n' > "$work_dir/data.txt"
case $mode in
success) printf 'completed\n' ;;
fail) false ;;
*) printf 'unknown mode: %s\n' "$mode" >&2; exit 2 ;;
esac
SCRIPT
chmod u+x cleanup-demo.sh
bash -n cleanup-demo.sh
./cleanup-demo.sh success "$lab/success-path.txt"
success_path=$(<"$lab/success-path.txt")
[[ ! -e $success_path ]]
if ./cleanup-demo.sh fail "$lab/fail-path.txt"; then
printf 'FAIL: injected failure unexpectedly succeeded\n' >&2
exit 1
fi
fail_path=$(<"$lab/fail-path.txt")
[[ ! -e $fail_path ]]
printf 'PASS: both workspaces were removed\n' Verification checklist
7. Common reliability mistakes
“set -e means every error stops the script.”
Its behavior depends on grammatical context. Critical failures should be handled explicitly and regression-tested.
“An ERR trap is an exception handler.”
It shares many contextual exceptions with errexit and should be treated primarily as diagnostic instrumentation.
“rm -rf is safe inside an EXIT trap.”
Only after the path is proven non-empty, owned by the script, and created through a secure mechanism such as mktemp -d.
“Cleanup restores the system.”
Cleanup releases temporary resources. Rollback must be designed for each persistent mutation.
8. Knowledge check
Question 1. Why should an EXIT trap save $? immediately?
Question 2. What is the difference between pipefail and explicit pipeline recovery?
pipefail makes a pipeline status reflect a failing component. It does not identify the component, interpret expected statuses, or perform recovery.Question 3. Why is mktemp -d preferable to a path containing $$?
9. Summary
Reliable Bash uses options as documented policy, not folklore. Handle expected statuses explicitly, register cleanup before risky work, preserve the original exit status, keep traps conservative, create temporary resources atomically, and distinguish cleanup from rollback. Failure injection is the proof that the design works when the happy path does not.
10. Further reading
- GNU Bash Reference Manual —
setand shell options. - GNU Bash Reference Manual —
trapandgetopts. - GNU Coreutils —
mktemp. help trap,help set, andman 1 mktempon the target system.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.