编程 `set -euo pipefail` 写了,函数里失败却继续跑:errexit 的例外与 ERR trap 的继承

2026-09-15 00:03:29

set -euo pipefail 写了,函数里失败却继续跑:errexit 的例外与 ERR trap 的继承

脚本第一行是 set -euo pipefail,函数里一条命令失败了,脚本却没有停下来;trap 'echo ERR' ERR 写在函数外面,函数内部失败时 trap 也不触发。这两个都是 man bash 里明确列出的例外,不是 bug(大部分情况下)。

-e / errexit 的例外清单

man bash 对 -e 的描述要点:

  • 若管道(可能是单条简单命令、一个 list、或一个复合命令)返回非零,则立即退出。
  • 但在以下上下文里 shell 不会退出:
  1. while / until 关键字后紧跟的命令列表里的命令;
  2. if 语句的 test 部分;
  3. 任何在 &&|| 列表中执行的命令,除了最后一个 &&|| 之后的命令;
  4. 管道中除最后一个命令以外的任何命令;
  5. 返回值被 ! 取反的命令。
  • 若一个非 subshell 的复合命令因为某命令失败返回非零(此时 -e 被忽略),shell 不会退出。
  • 如果设置了 ERR 上的 trap,shell 退出前会执行它。
  • 该选项分别作用于 shell 环境与每个 subshell 环境,可能导致 subshell 在跑完之前就退出。
  • 关键一句:如果一个复合命令或 shell 函数在 -e 被忽略的上下文中执行,那么即使 -e 是打开的、命令返回失败,这个复合命令或函数体内执行的任何命令都不会受 -e 影响。如果函数在这种上下文里再 set -e,也不会有任何效果,直到该函数调用完成。

ERR trap 遵守同一组例外

man bash 原文:

The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted using !.

这些条件与 -e 遵守的完全一致。

常见踩法

1. if func; then ...func || ...

函数体内 errexit 失效:函数里第一条失败命令不会终止脚本,函数继续跑完,返回最后一条命令的状态码。想「函数失败就回滚」的人会发现回滚逻辑根本没被触发。

2. ERR trap 不继承,要 set -E

man bash 关于 -E / errtrace:如果设置了此选项,任何 ERR 上的 trap 都会被 shell 函数、命令替换、以及子 shell 环境中执行的命令继承;通常这些情况下 ERR trap 不会被继承。所以要用 set -eE(等价 set -o errexit -o errtrace)。

没有 -E 时:函数里 ls /root 失败,因为 -e 生效,bash 直接从函数作用域退出,而那里没有生效的 ERR trap(未继承),所以 trap 不执行。

3. 命令替换 $(...)-e 默认关闭

子 shell 里 -e 默认被关掉(bash 在命令替换里关闭 -e,普通 subshell () 则不是):

bash -c 'set -e; echo $(false; echo survived)'
# 输出 survived

var=$(func) 这种赋值语句的退出码是「最后一个命令替换」的退出码,所以赋值本身会带上失败状态,外层会退出;但 echo "$(func)" 的退出码是 echo 的 0,失败被吞掉。

Bash 4.4+ 可用 shopt -s inherit_errexit 让命令替换内的错误也能传播;bash 以 sh 名字调用(POSIX 模式)时该选项默认打开。

4. 函数里 trap - ERR 取消不掉

没有 set -E 时不生效(函数内 trap 操作本身有作用域/继承问题)。Bash 4.4 之前还有 t1 覆盖 t2 的 bug(已在 4.4 修复)。加上 set -Etrap - ERR 才在函数内按预期取消。

5. ret=$(func1) 里 func1 用 exit 1

子 shell 以 1 退出,赋值语句退出码也是 1,外层 -e 会退出。看起来对了,但原因是 exit 而非 -e,容易误判 -e 生效范围。

一组完整的 set 选项(Bash 4.4+)

#!/usr/bin/env -S bash -euET -o pipefail -O inherit_errexit
  • -u / -o nounset:解引用未设置变量即退出,挡拼写错误。
  • -e / -o errexit:非零状态直接退出(有上述例外)。
  • -o pipefail:管道任一元素失败即整体失败,而不是只取最后一条的状态码。
  • -O inherit_errexit(= shopt -s inherit_errexit):把退出语义扩展到子 shell / 命令替换,堵住 $(...) 的漏洞。
  • -E / -o errtrace:ERR trap 继承进函数体、命令替换、子 shell。注意 -E 只影响 trap 的继承,跟 -e 本身无关。
  • -T / -o functrace:DEBUG / RETURN trap 进函数体。
  • -S 是让 env 接受多参数的写法。

想拿函数退出码又要函数内 errexit 生效:包装函数

callfunc() {
set +e
( set -e; "$@" )
EXIT_CODE=$?
set -e
}
callfunc bar
if [[ ${EXIT_CODE} -eq 0 ]]; then ...

优点:对函数无侵入,函数体按 errexit 写。缺点:函数在 subshell 里跑,函数内对全局/环境变量的修改只在该 subshell 生效,传不回父进程。

trap 配方

trap 'printf "\e[31m%s: %s\e[m\n" "ERR" $?' ERR
trap 'echo "Error at line $LINENO: exit $?" >&2' ERR
trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT
  • ERR trap 可用 $LINENO 报出错行、$? 报退出码(建议输出到 stderr)。
  • EXIT trap 无论正常结束还是异常退出都会执行,是临时资源清理的兜底;但它成功时也会触发,所以只在需要时用 $? 判断。
  • eprintf / 日志写 stderr。

静态检查:ShellCheck

  • 项目地址:,官网 ,GPL-3.0,Haskell 实现,支持 sh/bash/dash/ksh,按 shebang 判方言。
  • 基本用法:shellcheck deploy.sh;按 Bash 规则加 -s bash;跟踪 source 用 -x -s bash main.sh
  • 高频编号:SC2086 变量未加引号、SC2046 命令替换未加引号、SC2164 cd 失败未处理(补 || exit 1)、SC2181 误用 $?(直接用 if cmd;)、SC2154 变量未赋值、SC1090 无法解析非常量 source、SC3043 POSIX sh 未必支持某选项(如 set -o pipefail)。
  • CI 阻断:shellcheck -S warning deploy.sh scripts/*.sh,非零即失败。
  • 抑制单条:行上或上一行写 # shellcheck disable=SC2034;先修再 disable,disable 旁写理由,别整体 disable=SC2086
  • ShellCheck 不做的事:不执行脚本,抓不到运行时(缺二进制、权限、网络、路径)错误;业务逻辑错误也抓不到。

单测:Bats

  • 项目地址:,文档 ,可覆盖成功与失败双向路径。
  • 与 set 选项相关:#!/bin/sh + set -o pipefail 会触发 SC3043,要么改 shebang 为 bash,要么去掉 pipefail。

不适用边界

  • 需要最大可移植性(POSIX sh)时,pipefail、inherit_errexit、[[ ]] 都不是 POSIX;只能退到 set -eu[ ]
  • set -e 语义在 POSIX 规范里定义得含糊,各 shell 实现大同小异但有差异;用 bash -x 跟踪(子 shell 会打 ++ 前缀)确认实际行为,别凭直觉。
  • 复杂流程依赖 set -e 却不配 ERR trap,等于出错后只有一句非零退出,没有上下文。
  • inherit_errexit 和 trap 修复都需要 Bash 4.4+;老版本(如 4.3)行为不同,部署到老机器要显式检查 echo $BASH_VERSION
复制全文 生成海报 脚本编程 Shell Bash 错误处理 ShellCheck

推荐文章

程序员茄子在线接单