tee 有日志,构建就成功了吗?20 个 Bash 管道退出码实验

举报
yd_241387476 发表于 2026/11/03 09:13:16 2026/11/03
【摘要】 用20个独立实验区分日志存在、管道整体状态和各阶段退出码,验证 pipefail、PIPESTATUS、set -e 与 tee 的行为,并给出保留诊断信息和正确返回失败状态的写法。

在构建脚本后面加上 | tee build.log,可以同时看终端输出和保存日志。但“日志文件生成了”“终端没有停下来”和“构建成功”是三件不同的事。下面用合成命令复现这些差异,再给出一种同时保留整体状态和分段状态的写法。

本文讨论 Bash 的前台同步管道。实验只使用 true、false、printf、显式 exit 和 tee,不连接业务系统。每个场景都在独立临时目录、独立 Bash 进程中运行;测试开始时明确关闭 errexit 和 pipefail,随后按场景开启,避免继承终端配置影响结果。

一、先区分三个结果

看这条命令:

(printf 'partial\n'; exit 42) | tee build.log

左侧先输出一行文本,再用退出码 42 表示失败;右侧 tee 成功接收文本,并写入日志。默认配置下,整条管道却返回 0。

这不是 tee 把失败修好了。日志内容只是 partial,上游仍然以 42 退出。GNU Bash 手册规定,未启用 pipefail 时,管道的整体退出状态取最后一个命令的状态;此处最后一个命令就是 tee。(参考:GNU Bash Reference Manual,Pipelines 章节。)

因此,观察结果要分开记:上游退出码为 42;tee 退出码为 0;整条管道默认退出码为 0。日志是否存在无法替代其中任何一项。

tee 的职责是复制标准输入到标准输出和指定文件。需要注意,它默认覆盖已有的目标文件;本实验始终在新建的临时目录里运行,实际使用时应自行选择日志目录与追加策略。(参考:GNU Coreutils Manual,tee invocation 章节。)

二、pipefail 改的是状态规则

在 Bash 中启用:

set -o pipefail
(printf 'partial\n'; exit 42) | tee build.log

同样的输出、同样的日志内容,这次管道返回 42。只改变状态汇总规则,不能让失败的构建变成功,也不会回滚已经写出的文件。

多个阶段同时失败时,结果取管道位置最右侧的非零退出码:

set -o pipefail
(exit 7) | (exit 3) | true
printf 'pipeline=%s\n' "$?"
# pipeline=3

这里返回 3,不是 7。“右侧”指命令在管道中的排列位置,不应当理解为对失败发生时间的排序。上面的整体状态规则来自 Bash 手册,返回值则由实验脚本单独核验。

还要区分“失败状态”与“立即终止脚本”:

set -o pipefail
false | true
printf 'after\n'

这个场景仍然输出 after,而整个脚本最终返回 0,因为最后执行的 printf 成功了。pipefail 本身不等于 set -e。

三、PIPESTATUS 要及时保存

要知道具体哪一段失败,可以立即读取 Bash 数组 PIPESTATUS:

(exit 7) | (exit 3) | true
printf '%s\n' "${PIPESTATUS[*]}"
# 7 3 0

它记录最近一条前台管道中各命令的退出状态;单条命令也会更新它。这一定义可在 Bash 的变量手册中查到,本地 man bash 的 PIPESTATUS 条目也作了相同说明。(参考:GNU Bash Reference Manual,Bash Variables 中的 PIPESTATUS 条目。)

容易踩坑的是先保存 $?,再读数组:

set -o pipefail
(exit 7) | (exit 3) | true
rc=$?
printf 'rc=%s stages=%s\n' "$rc" "${PIPESTATUS[*]}"
# rc=3 stages=0

rc 正确保留了整体退出码,但赋值命令本身成功执行后,PIPESTATUS 已更新。此时输出的 0 不能再解释成原来的三个阶段全部成功。

实验采用同一条数组赋值,同时捕获两份状态:

set -o pipefail
(exit 7) | (exit 3) | true
snapshot=( "$?" "${PIPESTATUS[@]}" )
printf '%s\n' "${snapshot[*]}"
# 3 7 3 0

第一个元素是整条管道的状态,后续元素按顺序对应各个阶段。展开右侧表达式后,再执行数组赋值;后续打印、判断都使用保存好的数组。

四、set -e 也需要看使用位置

实验中,单独设置 set -e 后执行 false | true,脚本仍会继续,因为默认的管道整体状态是 0。设置 set -e -o pipefail 后,在普通命令位置执行同一条管道,脚本以 1 退出,后面的 printf 不再执行。

但不能把它概括成“出现非零就必定退出”。Bash 对 if 条件、部分 && / || 列表等位置有例外。本文只演示这些特定场景,不试图用一句口诀覆盖全部 shell 语义。(参考:GNU Bash Reference Manual,The Set Builtin 章节。)

如果希望保留日志、打印诊断并向调用方返回管道状态,可以显式处理成功和失败分支:

#!/usr/bin/env bash
set -e -o pipefail

if ./build.sh 2>&1 | tee build.log; then
  snapshot=( "$?" "${PIPESTATUS[@]}" )
else
  snapshot=( "$?" "${PIPESTATUS[@]}" )
fi

printf 'pipeline=%s build=%s tee=%s\n' \
  "${snapshot[0]}" "${snapshot[1]}" "${snapshot[2]}"
exit "${snapshot[0]}"

两个分支都把状态捕获作为第一条命令,避免诊断输出先覆盖现场。2>&1 将上游标准错误也交给 tee;这是日志合并选择,和是否启用 pipefail 是两个问题。

这里的 ./build.sh 指外部可执行脚本,它内部如何处理失败仍由自身负责。若换成 shell 函数,放进 if 条件可能影响函数内部的 -e 行为,不能直接照搬结论。也不要习惯性追加 || true:实验显示,这会让原本的失败路径继续执行并最终返回成功。

五、20 个场景的核验范围

独立脚本逐项比较退出码、标准输出、标准错误,并在涉及 tee 时读取实际日志内容。覆盖范围如下:

编号 场景 核验重点
1—3 默认管道规则 左侧失败、末尾失败、三段管道
4—7 pipefail 开关 左侧失败、右侧非零、全成功、重新关闭
8—12 后续执行与错误处理 pipefail 不自动退出、set -e 组合、条件分支、忽略失败
13—15 状态保存时机 直接读取、赋值覆盖、同时保存两份状态
16—18 tee 和日志 上游失败但日志存在、整体失败、标准错误合并
19—20 显式分支包装 失败和成功两条路径都正确保存与返回状态

本地验证环境为 macOS、GNU Bash 3.2.57,20/20 场景通过。

这组实验没有覆盖后台管道、进程替换、信号导致的退出、磁盘写满或其他 shell。特别是某个下游提前退出导致的 SIGPIPE,需要结合具体命令另做实验。本文的示例用于定位状态判断问题,不能替代构建工具自身的错误处理。

日常排查时,可以把“日志内容”“整条管道返回值”“每个阶段返回值”分别记录。只有明确需要的阶段都成功,才能继续后续发布或部署步骤。

本文由 AI 辅助整理,代码示例使用合成数据并通过独立脚本执行核验;未使用真实业务日志、账号凭据或生产环境数据。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。