Bash Scripting Expert
Scripts fail in production on the input you didn't quote. Start strict, quote everything, clean up on exit, and run ShellCheck. When logic gets complex (JSON, data structures), switch to a real language.
When to Use
- Writing or debugging a Bash/POSIX shell script or CI/automation glue.
- "Works until a path has a space/newline" bugs; word-splitting, globbing, quoting.
- Argument parsing, exit codes, error handling, temp files, cleanup.
When NOT to Use
- Complex data/JSON/HTTP logic →
python-expert(shell is the wrong tool). - PowerShell scripting → use PowerShell tooling.
- CI pipeline YAML →
github-master.
Core Principles
1. Strict mode, every script
#!/usr/bin/env bash+set -euo pipefail: exit on error, error on unset vars, fail on any pipeline stage. SetIFS=$'\n\t'to stop surprise word-splitting on spaces.- Know
set -e's gotchas: it's suppressed insideif/||/&&conditions and command substitutions — check critical commands explicitly when needed.
2. Quote everything
- Always
"$var","${arr[@]}","$@"(not$*). Unquoted expansions split onIFSand glob — the #1 source of bugs. - Use
[[ … ]]for tests (not[ … ]),$(…)not backticks,(( … ))for arithmetic, andlocalfor all function variables.
3. Errors, cleanup, and exit codes
trap 'cleanup' EXITto remove temp files even on failure/interrupt. Create temps withmktemp.- Validate args and required commands up front; print usage to stderr and
exitnon-zero on misuse. Exit codes are meaningful —0success, non-zero failure. - Send errors/logs to
stderr(>&2); keepstdoutfor real output so the script composes in pipes.
4. Robust file & data handling
printfoverechofor anything with escapes/variables. Use--before paths, andfind … -print0 | while IFS= read -r -d ''for filenames with spaces/newlines.- For POSIX
sh, avoid bashisms ([[, arrays,local); if you use them, declarebashin the shebang. Lint with ShellCheck and format withshfmt.
Common Mistakes
- Unquoted
$var→ breaks on spaces/globs/empties. - Parsing
lsorfor f in $(ls)→ use globs orfind -print0. set -eassumed to catch everything → it won't inside conditions/substitutions; check explicitly.cd somewherewithout||exit→ subsequent commands run in the wrong directory.- No cleanup trap → leftover temp files on failure.
echowith-e/escapes → non-portable; useprintf.- Mixing logs into stdout → corrupts piped output.
Examples
Defensive script skeleton
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
usage() { printf 'usage: %s <input-dir> <out-file>\n' "$0" >&2; exit 2; }
[[ $# -eq 2 ]] || usage
input_dir=$1
out_file=$2
[[ -d $input_dir ]] || { printf 'no such dir: %s\n' "$input_dir" >&2; exit 1; }
command -v jq >/dev/null || { printf 'jq required\n' >&2; exit 1; }
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
# safe iteration (handles spaces/newlines in names)
find "$input_dir" -type f -name '*.log' -print0 |
while IFS= read -r -d '' f; do
wc -l -- "$f" >> "$tmp"
done
mv -- "$tmp" "$out_file"
trap - EXIT
printf 'wrote %s\n' "$out_file"
See Also
github-master— running scripts safely in GitHub Actions.docker-expert— robust container entrypoint scripts.python-expert— when a script outgrows the shell.