Bash set -e 的条件边界:函数放进 if 后,内部失败为什么继续执行

10-01 3阅读

脚本开头已经写了 set -e,一个包含多步操作的函数单独调用时会在失败后退出。后来为了输出更友好的错误信息,调用者把它放进 if 条件,失败步骤之后的命令却继续运行,甚至走进成功分支。这不是失败命令突然成功,而是调用上下文改变了自动退出规则。

Bash 对条件测试等位置有明确例外。如果函数在忽略 errexit 的上下文中执行,函数体内的命令也不会因为这个选项自动中断。失败命令后若还有成功的打印,函数最终状态可能变成零;调用者看到的便是最后一个命令的结果。

用固定失败替代真实部署步骤

下面保存为 demo.sh,再执行 bash demo.sh。例子只创建一个临时脚本并打印文字,使用 false 模拟失败,不执行任何部署。每个模式在新的 Bash 进程里运行,避免前一轮退出状态或选项影响下一轮,外层则保存日志并断言内容。

Bash set -e 的条件边界:函数放进 if 后,内部失败为什么继续执行

AI概念示意图:以抽象物件说明本文主题,不代表真实界面或运行结果。

#!/usr/bin/env bash
set -eu
work=$(mktemp -d)
cat > "$work/child.sh" <<'BASH'
set -e
bad() {
  printf 'start\n'
  false
  printf 'after\n'
}
good() {
  printf 'start\n'
  false || return $?
  printf 'after\n'
}
case "$1" in
  direct) bad ;;
  conditional)
    if bad; then printf 'accepted\n'; else printf 'rejected\n'; fi
    ;;
  fixed)
    if good; then printf 'accepted\n'; else printf 'rejected:%s\n' "$?"; fi
    ;;
esac
BASH
for mode in direct conditional fixed; do
  if bash "$work/child.sh" "$mode" > "$work/$mode.log"; then
    rc=0
  else
    rc=$?
  fi
  case "$mode" in
    direct)
      test "$rc" -eq 1
      test "$(cat "$work/$mode.log")" = 'start'
      ;;
    conditional)
      test "$rc" -eq 0
      test "$(cat "$work/$mode.log")" = $'start\nafter\naccepted'
      ;;
    fixed)
      test "$rc" -eq 0
      test "$(cat "$work/$mode.log")" = $'start\nrejected:1'
      ;;
  esac
  printf '%s status=%s\n' "$mode" "$rc"
  cat "$work/$mode.log"
done

三种调用分别发生了什么

direct 的子进程只打印 start,状态为一,说明 false 后没有继续。conditional 打印 start、after、accepted,状态为零:函数作为 if 条件执行,内部 false 没有触发退出,末尾打印成功又让函数报告成功。日志同时检查状态和动作,避免只看一个返回数字。

fixed 仍在 if 中调用,却只打印 start 和 rejected:1。关键在于 false 后面的显式 return,把失败作为函数接口的一部分传回去;后面的 after 因函数已经返回而不可达。外层子进程状态为零,是因为该模式已经成功演示并处理了预期失败,并不表示模拟步骤完成。

不要依赖调用者维持隐含条件

把 set -e 再写进函数内部,并不能在本来忽略它的上下文中立刻恢复期望的自动退出。给函数调用加上逻辑与、逻辑或或否定,也可能改变这一规则。可复用函数应该主动检查每个决定后续工作能否继续的关键命令,而不是要求所有调用者都记住某种特殊写法。

简单情况可用“命令失败便 return 原状态”的形式。需要清理和打印时,先在失败分支保存状态,再执行其他命令,最后返回保存的数值。否则一次成功的日志输出就会覆盖原来的失败。若用否定条件,分支里看到的状态可能已经被取反,也需要小心。

本例外层使用 if 启动一个新的 bash 进程,并不会把当前 shell 对函数条件的处理直接传进新进程。子进程读取脚本后按自己的 set -e 和语法上下文执行。这正是 direct 可以稳定演示中断的原因,不应把外部命令和同一 shell 内的函数混为一谈。

验收函数是否真正在失败处停住

将 false 换成可控的测试替身,分别放在第一步、中间一步和最后一步。每次同时检查返回状态、输出文件是否产生,以及后续操作是否发生。单独调用与作为条件调用都要覆盖;只测整段脚本在终端上看似失败,容易漏掉其中已经发生的副作用。

errexit 可以帮助一些直线流程尽早退出,但不是事务机制,也不会回滚已完成的写入。即使修复了停止位置,前面创建的文件仍可能存在。清理与恢复要按任务另外设计,不能把“后面的步骤没执行”扩大解释为“系统已经恢复到开始状态”。

示例使用 Bash 语义,应明确以 bash 运行,不要改成 sh 后假定所有细节一致。代码里的日志和断言比记忆一条“失败就退出”的口号更可靠:它们直接证明这个调用方式下,哪条命令执行过,失败如何被交还给调用者。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。