编程

Bash

试用

产出可正确运行的 Bash 脚本:引用、严格模式、trap 清理与跨平台兼容默认就位。

它能做什么

按 set -euo pipefail 写脚本,trap 兜底,所有展开加引号,命令用数组组装。专门处理实际会踩的坑:文件名带空格、严格模式下静默退出、SIGPIPE 141、cron 中 PATH 被裁、shellcheck SC2086 等。默认 bash 版本下限 4.4,兼容 macOS 3.2,超过 100 行建议改写为 Python。每个子主题(quoting、errors、debugging、portability 等)对应一份参考文档。

什么时候用它

  • 编写或评审 CI 步骤、部署脚本、cron 任务、容器入口
  • 排查文件名带空格就崩、或静默退出的脚本
  • 在 macOS bash 3.2 与 Linux 之间移植脚本
  • 定位退出码异常、SC2086 告警、或 cron/CI runner 下的失败

技能文档

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

  • Writing or reviewing any Bash beyond a one-liner: CI steps, deploy scripts, cron tasks, entrypoints, glue code
  • Debugging scripts that break on spaces in filenames, fail silently, hang, or exit with the wrong code
  • Hardening an existing script: strict mode, cleanup traps, argument parsing, re-runnability, portability
  • Porting a script between macOS and Linux, or down to POSIX sh
  • Deciding whether the task belongs in Bash at all (Core Rule 9)
  • Not for POSIX-sh-only targets (dash, busybox, alpine /bin/sh) — most patterns here are bashisms; portability.md covers the downgrade

Quick Reference

SituationPlay
Breaks on spaces or hostile filenamesQuote every expansion, iterate with find -print0 + while IFS= read -r -d ''quoting.md
set -e missed a failure, cleanup never ranThe five blind spots (conditions, ||/&&, $( ), ! cmd, exit in a subshell) → errors.md
Pipeline "fails" but each command workedExit code 141 = SIGPIPE from an early-exit consumer; PIPESTATUS names the segment → errors.md
Wrong output and you cannot see whyPS4='+ ${BASH_SOURCE##*/}:${LINENO}: ' bash -x scriptdebugging.md
Command built from variables misfiresBuild 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 cronCron has no login shell: minimal PATH, no profile, $HOME as cwd, % means newline → cron.md
Works locally, fails in the CI runnerEach step is a fresh non-interactive shell; strict mode does not carry over → ci.md
Script takes minutes on a large fileCount forks: one external command per line is the cost — batch into awk/sort → performance.md
unbound variable / bad substitution / ambiguous redirectSymptom→cause chains → debugging.md
Must run on macOS stock bash or an old serverVersion Floors below, then GNU-vs-BSD flags → portability.md
Flags, --help, subcommands, usage exit codesgetopts with a silent optstring, then shift $((OPTIND-1))arguments.md
Redirection order, heredocs, one-instance lockingRedirections apply left to right before the command runs → redirection.md
Paths, globs, temp files, deletes that must be safeResolve once with cd … && pwd -P; write temp + mvfiles.md
Parsing CSV/JSON/logs, choosing awk vs sed vs jqPer-line and stateless → one awk pass; never grep JSON → text-processing.md
Background jobs, signals, timeouts, N in parallelpid=$! then wait "$pid"; xargs -P for fan-out → processes.md
String surgery: defaults, trim, replace, basenameBuiltin expansions, no forks → expansion.md
Lists, dictionaries, sets, countersmapfile -t to load, declare -A for maps → arrays.md
Splitting into functions or a sourced librarymain "$@" behind a BASH_SOURCE guard; scope is dynamic → functions.md
Prompts, confirmations, color, progressGate every one of them on [[ -t 1 ]]interactive.md
Calling an API, webhook, or health checkcurl exits 0 on a 500 — capture %{http_code} and branch → http.md
Untrusted input, secrets, temp-file races, sudoKeep values as data, never as syntax → security.md
Adding tests, stubbing commands, lint in CIbash -n, shellcheck, then bats with PATH stubs → testing.md
Anything elseCore 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] 
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

  • "$var", "$(cmd)", "${arr[@]}" — always. $(cmd) strips ALL trailing newlines, not just one.
  • Single quotes are literal; $'...' interprets escapes: $'\t', $'\r', $'\0'.
  • Filenames from variables get -- or ./: rm -- "$f" survives a file named -rf.
  • echo "$var" breaks when var is -n, -e, or has backslashes — printf '%s\n' "$var" never does.
  • Arguments to ssh/su -c/bash -c are re-parsed by the receiving shell — build them with printf '%q ' (quoting.md).
  • ${arr[*]} joins with the first char of IFS into one word; "${arr[@]}" preserves elements. Joining is the only reason to write [*].

