Three silent failures of a bare script
Bash defaults are the root of debugging hell: commands do not stop the script (`-e` off), undefined variables act as empty strings (`-u` off), and a pipe only reports the last command’s status (`pipefail` off).
The result is a script that "looks finished" but broke halfway: `cd` fails yet `rm` still runs in the wrong directory, a typo’d variable silently becomes empty, `grep | head` hides an upstream crash. Production incidents hide in exactly these lines.
cd /data/release && rm -rf old/ # cd 失败时 rm 根本不该执行
cp app.conf /etc/app.conf # 失败了脚本照样往下走
echo "deploy done" # 假象:一切都好First line of defense: set -euo pipefail
Put `set -euo pipefail` right after the shebang: `-e` exits on any failing command, `-u` errors on undefined variables instead of treating them as empty, `-o pipefail` makes a pipe adopt the first failure’s exit code.
One caveat about `-e`: failures inside conditional contexts (`if`/`while`/`&&`/`||`) do not trigger exit — that is a feature, and patterns like `cmd || exit 1` still need explicit handling.
#!/usr/bin/env bash
set -euo pipefail
# 调试期可加:逐条回显执行的命令
# set -xClean up with trap
When a script dies midway or gets killed by Ctrl+C, temp files, child processes and lock files are left behind. Hook cleanup onto `EXIT` with `trap`: it runs on normal exit, on error, and on interruption alike.
Create temp dirs with `mktemp -d` (never hardcode paths — avoids concurrency clashes and symlink attacks), and have the cleanup function remove only what it created.
tmpdir=$(mktemp -d)
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
# 需要 Ctrl+C / kill 也走一遍显式清理时可再加:
# trap cleanup INT TERMQuoting discipline: double-quote every expansion
An unquoted variable splits on spaces and vanishes when empty: `rm -rf $dir` with an empty dir becomes a bare `rm -rf` (disaster-grade). The discipline is simple — always write `"$var"`, never `$var`.
Three common companions: use `--` or a `./` prefix when filenames may start with `-`; use `${1:?message}` to fail fast on required args; use `${2:-default}` for optional ones.
rm -rf -- "$dir" # -- 防止文件名被当成选项
src="${1:?用法: $0 <源目录>}" # 缺参数直接报错退出
out="${2:-./dist}" # 提供默认值Validate inputs, stay idempotent
Validate external inputs (positional args, env vars, find output) before use: check commands with `command -v`, paths with `[ -d ]`/`[ -f ]`. Fail at startup rather than halfway through.
Make scripts idempotent: `mkdir -p` over `mkdir`, `cp -f`/`rsync` over bare `cp`. A half-failed run can then simply be re-run without manual cleanup.
command -v jq >/dev/null || { echo "需要 jq" >&2; exit 1; }
[ -d "$src" ] || { echo "目录不存在: $src" >&2; exit 1; }
mkdir -p "$out" # 幂等:已存在不报错Debugging and the pre-production checklist
Three tools cover ninety percent of issues: `bash -n` checks syntax only, `bash -x` traces every line, and shellcheck statically catches nearly all of the pitfalls above, including unquoted variables. Wire shellcheck into CI and script-quality problems mostly vanish.
Final checklist: shebang + `set -euo pipefail` + trap cleanup + double-quoted expansions + input validation + idempotent operations + shellcheck passing. Copy this skeleton and your scripts go from "good luck" to "trustworthy".
bash -n deploy.sh # 只查语法
bash -x deploy.sh # 逐行回显执行(调试)
shellcheck deploy.sh # 静态分析(推荐装进 CI)