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

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.