使用 set -euo pipefail 启用严格模式
启用快速失败行为,并准确了解严格模式的每个标志能捕获和遗漏哪些错误。
使用 set -euo pipefail 启用严格模式 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。
为什么 Bash 默认会无声失败
默认情况下,即使命令失败,Bash 仍会继续运行。这会导致生产脚本出现细微且难以调试的灾难。
请看下面这个尝试创建备份的脚本:
- 路径中的拼写错误导致
cp失败 - Bash 忽略该失败并继续运行
- 尽管数据从未备份,脚本仍报告成功
这就是无声失败问题。严格模式通过让 Bash 的行为类似编译型语言来解决此问题:一旦出现问题就立即停止。
#!/usr/bin/env bash
# Without strict mode — dangerous default behavior
cp /important/data /backups/data # fails (path doesn't exist)
echo "Backup complete" # still prints — false confidence!
rm -rf /tmp/staging # still runs — potentially destructive三个核心标志:set -euo pipefail
在每个脚本开头附近加入以下行即可启用严格模式:
set -euo pipefail
这会启用三个不同的保护机制:
-e— 如果任何命令返回非零状态,则立即退出-u— 将未设置的变量视为错误(而不是展开为空字符串)-o pipefail— 如果管道中的任何命令失败,整个管道就会失败,而不只是最后一个命令失败
它们共同构成了严肃 Bash 脚本的标准防御性文件头。每个标志都能捕获一类不同的错误。
#!/usr/bin/env bash
set -euo pipefail
echo "Strict mode is now active"
echo "Every command failure will abort the script"理解 set -e(errexit)
set -e(也可写作 set -o errexit)会使脚本在某个命令以非零状态退出时立即退出。
需要了解的关键行为:
- 简单命令:
false、grep pattern file(没有匹配结果)以及ls /nonexistent都会触发退出 - 脚本中最后一个命令的退出代码会成为脚本的退出代码
if条件中的命令不受此规则影响——-e不会对测试表达式生效- 后面跟有
|| true的命令也不受此规则影响(见后续内容)
可以将 -e 理解为防止脚本在失败后无声地继续运行的第一道防线。
#!/usr/bin/env bash
set -e
echo "Before failure"
ls /this/path/does/not/exist # exits here with code 2
echo "This line never runs"理解 set -u(nounset)
set -u(也可写作 set -o nounset)会使 Bash 将对未设置变量的任何引用视为致命错误。
没有 -u 时,像使用 $FLENAME 代替 $FILENAME 这样的拼写错误会无声地展开为空字符串,导致命令行为异常,甚至造成危险后果(试想在 $TMPDIR 未设置时执行 rm -rf "$TMPDIR/")。
重要例外:
${VAR:-default}— 安全的默认值替换,不会触发-u${VAR:+value}— 条件展开,同样安全- 未传入位置参数时,
"$@"和"$*"不受此规则影响
#!/usr/bin/env bash
set -euo pipefail
# Safe: provide a default for optional vars
OUTPUT_DIR="${1:-/tmp/output}"
LOG_LEVEL="${LOG_LEVEL:-info}"
echo "Writing to: $OUTPUT_DIR"
echo "Log level: $LOG_LEVEL"
# This would abort the script:
# echo "$UNDEFINED_VAR" # bash: UNDEFINED_VAR: unbound variable理解 -o pipefail
没有 pipefail 时,管道的退出状态完全由最后一个命令决定。前面命令的失败会被无声地吞掉。
未启用 pipefail 时的示例:
cat /missing/file | wc -lcat以退出代码 1 失败,但wc -l以代码 0 成功- 管道返回 0——表示成功!尽管数据已经丢失。
启用 pipefail 后,Bash 会返回最右侧失败命令的退出代码。这样就能发现并捕获管道失败。
注意:pipefail 不是字母标志,必须使用 -o pipefail 进行设置。
#!/usr/bin/env bash
set -euo pipefail
# With pipefail: this aborts if grep finds nothing (exit 1)
# grep returns 1 when no match found
ps aux | grep "[n]ginx" | awk '{print $2}'
echo "If we reach here, nginx is running"有意让命令失败——使用 || true
有时命令允许失败。使用 set -e 时,您必须明确指出哪些失败可以容忍,否则脚本会中止。
惯用的解决方案是 || true,它会追加一个始终成功的后备操作:
command || true— 完全忽略失败command || echo "Warning: step failed, continuing"— 记录警告并继续command || { echo "fatal"; exit 1; }— 自定义失败处理
这种模式会在代码中明确表达您的意图:普通命令表示“此命令必须成功”;|| true 表示“此命令可能失败,但没关系”。
#!/usr/bin/env bash
set -euo pipefail
# Remove temp dir if it exists — OK if it doesn't
rm -rf /tmp/my_workspace || true
mkdir -p /tmp/my_workspace
# Check if a service is running — OK if not
if systemctl is-active --quiet nginx 2>/dev/null || true; then
echo "nginx is active"
fi
# Grep that may find nothing — OK
grep 'ERROR' /var/log/app.log || true
echo "Done"set -e 无法捕获什么
set -e 存在一些众所周知的例外和陷阱。了解这些情况可以避免产生错误的信心:
if/while/until条件中的命令——测试表达式按设计不受此规则影响- 使用
!取反的命令——! false不会触发退出 ||之前的最后一个命令——例如false || handle_error- 特定上下文中的子 Shell 退出状态——例如在某些 Bash 版本中,
VAR=$(failing_command) - 函数返回值——只有函数中的最后一个命令会计入结果
严格模式不能替代显式的错误检查;它是一张安全网,可以捕获大多数意外失败。
#!/usr/bin/env bash
set -euo pipefail
# These do NOT trigger -e:
if false; then echo "never"; fi # -e exempt in conditions
! false # negation exempts
false || echo "handled" # || exempts the left side
# This DOES trigger -e (no condition, no ||):
# false
echo "Script continues after exempted failures"严格模式下的子 Shell 与函数
严格模式设置会由子 Shell 继承,但在函数和命令替换中的行为较为微妙。
关键规则:
- 函数会从调用它的 Shell 继承
-e、-u和pipefail - 函数返回非零值会导致调用方退出(当设置了
-e时)——但如果调用位于条件中或位于||之后,则不会退出 - 命令替换
$():在较旧的 Bash 中,$()内部失败的命令可能不会触发父 Shell 的-e;为确保安全,请先赋值,再单独使用 - 显式子 Shell
()会继承所有标志
#!/usr/bin/env bash
set -euo pipefail
setup_workspace() {
local dir="$1"
mkdir -p "$dir" # fails here if permissions denied
cd "$dir"
echo "Ready in $(pwd)"
}
# Safe pattern: assign result then use it
TODAY=$(date +%Y-%m-%d) # capture separately
WORKDIR="/tmp/run_${TODAY}"
setup_workspace "$WORKDIR"
echo "Workspace: $WORKDIR"将严格模式与错误捕获结合使用
严格模式告诉 Bash 何时停止。在 ERR 上设置 trap,可以让您在脚本退出之前执行清理或诊断操作。
常见模式如下:
- 在开头设置严格模式
- 定义
cleanup或on_error函数 - 使用
trap 'on_error' ERR注册该函数 - 还可以选择针对
EXIT设置捕获,以确保无论成功还是失败都能执行清理
重要:请使用 set -E(大写 E,也称为 errtrace),以便函数和子 Shell 也能继承 ERR 捕获;没有它,捕获操作只会在主 Shell 主体中触发。
#!/usr/bin/env bash
set -Eeuo pipefail
on_error() {
local exit_code=$?
local line_number=${BASH_LINENO[0]}
echo "ERROR: command failed with code ${exit_code} at line ${line_number}" >&2
}
cleanup() {
echo "Cleaning up temporary files..." >&2
rm -rf /tmp/my_run_dir 2>/dev/null || true
}
trap on_error ERR
trap cleanup EXIT
mkdir -p /tmp/my_run_dir
echo "hello" > /tmp/my_run_dir/output.txt
cat /tmp/my_run_dir/output.txt
echo "Done"在局部禁用严格模式
有时某段代码就是有意写得“不严格”——例如探测可选工具,或运行因非错误原因返回非零值的旧命令。您可以暂时禁用严格模式,并在之后恢复它。
安全模式如下:
- 使用
set +e保存状态(禁用-e),运行代码块,然后使用set -e重新启用 - 或者使用子 Shell
( set +e; ... ),这样就不会影响父 Shell 的标志 - 风险代码块结束后应立即重新启用标志;让标志保持关闭是产生错误的常见原因
当代码块包含多个命令时,优先使用子 Shell 形式,因为它会在退出时自动恢复标志。
#!/usr/bin/env bash
set -euo pipefail
# Probe for optional tools without aborting
HAS_JQ=false
(
set +e
command -v jq > /dev/null 2>&1
[[ $? -eq 0 ]] && echo "jq_found"
) && HAS_JQ=true || true
if [[ "$HAS_JQ" == "true" ]]; then
echo "jq is available — using JSON output"
else
echo "jq not found — using plain text"
fi完整的严格模式脚本模板
下面是一个适用于生产环境的模板,汇集了本课程介绍的所有严格模式最佳实践:
set -Eeuo pipefail— 包含errtrace在内的全部四个标志IFS=$'\n\t'— 更安全的单词拆分(避免按空格拆分)- 用于诊断和清理的 ERR + EXIT 捕获
- 为可选参数设置明确的默认值
- 使用
readonly和local限制变量作用域
请将此模板复制到每个非简单 Bash 脚本的开头,从而立即获得快速失败行为和可追踪的错误。
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
# ── Constants ────────────────────────────────────────────
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
# ── Trap handlers ────────────────────────────────────────
err_handler() {
echo "[${SCRIPT_NAME}] ERROR on line ${BASH_LINENO[0]}: exit ${?}" >&2
}
cleanup() {
echo "[${SCRIPT_NAME}] Exiting" >&2
}
trap err_handler ERR
trap cleanup EXIT
# ── Defaults ─────────────────────────────────────────────
ENV="${1:-production}"
MAX_RETRIES="${MAX_RETRIES:-3}"
# ── Main ─────────────────────────────────────────────────
main() {
echo "Running in env=${ENV}, max_retries=${MAX_RETRIES}"
echo "Script dir: ${SCRIPT_DIR}"
}
main "$@"知识检查:pipefail 的行为
测试您对 pipefail 如何影响管道退出代码的理解。
回顾:使用 set -euo pipefail 的严格模式
在本课中,您学习了如何使用严格模式,让 Bash 脚本快速失败并明确报告失败。
三个标志及其防护内容:
-e(errexit)——任何返回非零状态的命令都会导致退出;在条件中以及||之后不适用-u(nounset)——引用未设置的变量时中止;对于可选变量,请使用${VAR:-default}-o pipefail——只要管道中的任一阶段失败,整个管道就会失败,而不仅仅是最后一个阶段失败
配套实践:
- 添加
-E(errtrace),让 ERR 陷阱传递到函数中 - 在
ERR和EXIT上使用trap进行诊断和清理 - 使用
|| true有意容忍失败 - 在子 shell 中使用
set +e临时禁用该功能,以处理旧代码或探测代码
严格模式并非万能药——您需要了解它的例外情况——但它是编写可靠且具有防御性的 Bash 脚本最有效的单一习惯。
常见问题解答
「使用 set -euo pipefail 启用严格模式」课时是免费的吗?
是的 — 「使用 set -euo pipefail 启用严格模式」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。
「使用 set -euo pipefail 启用严格模式」这节课中我会学到什么?
启用快速失败行为,并准确了解严格模式的每个标志能捕获和遗漏哪些错误。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DevOps Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「使用 set -euo pipefail 启用严格模式」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DevOps Bootcamp 课中编写并运行代码吗?
能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 set -euo pipefail 启用严格模式
- 用于清理和处理信号的陷阱处理器
- 安全的临时文件与锁目录
- 幂等脚本与带退避的重试逻辑