User preferences live in ~/Clawic/data/bash/config.yaml (see Configuration); nothing else is stored on the user’s machine. If you have data at an old location (~/bash/ or ~/clawic/bash/), move it to ~/Clawic/data/bash/.

When To Use

Quick Reference

Situation Play
Breaks on spaces or hostile filenames Quote every expansion, iterate with find -print0 + while IFS= read -r -d ''quoting.md
set -e missed a failure, cleanup never ran The five blind spots (conditions, \|\|/&&, $( ), ! cmd, exit in a subshell) → errors.md
Pipeline “fails” but each command worked Exit code 141 = SIGPIPE from an early-exit consumer; PIPESTATUS names the segment → errors.md
Wrong output and you cannot see why PS4='+ ${BASH_SOURCE##*/}:${LINENO}: ' bash -x scriptdebugging.md
Command built from variables misfires Build it as an array (cmd=(rsync -a); cmd+=(--dry-run); "${cmd[@]}"), never as a string → quoting.md
Comparison wrong: [ vs [[, numeric vs lexical [[ 10 < 9 ]] is TRUE (lexical); numbers belong in (( )) or -ltconditionals.md
Runs fine by hand, fails from cron Cron has no login shell: minimal PATH, no profile, $HOME as cwd, % means newline → cron.md
Works locally, fails in the CI runner Each step is a fresh non-interactive shell; strict mode does not carry over → ci.md
Script takes minutes on a large file Count forks: one external command per line is the cost — batch into awk/sort → performance.md
unbound variable / bad substitution / ambiguous redirect Symptom→cause chains → debugging.md
Must run on macOS stock bash or an old server Version Floors below, then GNU-vs-BSD flags → portability.md
Flags, --help, subcommands, usage exit codes getopts with a silent optstring, then shift $((OPTIND-1))arguments.md
Redirection order, heredocs, one-instance locking Redirections apply left to right before the command runs → redirection.md
Paths, globs, temp files, deletes that must be safe Resolve once with cd … && pwd -P; write temp + mvfiles.md
Parsing CSV/JSON/logs, choosing awk vs sed vs jq Per-line and stateless → one awk pass; never grep JSON → text-processing.md
Background jobs, signals, timeouts, N in parallel pid=$! then wait "$pid"; xargs -P for fan-out → processes.md
String surgery: defaults, trim, replace, basename Builtin expansions, no forks → expansion.md
Lists, dictionaries, sets, counters mapfile -t to load, declare -A for maps → arrays.md
Splitting into functions or a sourced library main "$@" behind a BASH_SOURCE guard; scope is dynamic → functions.md
Prompts, confirmations, color, progress Gate every one of them on [[ -t 1 ]]interactive.md
Calling an API, webhook, or health check curl exits 0 on a 500 — capture %{http_code} and branch → http.md
Untrusted input, secrets, temp-file races, sudo Keep values as data, never as syntax → security.md
Adding tests, stubbing commands, lint in CI bash -n, shellcheck, then bats with PATH stubs → testing.md
Anything else Core Rules below, then reproduce with bash -x on the smallest input that still fails

Each file above is one sub-job and is self-contained: read SKILL.md by default, open exactly one guide when the situation matches.

Core Rules

  1. Open every script with #!/usr/bin/env bash and set -euo pipefail, then learn the -e holes (errors.md) instead of dropping strict mode — the holes are enumerable; silent failures are not.
  2. Quote every expansion: "$var", "$(cmd)", "${arr[@]}". An unquoted expansion is a deliberate act that carries a comment saying why. Word splitting plus globbing is Bash’s #1 bug class (shellcheck SC2086).
  3. Build commands as arrays, never as strings. opts="--exclude '*.log'"; rsync $opts src dst passes the quotes as literal characters; opts=(--exclude '*.log'); rsync "${opts[@]}" src dst passes --exclude and *.log as two clean arguments. Conditional flags append: [[ $dry == 1 ]] && opts+=(--dry-run).
  4. Run shellcheck before shipping, blocking at lint_gate severity. Suppress only with the code and a reason on the same line: # shellcheck disable=SC2086 -- flags must split.
  5. Know your floor: macOS /bin/bash is 3.2 forever (GPLv3 freeze). If the script uses any bash >=4.0 feature (Version Floors), state the floor in a header comment and enforce it: ((BASH_VERSINFO[0] >= 4)) || { echo "needs bash 4+" >&2; exit 1; }.
  6. Never parse ls. Iterate with globs or find -print0: filenames may contain newlines, so NUL is the only delimiter a filename cannot contain.
  7. Test the failure path before delivering: swap one command for false, confirm the script stops, the trap fires, and the exit code is nonzero. A cleanup you never saw run is a cleanup you do not have.
  8. Untrusted input never reaches eval, arithmetic, or array subscripts: (( $userinput )) executes commands via arr[$(cmd)] subscripts. Gate with a regex first: [[ $n =~ ^[0-9]+$ ]] || die "not a number: $n".
  9. Past rewrite_threshold lines (default 100, the Google Shell Style Guide cutoff) or once you need nested data structures, rewrite in Python or similar. Bash orchestrates processes; it does not model data.

Script Skeleton

#!/usr/bin/env bash
# Requires bash >= 4.4 (inherit_errexit). Run: script.sh [-n] <target>
set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true   # bash >=4.4: $(cmd) failures propagate

die() { printf '%s\n' "$*" >&2; exit 1; }

tmp=$(mktemp) || die "mktemp failed"
trap 'rm -f "$tmp"' EXIT   # single-quoted: expands when it FIRES, not now
                           # fires on error and normal exit; kill -9 bypasses all traps

Quoting

Version Floors

Feature Needs
printf -v var, += append bash >=3.1
declare -A, mapfile, ${var^^}/${var,,}, globstar, ;& fallthrough, \|& bash >=4.0
[[ -v var ]], shopt -s lastpipe, declare -g bash >=4.2
${arr[-1]}, declare -n namerefs, wait -n bash >=4.3
inherit_errexit, ${var@Q}, mapfile -d, empty "${arr[@]}" safe under set -u bash >=4.4
EPOCHSECONDS/EPOCHREALTIME, SRANDOM (5.1) bash >=5.0

macOS /bin/bash stays at 3.2. #!/usr/bin/env bash finds a Homebrew bash on PATH; #!/bin/bash never will. Check at runtime with BASH_VERSINFO, not by parsing bash --version.

Exit Codes

Formula: a code above 128 means killed by signal code − 128. Codes are mod 256 — exit 256 reports 0, exit -1 reports 255.

Code Meaning First move
1 Generic failure — also (( expr )) evaluating to 0 Read the last command, then the (( traps below
2 Shell syntax or builtin usage error bash -n script locates it; conventionally also “wrong CLI usage” (arguments.md)
126 Found but not executable chmod +x, or the shebang interpreter is not executable
127 Command not found PATH (the cron classic), typo, or a missing shebang interpreter (“bad interpreter”)
130 SIGINT (128+2) User pressed Ctrl-C — propagate it, do not swallow it
137 SIGKILL (128+9) OOM killer or kill -9; no trap ever ran, so cleanup did not happen
141 SIGPIPE (128+13) A consumer (head, grep -q) closed the pipe early — usually success misread as failure
143 SIGTERM (128+15) Orderly external stop (systemd, CI timeout) — trap it to clean up
124 GNU timeout expired (125 = timeout itself failed) Raise the timeout or fix the hang (processes.md)
255 ssh transport error, and any exit with a negative or >255 value wrapped Distinguish ssh’s own failure from the remote command’s

Subshells and State

Robust Iteration

Output Gates

Before delivering any script, check:

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/bash/config.yaml. Never interview the user — record a preference the moment it is stated.

Variable Type Default Effect
bash_floor 3.2 | 4.4 | 5.x 4.4 Gates which Version Floors features may be used unguarded; 3.2 bans mapfile, declare -A, namerefs and emits the portable fallbacks instead
target_os linux | macos | both both Picks GNU or BSD flag forms in every emitted command (sed -i, date, stat, readlink); both restricts to the intersection (portability.md)
strict_mode set-euo | explicit-checks set-euo Chooses the Script Skeleton and which school reviews enforce (Where Experts Disagree)
lint_gate error | warning | style | none warning Severity at or above which shellcheck findings block delivery (shellcheck -S <value>); Output Gates use it
rewrite_threshold number (lines) 100 Length at which Core Rule 9 recommends another language
indent_style 2-spaces | 4-spaces | tabs 2-spaces Formatting of emitted scripts and the shfmt -i value
destructive_confirm bool true Emitted scripts guard deletes, overwrites, and remote pushes behind --yes or a dry-run pass

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

Traps

Trap Why it fails Do instead
local out=$(cmd) local returns 0, masking cmd’s failure — set -e never fires local out; out=$(cmd)
((count++)) when count is 0 expression evaluates to 0 → exit status 1 → set -e kills the script count=$((count+1))
grep -q downstream under pipefail early exit sends SIGPIPE upstream; producer dies with 141 (128+13) and the pipeline “fails” on success capture first: out=$(cmd), then grep the variable
rm -rf "$dir/" empty/unset dirrm -rf / rm -rf "${dir:?}/" aborts if empty
trap "rm -rf $tmp" EXIT (double quotes) the body expands NOW, when tmp may still be empty — you registered rm -rf single quotes: trap 'rm -rf "$tmp"' EXIT
Checking $? after a log line the echo overwrote it rc=$? on the very next line
which cmd to test existence external, output format varies (SC2230) command -v cmd >/dev/null
cd "$dir" without a check without -e, everything after runs in the wrong directory cd "$dir" \|\| exit 1 — habit survives scripts that lack -e
sudo cmd > /root/out the redirection is performed by YOUR shell before sudo runs — permission denied cmd \| sudo tee /root/out >/dev/null
set -euo pipefail in a sourced library it mutates the caller’s shell and breaks their error handling set options in executables only; libraries return codes (functions.md)

Where Experts Disagree

More Clawic skills, get them at https://clawic.com/skills/bash (install if the user confirms):

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/bash.