Version Floors

FeatureNeeds
printf -v var, += appendbash >=3.1
declare -A, mapfile, ${var^^}/${var,,}, globstar, ;& fallthrough, |&bash >=4.0
[[ -v var ]], shopt -s lastpipe, declare -gbash >=4.2
${arr[-1]}, declare -n namerefs, wait -nbash >=4.3
inherit_errexit, ${var@Q}, mapfile -d, empty "${arr[@]}" safe under set -ubash >=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.

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

Subshells and State

  • Every pipe segment runs in a subshell: cmd | while read -r x; do ((n++)); done loses n. Fix: done < <(cmd), or shopt -s lastpipe (bash >=4.2, scripts only).
  • ( ) is a subshell, { ...; } is the current shell — exit inside ( ) or $( ) exits only that subshell.
  • Background jobs: cmd & pid=$! then wait "$pid"wait returns the job's exit code, your only way to check it.
  • cd inside ( ) to visit a directory without having to cd back.

Robust Iteration

  • Globs: shopt -s nullglob first — otherwise for f in *.txt in an empty dir runs once with the literal string *.txt.
  • Hostile filenames or recursion: while IFS= read -r -d '' f; do ...; done < <(find . -name '*.log' -print0).
  • Lines of a file: while IFS= read -r line; do ...; done < fileIFS= keeps leading whitespace, -r keeps backslashes. A final line without a trailing newline is still skipped: append || [[ -n $line ]] to the read.
  • Any command inside the loop that reads stdin (ssh, ffmpeg, mysql) eats the rest of the input and the loop ends after one pass — pass ssh -n or redirect < /dev/null.
  • Split a string: IFS=, read -ra fields <<< "$csv". Join: (IFS=,; echo "${arr[*]}") — the subshell keeps the IFS change local.

Output Gates

Before delivering any script, check:

  • Every expansion quoted, or the unquoted one carries a comment saying why
  • Commands with variable flags built as arrays, not concatenated strings
  • shellcheck clean at lint_gate, or each disable names its SC code and reason
  • Failure path exercised: injected false, watched the trap fire and the exit code go nonzero
  • Bash floor stated in a header comment and matching bash_floor if any bash >=4.0 feature is used
  • No eval; no unvalidated input inside (( )) or array subscripts
  • Re-runnable: a run that dies halfway leaves nothing half-written — temp file plus mv, mkdir -p, rm -f
  • Destructive steps gated per destructive_confirm; no secret can appear in set -x output or ps

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.

VariableTypeDefaultEffect
bash_floor3.2 | 4.4 | 5.x4.4Gates which Version Floors features may be used unguarded; 3.2 bans mapfile, declare -A, namerefs and emits the portable fallbacks instead
target_oslinux | macos | bothbothPicks GNU or BSD flag forms in every emitted command (sed -i, date, stat, readlink); both restricts to the intersection (portability.md)
strict_modeset-euo | explicit-checksset-euoChooses the Script Skeleton and which school reviews enforce (Where Experts Disagree)
lint_gateerror | warning | style | nonewarningSeverity at or above which shellcheck findings block delivery (shellcheck -S ); Output Gates use it
rewrite_thresholdnumber (lines)100Length at which Core Rule 9 recommends another language
indent_style2-spaces | 4-spaces | tabs2-spacesFormatting of emitted scripts and the shfmt -i value
destructive_confirmbooltrueEmitted 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:

  • Tooling: shellcheck/shfmt/bats availability, GNU coreutils on macOS (gsed, gdate), jq vs python for JSON — affects testing.md gates and every parsing example
  • Conventions: function and variable naming, usage/help layout, log line format, script header content — affects functions.md and arguments.md output
  • Platform: bash floor and OS mix, POSIX-sh-only targets, alpine images where /bin/sh is ash and bash may be absent — affects portability.md guidance
  • Safety posture: whether scripts may sudo, dry-run first, banned constructs (eval, curl | sh, rm -rf on a variable) — affects security.md and destructive workflows
  • Runtime home: where the script actually runs unattended — cron, systemd timer, launchd, CI runner, container entrypoint — affects cron.md and ci.md advice
  • Output: verbosity, color only when the output is a TTY, timestamps, quiet or machine-readable logging — affects interactive.md and logging helpers

