# Bash Code Rules Applies to: `**/*.sh`, `**/*.bash`, and extensionless files with a `sh`/`bash` shebang Shell-specific style and structure. Pairs with `bash-testing.md` for tests and the generic `verification.md` / `commits.md` rules. When in doubt, defer to [ShellCheck](https://www.shellcheck.net/wiki/) (every SCxxxx code has a wiki page explaining the fix) and Google's [Shell Style Guide](https://google.github.io/styleguide/shellguide.html). ## ShellCheck Is the Gate, Not a Suggestion The bundle's PostToolUse hook runs `shellcheck` on every edited shell file and blocks on a violation; the pre-commit hook re-checks staged files. ShellCheck catches the bugs that define shell: unquoted expansions that word-split, unset variables, `[ ]` pitfalls, masked exit codes. Fix the finding rather than silence it. When a warning is a genuine false positive, disable it narrowly with a `# shellcheck disable=SCxxxx` directive on the line above and a comment saying why, never a file-wide blanket disable. ## The Header: Strict Mode Every script starts with `#!/usr/bin/env bash` and `set -euo pipefail`: - `-e` exits on an unhandled non-zero command. Handle the expected-failure cases explicitly (`cmd || true`, an `if`, a `case`) so the exit is a real error. - `-u` treats an unset variable as an error. Use `"${VAR:-default}"` for the ones that are legitimately optional. - `-o pipefail` makes a pipeline fail if any stage fails, not just the last. A script meant to be *sourced* (a library) skips `set -e` — it would change the caller's shell. Libraries guard their own commands instead. ## Quote Everything - Double-quote every expansion: `"$var"`, `"$@"`, `"${arr[@]}"`, `"$(command)"`. Unquoted is the single largest source of shell bugs — a path with a space becomes two arguments. - `"$@"` (quoted) passes arguments through untouched; `$*` and unquoted `$@` word-split. Use `"$@"` unless you specifically want the joined string. - Loop over arrays and `find -print0 | while IFS= read -r -d ''`, never over unquoted command substitution or `ls` output. ## Test, Compare, Branch - Use `[[ ]]` for tests, not `[ ]` / `test`. `[[ ]]` doesn't word-split its operands, supports `&&`/`||`/`=~`, and has fewer quoting traps. - Arithmetic goes in `(( ))` or `$(( ))`, not `[ ]` with `-eq`. - `$(command)`, never backticks — nests cleanly and reads better. - Prefer `printf` over `echo` for anything but a fixed literal string; `echo` mangles values that start with `-` or contain backslashes. ## Functions and Scope - Declare function-local variables with `local`. A bare assignment in a function writes a global and leaks across calls. - `local var; var="$(cmd)"` on two lines when you need the command's exit status: `local var="$(cmd)"` masks `cmd`'s exit code behind `local`'s. - Keep functions focused. A function that fetches, parses, and writes is three functions; the test difficulty in `bash-testing.md` is the tell. - Put `main "$@"` at the bottom for a script with more than a couple of functions, so definition order doesn't dictate execution order. ## Robustness - `trap 'rm -rf "$tmpdir"' EXIT` right after creating a temp resource, so cleanup runs on every exit path including errors. - Make a temp file or dir with `mktemp` / `mktemp -d`, never a fixed `/tmp/name` (race + collision). - Check that a required command exists before the work: `command -v jq >/dev/null || { echo "jq required" >&2; exit 1; }`. - Never parse `ls` output and don't `cat` a file into a pipe you could read directly. Glob, or use `find`, or read the file in place. ## What Not to Do - Don't leave an expansion unquoted to "save a quote" — quote it. - Don't use `[ ]` when `[[ ]]` is available, or backticks when `$()` is. - Don't silence a ShellCheck warning file-wide to clear it; fix it or disable the one code with a reason. - Don't refactor surrounding code while fixing a bug — keep the diff scoped. - Don't commit credentials or API keys — the pre-commit hook catches common patterns but isn't a substitute for care.