Files
skills-public/code-languages/bash.md
T
BluscreamandClaude Opus 5 5d96e69d2b refactor(skills): inline cross-links, drop Related sections, fix conventions
- all nine files: trailing "Related" sections removed, their unique links
  inlined where the topic actually arises (FFI boundary -> c-cpp, embedded
  frontend -> js-typescript, build scripts -> bash, etc.)
- README conventions: rule 2 mandated a Related Skills section, which now
  contradicts practice — replaced with inline-linking and one-home-per-fact
  rules. The <repo>/<rel path> rule was appended after the wrong section
  last commit; moved into the list where it belongs.

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

6.4 KiB

name, description
name description
bash 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. 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:

#!/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:

# 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:
    [[ -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:
    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:
    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 for the reasoning and the pattern.