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)