aboutsummaryrefslogtreecommitdiff
path: root/languages/bash/claude/rules/bash.md
blob: 042138ae2cff3ef18c5d5b6405889672a2be35a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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.