Traps

TrapWhy it failsDo instead
local out=$(cmd)local returns 0, masking cmd's failure — set -e never fireslocal out; out=$(cmd)
((count++)) when count is 0expression evaluates to 0 → exit status 1 → set -e kills the scriptcount=$((count+1))
grep -q downstream under pipefailearly exit sends SIGPIPE upstream; producer dies with 141 (128+13) and the pipeline "fails" on successcapture 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 -rfsingle quotes: trap 'rm -rf "$tmp"' EXIT
Checking $? after a log linethe echo overwrote itrc=$? on the very next line
which cmd to test existenceexternal, output format varies (SC2230)command -v cmd >/dev/null
cd "$dir" without a checkwithout -e, everything after runs in the wrong directorycd "$dir" || exit 1 — habit survives scripts that lack -e
sudo cmd > /root/outthe redirection is performed by YOUR shell before sudo runs — permission deniedcmd | sudo tee /root/out >/dev/null
set -euo pipefail in a sourced libraryit mutates the caller's shell and breaks their error handlingset options in executables only; libraries return codes (functions.md)

Where Experts Disagree

  • set -e: the strict-mode school makes it mandatory; the Google Shell Style Guide school argues its exceptions (conditions, ||, command substitution) make it false comfort and prefers explicit || die. Boundary: short glue scripts → strict mode; sourced libraries and functions whose return codes callers inspect → explicit handling. Never mix philosophies in one file.
  • Bash vs POSIX sh: write sh only when the target set actually contains dash/busybox/alpine. "Portable by default" costs arrays, [[ ]], and set -o pipefail for hosts you may never meet.
  • Returning values from functions: print to stdout and capture (composable, costs a fork per call) vs write through a nameref or a documented global (no fork, couples caller and callee). Boundary: hot loops and large payloads → nameref (bash >=4.3); everything else → stdout.
  • Long options: GNU getopt(1) parses them but does not exist usably on macOS (BSD getopt has no long options); a hand-rolled while/case loop is portable and you own the error messages. Boundary: Linux-only tooling → getopt; anything shipped to laptops → hand-rolled (arguments.md).

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

  • linux — when the bug is the system, not the script: permissions, cron daemon config, systemd units, OOM
  • regex — when the =~ pattern itself is the hard part
  • github-actions — when the script lives in CI and the failure is workflow wiring, not shell
  • cli-design — when the question is the tool's interface: subcommand layout, flag naming, help text contract

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

常见问题

macOS /bin/bash 锁死在 3.2,能用吗?
可以。默认 bash_floor 是 4.4,但脚本会强制写入 shebang 注释并在运行时检查版本,需要 bash >=4 的特性会被门禁拦截。把 bash_floor 设为 3.2 时改用兼容写法。
set -e 的盲区怎么破?
不靠放弃严格模式,而是把五个盲区(条件、||/&&、$( )、! cmd、子 shell 里的 exit)逐一列在 errors.md,每个都有对应的修法。
什么时候会拒绝写 Bash?
超过 100 行(Google Shell Style Guide 阈值)或需要嵌套数据结构时,建议改写成 Python。同样不覆盖 dash/busybox 这类纯 POSIX sh 目标,也不覆盖交互式 zsh/fish、PowerShell,以及宿主机 cron/systemd/权限问题。

相关技能

Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash.

36 次安装

Audits shell scripts for correctness, portability, and common pitfalls

21 次安装

围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。

作者 Iván136 次安装6 星标

按症状定位 Linux 主机故障并按层面对主机进行加固。

158 次安装8 星标

Windows 命令行工作规范:先选对 shell(默认 Git Bash),再避开编码与 MSYS2 参数改写两类陷阱。覆盖 GBK/UTF-8、BOM、MSYS2 路径转换、PowerShell/pwsh、WSL 判定、Python/Node.js、Git 配置与代码生成规则。适用于 Windows 10/11 + MSYS2/Git Bash 环境下的所有命令行操作。细节按需读 references/。

7 次安装1 星标