Debugging Bash Scripts: set -x, set -e, traps
Bash scripts fail silently and confusingly by default. A handful of options change that, turning your scripts into ones that crash loudly and show you exactly where. Five techniques cover almost every “why didn’t this work?” moment.
set -e: exit on any error
By default, bash continues running even if a command fails. set -e makes it stop:
#!/bin/bash
set -e
cd /nonexistent # this fails
echo "still here" # without set -e, this would print
Caveats: set -e doesn’t stop on commands inside if conditions, ||, or && chains. Sometimes that’s good; sometimes it surprises you.
set -u: error on undefined variable
#!/bin/bash
set -u
echo "Hello, $name" # error: name: unbound variable
Catches typos in variable names that would otherwise silently produce empty strings.
set -o pipefail: catch errors in pipelines
Without it, a pipeline’s exit status is just the LAST command’s:
false | true # exit status: 0 (true succeeded)
With it, the pipeline fails if ANY command fails:
set -o pipefail
false | true # exit status: 1
The standard preamble
Put this at the top of every script:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'nt' # safer field splitting
This makes your script behave like a normal program — it dies on error, complains about typos, and respects pipeline failures.
set -x: trace every command
Print every command before it runs (with variable expansion shown):
#!/bin/bash
set -x
name="alice"
echo "Hello, $name"
ls /tmp
# Output:
# + name=alice
# + echo 'Hello, alice'
# + ls /tmp
Turn on/off in the middle of a script:
set -x # turn on tracing
problematic_function
set +x # turn off
Run an existing script with tracing without editing:
bash -x ./myscript.sh
trap: cleanup on exit or signal
Run code when the script exits — successfully or not:
#!/bin/bash
set -euo pipefail
tmpdir=$(mktemp -d)
cleanup() {
rm -rf "$tmpdir"
echo "cleaned up"
}
trap cleanup EXIT
# work in tmpdir
echo "data" > "$tmpdir/file"
# script exits — cleanup runs automatically
Catch specific signals:
trap 'echo "got SIGTERM"; exit 1' TERM
trap 'echo "got SIGINT"; exit 1' INT
trap 'echo "got error on line $LINENO"' ERR
The death-trap pattern (show line on error)
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "ERROR on line $LINENO" >&2' ERR
# now any command failure prints the line number first
Print debug info conditionally
VERBOSE=${VERBOSE:-0}
debug() {
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] $*" >&2
}
debug "starting backup"
# Run with: VERBOSE=1 ./script.sh
shellcheck: lint your scripts
The single best tool for finding bash bugs. Catches quoting issues, unused variables, broken patterns, common mistakes.
sudo apt install shellcheck # debian/ubuntu
sudo dnf install ShellCheck # fedora
brew install shellcheck # mac
shellcheck myscript.sh
Run shellcheck on every script you write. It will save you hours.
The full debug template
#!/usr/bin/env bash
set -euo pipefail
IFS=$'nt'
# Trace everything if DEBUG=1
[[ "${DEBUG:-0}" == "1" ]] && set -x
# Cleanup
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
trap 'echo "ERROR on line $LINENO" >&2' ERR
# Helpers
log() { echo "[$(date +%H:%M:%S)] $*" >&2; }
die() { echo "ERROR: $*" >&2; exit 1; }
# Sanity checks
[[ $# -ge 1 ]] || die "usage: $0 SOURCE_DIR"
[[ -d "$1" ]] || die "not a directory: $1"
# Real work
log "processing $1"
# ...
Real debugging session
# 1. Add tracing
bash -x myscript.sh
# 2. If the issue is silent, add verification
echo "before risky step: file size = $(stat -c%s "$file")"
risky_step
echo "after: $?"
# 3. Use shellcheck for static analysis
shellcheck myscript.sh
# 4. Bisect with set -e on / off
# Comment out half the script, find which half fails
# 5. Run interactively (no -e) to inspect state after failure
bash -i # or 'bash --rcfile <(echo "set -x")'
Common mistakes
- Forgetting
set -eand not noticing scripts continue past failures. - Using
set -ewith commands that legitimately may fail — guard them with|| true. - Not running shellcheck. It catches real bugs every time.
- trap’ing EXIT but not cleaning up on partial state — make cleanup idempotent.
What to learn next
Bash scripting is covered. Next big section: storage — disks, partitions, mounting, LVM. The plumbing that holds your data.