Reading — step 1 of 7
Learn
Bash scripts fail in confusing ways. The toolkit for debugging is built-in — knowing it saves hours.
set -x — trace execution
set -x
name="Ada"
echo "hello $name"
set +x
With -x, every command is printed before execution, prefixed with +:
+ name=Ada
+ echo 'hello Ada'
hello Ada
See variable values, expansion, all of bash's machinery exposed. Toggle with set -x / set +x to scope to a section.
bash -x script.sh — trace from the start without modifying the script.
PS4 — customize the trace prefix
export PS4='+ ${BASH_SOURCE}:${LINENO} ${FUNCNAME[0]:-main}: '
set -x
# ... script ...
Now each trace line shows file, line number, function:
+ /home/me/script.sh:10 main: name=Ada
+ /home/me/script.sh:11 main: echo 'hello Ada'
Great for sourced libraries — you see WHICH file each line came from.
Microsecond timestamps (Bash 5+):
export PS4='+ $EPOCHREALTIME ${BASH_SOURCE}:${LINENO} '
set -x
Profile time spent per command.
set -v — verbose (print before expansion)
set -v
# echoes each line as written, before expansion
Less common than -x. Useful for showing the SOURCE rather than the expanded form.
ERR trap — log on failure
set -euo pipefail
trap 'echo "failed at $LINENO with exit $?"' ERR
false # triggers ERR before script exits
With set -e, the script exits on any failure. The ERR trap fires first — chance to log the location.
Detailed error logging:
on_error() {
local line=$1
local cmd=$2
echo "FAIL line $line: $cmd" >&2
echo "call stack:" >&2
local i
for ((i = ${#FUNCNAME[@]} - 1; i >= 0; i--)); do
echo " $i: ${FUNCNAME[$i]}() in ${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}" >&2
done
}
trap 'on_error $LINENO "$BASH_COMMAND"' ERR
$BASH_COMMAND is the command currently being executed. BASH_SOURCE and BASH_LINENO arrays trace the call stack.
DEBUG trap — fires before EVERY command
trap 'echo "about to run: $BASH_COMMAND"' DEBUG
Very slow but extremely powerful for tracing complex scripts. Don't ship enabled — wraps every command.
ShellCheck
Not built-in but essential. Static analysis for shell scripts:
shellcheck script.sh
Catches:
- Unquoted variables
- Misused test operators
- Subshell variable scope bugs
- Word-splitting hazards
- POSIX vs bash mismatches
Run on every commit. Free, open source, brilliantly accurate.
bash -n — syntax check without execution
bash -n script.sh # parse only, don't run
Fast sanity check before deploying. Catches bracket mismatches, unfinished if/then.
Common debugging patterns
# Step-by-step
echo "DEBUG: x=$x" >&2
# Pause and inspect
read -p "press enter to continue"
# Conditional verbose
DEBUG=${DEBUG:-0}
[[ $DEBUG -eq 1 ]] && echo "debug info" >&2
# Run with: DEBUG=1 ./script.sh
IDE-style debuggers exist (bashdb) but the built-ins handle 95% of bugs.
Common mistakes
- Forgetting to disable
set -x— output noise floods. Toggle off after the suspect section. - Not setting custom PS4 — default trace doesn't show line numbers. Always set PS4 with line info.
- Trapping ERR without
set -e— ERR only fires when a failure would have triggered set -e behavior. set -xin production logs — leaks command lines that may contain secrets. Disable for production runs.- Skipping shellcheck — it catches things you never thought to check. Use it.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…