SysOps

Bash Script Safety: set -euo pipefail and trap for Trustworthy Scripts

A Bash script that "finishes" is not a script that "worked": failed commands do not stop it, empty variables pass silently, and pipes hide upstream crashes. This guide fixes all of them with a copy-ready skeleton (set -euo pipefail + trap + quoting discipline).

By 巧匠 Team·7 min read·Updated 2026-08-31

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 -x

Clean 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 TERM

Quoting 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)