--- name: bash description: >- Use this skill when writing or modifying Bash/shell scripts, build pipelines, release scripts, or when running shell commands as an agent. Covers strict mode, shellcheck, quoting, the no-wait-loops rule, build-script structure, and destructive-command safety. --- # Bash / Shell — Rules & Preferences Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds shell specifics. Bash is for **orchestration** — sequencing tools, wiring a build pipeline, gluing CLIs. Once a script grows real data structures, arithmetic, or parsing, it wants to be Python or a compiled tool instead. --- ## 1. Strict Mode & Linting Every script starts with: ```bash #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' ``` - `-e` exit on error, `-u` error on unset variable, `-o pipefail` so a failure mid-pipe is not masked by a successful final command. - **`shellcheck` must be clean** and belongs in the build gate. Do not silence a finding with a `# shellcheck disable=` without a comment saying why. - Format with `shfmt`. > [!IMPORTANT] > `set -e` does **not** fire inside `if`, `&&`, `||`, or command substitution used in a > condition. Do not assume a failing command inside those aborts the script — check > explicitly where it matters. --- ## 2. Quoting & Expansion - **Quote every expansion**: `"$var"`, `"${arr[@]}"`, `"$(cmd)"`. Unquoted expansion word-splits and glob-expands, which is how a path with a space becomes two broken arguments. - `"${var:-default}"` for defaults; `"${var:?message}"` for required values. - Use arrays for argument lists — never build a command as a string and `eval` it. - `[[ ]]` over `[ ]`. `$(…)` over backticks. - `local` every variable inside a function. - `printf` over `echo` for anything with escapes or leading dashes. --- ## 3. Never Write Wait Loops > [!CAUTION] > **Do not write `until ; do sleep 2; done` or `while ! ; do sleep …; done`.** > They hang the terminal, and the only way out is an interrupt. To wait for something: - Run the work in the **background** and wait for its completion signal. - To check external state, **take a single sample per invocation** rather than looping. - If a tool has a real blocking wait (`--wait`, `wait-for`, `systemctl is-active --wait`), use that instead of polling around it. This mirrors the general events-over-polling preference: prefer a notification to a loop. --- ## 4. Never Filter Build Output Repeating the global rule because shell is where it gets violated: ```bash # WRONG cargo build 2>&1 | grep -E "error|warning" npm run build | tail -20 # RIGHT cargo build npm run build ``` Tee to a log if you need a copy — `cmd 2>&1 | tee build.log` preserves the full output — but never `grep`/`head`/`tail` it away. Narrow the command instead when output is genuinely huge. --- ## 5. Destructive Commands - **Look at the target before deleting or overwriting it.** `ls` it, `stat` it, read it. - Never `rm -rf "$VAR"` without first asserting `$VAR` is non-empty and is what you think: ```bash [[ -n "${BUILD_DIR:-}" && -d "$BUILD_DIR" ]] || { echo "refusing: BUILD_DIR unset" >&2; exit 1; } ``` - **A shared output directory belongs to nobody.** If two scripts write to `dist/`, neither may `rm -rf` it — each cleans only its own outputs. - Write scratch files to a `mktemp -d` and trap cleanup: ```bash tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT ``` - Prefer a `--dry-run` flag on anything that deploys, releases, or deletes — and implement it honestly. ### `pgrep -f` matches its own shell > [!WARNING] > The command line containing the pattern **is itself a process**, so `pgrep -f foo.conf` > matches the shell running it. Piping that into `kill` kills the running script — which reads > exactly like "the thing I was testing never happened". > > Write the pattern so it cannot match itself: `pgrep -f 'my[a]pp\.conf'` --- ## 6. Build Script Structure A build script is the project's **quality gate**, and it should be exhaustive enough to catch mistakes before a human does. - **Fixed action order regardless of flag order**, with any failure aborting the rest: ``` bump → lint → build → artifacts → stop → deploy → start → commit → push → release ``` - **A skipped test fails the gate.** Never let a skip report green. - **Print a summary with numbers**, not just a verdict — counts of warnings, suppressions, and file-size violations. A suppression count that has grown is worth seeing even on a pass. - Keep per-step logs in a known directory so a failure can be read after the fact. - **Never bypass a failed gate** by copying a binary or running the install step by hand. Fix the failing step; do not weaken the check to make it pass. - Support `--dry-run` for the full release path. - **Throttle heavy builds.** Detect when something latency-sensitive is running and drop to half the cores at `nice -n 10`, announcing that it did so. Provide an override env var. --- ## 7. Paths & Portability - Resolve the script's own directory rather than assuming the caller's `cwd`: ```bash script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ``` - Prefer absolute paths in anything non-interactive. - **Entering a container resets the working directory** — chain `cd` inside the same command string: `container-run -- bash -c "cd /path && make"`. - Don't hardcode `/tmp` when the content must survive a cleanup; `/var/tmp` persists longer. - `command -v tool >/dev/null || { echo "tool required" >&2; exit 1; }` before using an optional dependency. **A silently skipped step is worse than a failure** — a packaging step that quietly skips when its tool is missing will ship an incomplete release. --- ## 8. Secrets - Read secrets from an environment file or secret store; **never** inline them in a script. - Never `echo` a secret, and never pass one as a command-line argument — argv is world-readable in the process table. Use an env var or stdin. - Never commit a file containing real credentials; commit an example with empty values. --- ## 9. Script Placement Standalone utility scripts belong in a **dedicated, version-controlled scripts directory**, symlinked into `PATH` rather than copied there. Never write a loose script directly into `~/.local/bin` or another `PATH` directory — the source of truth should be one versioned location. See [`cli-tooling.md` §5](skills-public/cli-tooling.md) for the reasoning and the pattern. --- ## 10. Related - [`coding-dev.md`](skills-public/coding-dev.md) — the general rules this extends. - [`cli-tooling.md`](skills-public/cli-tooling.md) — invoking other tools *from* these scripts: discovery, non-interactive flags, structured output. - [`python.md`](skills-public/code-languages/python.md) — where a shell script should have become a Python script.