Files
skills-public/code-languages/bash.md
T
BluscreamandClaude Opus 5 32b47b075b fix(coding-dev): prefer a test over the build script for size limits
A shell check only runs where that script runs. A test travels with the repo,
needs no extra tooling, and fails with a message naming the offending files.
Enforcement is now a preference order: native linter rule > test > build
script as fallback, with a worked example and a note on why a per-compile
build hook usually is not worth the incremental-build cost.

Propagated per language: rust (clippy has no file-length lint -> workspace
root test), js-typescript (ESLint max-lines covers both natively, no test
needed), go/python/csharp (linter for functions, test for files), bash (don't
reimplement in the gate what a test can assert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:52:37 +02:00

172 lines
6.8 KiB
Markdown

---
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](skills-public/code-languages/python.md) 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 <check>; do sleep 2; done` or `while ! <check>; 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.
- **Don't reimplement in the gate what a test can assert.** Checks like file-size limits
belong in the test suite where they travel with the repo and fail with a useful message;
the build script is the fallback for projects with no suite to hang them on. See
[`coding-dev.md` §4](skills-public/coding-dev.md).
- 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.