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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
---
name: bash
description: Use when writing, reviewing, or debugging shell scripts — covers safe defaults, shellcheck, executable permissions, and scripting patterns
---
# Bash scripting
## New scripts
Start with safe defaults:
```bash
#!/usr/bin/env bash
set -euo pipefail
if [[ "${TRACE-0}" == "1" ]]; then
set -o xtrace
fi
main() {
}
main "$@"
```
`set -euo pipefail` exits on errors, undefined variables, and pipe
failures. The `TRACE` guard provides opt-in tracing via `TRACE=1
./script.sh` without cluttering normal output.
Wrapping logic in `main()` and calling `main "$@"` at the bottom
prevents partial execution if the script is truncated during
download or write.
## Command flags
Prefer long flags over short ones in scripts. `grep --invert-match
--line-number` reads more clearly than `grep -vn`, and a reviewer
doesn't have to recall what each letter means. Short flags optimize
for interactive typing; scripts are read far more often than they're
written, so favor legibility.
Use short flags only when no long form exists, or when long flags
would harm clarity (for example, repeated `-v` to raise verbosity).
On macOS "no long form exists" is broader than it looks, because the base
utilities are BSD rather than GNU. `awk` is the one that bites: it is BWK awk,
where `--assign` and `--field-separator` don't exist and `awk --assign FS='\t'`
fails with `unknown option --assign ignored` followed by a syntax error on the
mangled program. Use `-v` and `-F` there. `cat -A` is absent too — `sed 's/ /·/g'`
is the portable way to make whitespace visible. Reach for the long form first,
but check it on the utility you're actually invoking rather than assuming GNU.
## Executable permissions
Scripts meant to be run directly need the executable bit (`chmod +x`).
Check permissions before committing — a missing executable bit is a
common oversight that breaks scripts silently when deployed.
Non-executable shell files are appropriate for files meant to be
sourced (`. script.sh` or `source script.sh`), not run directly.
## Shellcheck
Run [shellcheck](https://www.shellcheck.net/) on all shell scripts
before committing.
```bash
shellcheck path/to/script.sh
```
Fix all warnings. If a warning is genuinely inapplicable, disable it
inline with a directive comment on the line above:
```bash
# shellcheck disable=SC2059
printf "$fmt" "$arg"
```
Don't disable warnings globally or at the file level unless there's a
compelling reason.
## Exit status
Two silent exit-1 traps in scripts that gate on exit status (rex floats stay
open, hooks fire, CI fails):
`[[ cond ]] && cmd` as a function's last statement returns 1 when the
condition is false — `set -e` doesn't fire, the failure only propagates as
`$?`. Use `if`/`fi` when the branch is conditional.
An `EXIT` trap runs after `main`'s locals are destroyed, so a trap
referencing a local errors under `set -u` and flips the exit status — and
the cleanup never runs. Keep trap-referenced variables global.
---
*This is a self-improving skill. If you used it and it came up short — a missing command, flag, gotcha, or workflow — invoke the `self-improving-skills` skill and follow it before you finish.*