bash
Use when writing or reviewing Bash scripts — deployment scripts, git hooks, scheduled jobs (cron or systemd timers), wrappers around CLIs, and glue between tools. Covers strict mode, quoting, locking
SKILL.mdskills/languages/bash/SKILL.md
GitHubBash
Default: #!/usr/bin/env bash, set -euo pipefail, ShellCheck clean, functions over long straight-line scripts, and a move to Python once logic needs data structures.
Script skeleton
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "usage: ${0##*/} [--dry-run] <target>" >&2; exit 2; }
dry=0
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run) dry=1 ;;
-h|--help) usage ;;
*) target=$1 ;;
esac
shift
done
[[ -n ${target:-} ]] || usage
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
Rules that prevent most bugs
- Quote every expansion:
"$var","${arr[@]}". Unquoted variables split on spaces and expand globs. - Use
[[ ... ]]for tests and$(...)for command substitution. set -edoes not fire insideifconditions,&&/||chains or command substitutions in some contexts; check critical commands explicitly.pipefailmakes a pipeline fail if any stage fails — butgrepreturning 1 on "no match" then becomes an error; handle it (grep ... || true).- Read files line by line with
while IFS= read -r line; do ...; done < file.
Running safely and repeatedly
- One instance at a time:
exec 9>"$lockfile"; flock -n 9 || { echo "already running" >&2; exit 0; }. - Idempotent: check state before changing it (does the branch exist, is the file already current) so a rerun after a failure is safe.
- Write atomically: write to a temp file in the same directory and
mvit into place. - Retries: loop with a cap and growing sleep for network commands; never retry forever.
- Dry run first for anything destructive, and print exactly what would happen.
Timers, hooks and CI
- Timers and hooks run with a minimal environment: no interactive profile, a short
PATH, a different working directory. Use absolute paths and setPATHat the top. - Log with timestamps to a file or the journal (
logger -t name), since nobody watches the terminal. - In git hooks, read the message file passed as
$1, do not assume the working directory, and chain to any pre-existing hook instead of replacing it.
Verification loop
shellcheck script.sh— zero findings, or each suppressed with a reason.bash -n script.shfor syntax.- Run with
--dry-run, then for real, then run it again to prove idempotency.
Pitfalls
cdin a script without|| exitcontinues in the wrong directory.rm -rf "$dir/"*with an empty$dirtargets/; guard with${dir:?}.local var=$(cmd)hidescmd's exit status; declare and assign separately.echointerprets options and escapes inconsistently; useprintf '%s\n'.- Arithmetic with leading zeros (
08) is treated as octal; force base 10 with10#$n. sed -idiffers between GNU and BSD; on macOS usesed -i ''or prefer Python for edits.