| Breaks on spaces or hostile filenames | Quote every expansion, iterate with find -print0 + while IFS= read -r -d '' → quoting.md |
set -e missed a failure, cleanup never ran | The five blind spots (conditions, ||/&&, $( ), ! cmd, exit in a subshell) → errors.md |
| Pipeline "fails" but each command worked | Exit code 141 = SIGPIPE from an early-exit consumer; PIPESTATUS names the segment → errors.md |
| Wrong output and you cannot see why | PS4='+ ${BASH_SOURCE##*/}:${LINENO}: ' bash -x script → debugging.md |
| Command built from variables misfires | Build it as an array (cmd=(rsync -a); cmd+=(--dry-run); "${cmd[@]}"), never as a string → quoting.md |
Comparison wrong: [ vs [[, numeric vs lexical | [[ 10 < 9 ]] is TRUE (lexical); numbers belong in (( )) or -lt → conditionals.md |
| Runs fine by hand, fails from cron | Cron has no login shell: minimal PATH, no profile, $HOME as cwd, % means newline → cron.md |
| Works locally, fails in the CI runner | Each step is a fresh non-interactive shell; strict mode does not carry over → ci.md |
| Script takes minutes on a large file | Count forks: one external command per line is the cost — batch into awk/sort → performance.md |
unbound variable / bad substitution / ambiguous redirect | Symptom→cause chains → debugging.md |
| Must run on macOS stock bash or an old server | Version Floors below, then GNU-vs-BSD flags → portability.md |
Flags, --help, subcommands, usage exit codes | getopts with a silent optstring, then shift $((OPTIND-1)) → arguments.md |
| Redirection order, heredocs, one-instance locking | Redirections apply left to right before the command runs → redirection.md |
| Paths, globs, temp files, deletes that must be safe | Resolve once with cd … && pwd -P; write temp + mv → files.md |
| Parsing CSV/JSON/logs, choosing awk vs sed vs jq | Per-line and stateless → one awk pass; never grep JSON → text-processing.md |
| Background jobs, signals, timeouts, N in parallel | pid=$! then wait "$pid"; xargs -P for fan-out → processes.md |
| String surgery: defaults, trim, replace, basename | Builtin expansions, no forks → expansion.md |
| Lists, dictionaries, sets, counters | mapfile -t to load, declare -A for maps → arrays.md |
| Splitting into functions or a sourced library | main "$@" behind a BASH_SOURCE guard; scope is dynamic → functions.md |
| Prompts, confirmations, color, progress | Gate every one of them on [[ -t 1 ]] → interactive.md |
| Calling an API, webhook, or health check | curl exits 0 on a 500 — capture %{http_code} and branch → http.md |
| Untrusted input, secrets, temp-file races, sudo | Keep values as data, never as syntax → security.md |
| Adding tests, stubbing commands, lint in CI | bash -n, shellcheck, then bats with PATH stubs → testing.md |
| Anything else | Core Rules below, then reproduce with bash -x on the smallest input that still fails |