Bash does not have a debugger, but it has something close: an execution trace, a linter with opinions, and the discipline of printing what you claim. Between bash -x, shellcheck, and honest logging, almost no script bug survives an afternoon.

bash -x: watch it run #

The trace flag prints every expanded command before execution — variables already filled in, which is exactly the information print-debugging lacks:

Tracing a failing script
$ bash -x deploy.sh prod
+ set -euo pipefail
+ [[ 1 -ne 2 ]]
+ region=prod
+ rsync -a dist/ server:/srv/prod/

+ prefixes trace lines; the difference between what you wrote and what bash expanded is where the bug lives. Scope it when the whole run is too noisy:

Trace one suspicious section
set -x
rsync -a "$src" "$dst"
set +x

shellcheck: the reviewer #

shellcheck reads the script statically and names the bug class — unquoted expansion, useless cat, missing quoting in a heredoc, [ x ] && [ y ] versus [[ x && y ]]:

The workflow
$ shellcheck deploy.sh
In deploy.sh line 12:
for f in $(ls *.log); do
        ^-- SC2045: Iterating over ls output is fragile...

Every message links to a wiki page explaining the fix. Most experienced bash developers run it continuously — in the editor, or as a git pre-commit hook so it reviews every change.

printf: honest logging #

Debug output belongs on stderr, timestamped, and distinguishable from real output — the same discipline as the logging function:

A debug helper that costs nothing
log()  { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
debug() { [[ ${DEBUG:-0} == 1 ]] && log "DEBUG: $*" || true; }

DEBUG=1 ./deploy.sh

The DEBUG=1 gate turns tracing on without editing the script — and stderr keeps stdout clean for pipelines and exit-code honesty.

The method #

SymptomFirst move
Wrong values, right flowbash -x, compare written vs expanded
Works alone, fails in pipeCheck pipefail and subshells
"Works in my shell"Quote and locale differences; shellcheck
Variable mysteriously emptyset -u, then trace where it was set
IntermittentTrace to a file: bash -x script 2> trace.log

Prevention starts earlier: strict mode turns silent corruption into immediate, named errors. The scripting series continues.