退出状态是 shell 用来决定下一步执行什么的小整数。对 shell 控制流来说,零表示成功,非零进入失败分支,但这并不意味着每个数字都有统一的业务含义。搜索没有匹配、输入格式错误和服务器不可用,都可能返回非零,却需要不同处理。决定重试、忽略或终止之前,应先阅读目标命令的契约。不能只因为状态不为零,就把所有结果归为同一种事故。
让状态始终对应原来的命令
Bash 通过 $? 提供最近前台命令的状态。需要保留时,必须在记录日志、执行另一个条件判断或清理操作之前保存;这些操作也有自己的状态,会覆盖原本想观察的证据。函数通常使用最后执行的命令状态作为返回值,因此,末尾一句成功的日志输出就可能把前面的失败包装成整个函数成功,调用者看到的结果与实际工作脱节。
当命令成功与否决定后续动作时,可以使用 if command; then ... else ... fi。如果需要精确退出码,在 else 分支第一步保存 $?,输出诊断或清理后再通过 return 或 exit 显式传播。不要写成 if ! command,却期待进入成功分支后的 $? 仍然是原始失败码:逻辑取反已经把非零结果转换成成功,数值含义在这里发生了变化。
Bash 在找不到命令时使用一百二十七,找到却无法执行时使用一百二十六。因致命信号终止时,它使用一百二十八加信号编号的约定。这些规则有助于理解 shell 的报告,但应用也可以主动返回相同数字。因此,只有一个状态值还不足以证明内部具体发生了哪件事;应同时保留失败操作的身份和标准错误信息,避免根据数字臆造故障原因。
坏掉的样子
管道默认使用最后一段的退出状态。上游生成器失败时,下游仍可能成功读取一个空流或不完整数据,从而让整体看起来成功。开启 pipefail 后,Bash 返回从左到右最靠右的非零状态;全部成功才为零。PIPESTATUS 能提供每一段的结果,但后续命令同样会覆盖这个数组,所以必须先完整保存观察结果,再进行打印或判断。
set -e 开启的 errexit 具有由语法和调用上下文决定的例外。作为 if 条件的失败、与或列表中的部分命令,以及其他文档规定的场景,都不会自动结束脚本。函数被用作条件时,函数体中的失败命令之后也可能继续执行。ERR trap 也有类似排除规则,并不是每次失败都会产生的通知机制。两者都不能证明整个脚本已经可靠处理所有错误。
大范围追加 || true 会丢掉有用区别。只有该操作所有失败都可接受时,这种写法才表达了真实需求;否则,它可能一起吞掉权限错误或命令不存在等意外问题。应单独处理文档规定的预期结果,并继续传播其他失败。另外,first && second || recover 会在 second 失败时执行 recover,它并不等同于只判断 first 的 if 语句,清理逻辑不能建立在这个错误等价关系上。
后台任务还存在启动与完成之间的边界。成功启动并不表示任务最终成功,应立即保存 $! 中的任务标识,并检查等待该任务时 wait 返回的状态。如果启动多项必需工作,就应分别保留标识并收集结果,否则最后一条成功的前台命令可能掩盖已经在后台失败的工作。返回给调用者的状态应对应业务所需的全部完成条件。
快速验证
下面完整的 Bash 实验主动构造失败,并断言状态如何传播。它检查即时保存、逻辑取反、被当作条件调用的函数,以及启动后失败的后台任务。不需要准备文件,也不会访问外部服务;其中的非零结果是有意安排的测试输入。
bash --noprofile --norc <<'BASH'
set -e
fail_seven() { return 7; }
if fail_seven; then
exit 1
else
saved=$?
fi
printf 'saved status %s\n' "$saved"
[[ $saved -eq 7 ]]
[[ $? -eq 0 ]]
if ! fail_seven; then
negated=$?
fi
[[ $negated -eq 0 ]]
continued=no
conditional_function() {
false
continued=yes
}
if conditional_function; then
[[ $continued == yes ]]
else
exit 1
fi
(exit 6) &
job_pid=$!
if wait "$job_pid"; then
exit 1
else
job_status=$?
fi
[[ $job_status -eq 6 ]]
printf 'PASS: saved failure, negation, conditional context, wait\n'
BASH
预期首先打印保存的退出码七,然后出现通过提示。取反后的状态应为零;即使开启 errexit,条件函数仍然执行到修改变量的语句;wait 则应报告子任务的退出码六。这些断言揭示控制流的边界,不会替任意工具定义状态码含义。实际采用时,还应分别测试预期的否定结果和意外运行错误,并确认包装脚本最终向调用者返回了真正需要的结果,而不是最后一次打印或清理的偶然状态。
来源
- GNU Bash:退出状态,shell 状态约定,已核验 2026-09-06。
- GNU Bash:Set 内建命令,errexit 的排除场景,已核验 2026-09-06。
- GNU Bash:管道,管道整体状态规则,已核验 2026-09-06。