Shell脚本精读 · S09-02 | 管道与管道退出码:`pipefail` 再探

📅 2026/7/6 23:41:03 👁️ 阅读次数 📝 编程学习
Shell脚本精读 · S09-02 | 管道与管道退出码:`pipefail` 再探

模块:S09 进程、管道与重定向
篇号:S09-02 / 42
预计阅读:45 分钟
主线:Bash


文章目录

    • 本篇目标
    • 30 秒速览
    • 正文
      • 1. 管道是什么
      • 2. 并行,不是「先跑完再跑」
      • 3. 默认退出码:只看最后一个
      • 4. `set -o pipefail` 再探(衔接 S01-04)
      • 5. `PIPESTATUS`:逐节查退出码
      • 6. 与 `set -e` 的配合
      • 7. stderr 与管道
      • 8. 多节管道与重定向
      • 9. 读脚本时的常见模式
        • 9.1 过滤 + 统计
        • 9.2 检查中间步骤(无 pipefail 时)
        • 9.3 `| while read` 与退出码(两个问题)
        • 9.4 显式检查第一节
      • 10. 对照表
      • 11. 注意点
    • 读脚本检查清单
    • 练习
      • 判断题
      • 实操题
      • 改错题
    • 下一篇预告

本篇目标

掌握管道cmd1 | cmd2的数据流与退出码规则:默认$?只看管道最后一节set -o pipefail为何与set -e配套(S01-04)。会用PIPESTATUS检查每一节,读懂grep | awk| while read等常见组合,并避开「前面命令失败了脚本却继续」的坑。


30 秒速览

  • A | B:A 的stdout接到 B 的stdin;两命令并行执行。
  • 默认$?:等于管道最后一个命令的退出码。
  • set -o pipefail:任一命令失败 →整管道视为失败(与 S01-04 骨架一致)。
  • PIPESTATUS:数组,按顺序存管道每一节的退出码(Bash)。
  • stderr 默认不进管道;要合并 stderr 用2>&1 |或 Bash|&
  • | while read右侧常在子 shell(S07-02);与$?是不同问题。

正文

1. 管道是什么

grepERROR app.log|wc-l
部分含义
grep ...从文件筛行,结果写到stdout
|把左边 stdout接到右边 stdin
wc -l从 stdin 读入,统计行数

左边不必写文件:数据流经管道,可省临时文件。

psaux|grepnginxcataccess.log|tail-n100

2. 并行,不是「先跑完再跑」

yes|head-n3
  • yes持续输出
  • head -n 3读满 3 行后退出
  • yes可能收到SIGPIPE被终止

读脚本:管道两端同时在跑;读者提前结束时,写者可能失败(broken pipe),这在set -o pipefail下会影响$?


3. 默认退出码:只看最后一个

false|trueecho$?# 0
命令退出码
false1
true0
管道整体$?0(默认取true
grep-qNOMATCH file.txt|echookecho$?# 0 — grep 失败,但 echo 成功

读脚本陷阱:管道前面grepfindcurl失败了,只要最后一条是trueteewc成功,默认$?仍为 0


4.set -o pipefail再探(衔接 S01-04)

set-efalse|trueecho"仍会执行"# 管道整体 $? 为 0,set -e 不触发
set-euopipefailfalse|trueecho"不会执行"# 管道失败,set -e 退出
模式管道失败条件
默认最后一个命令非 0
pipefail任一命令非 0

专栏骨架set -euo pipefail的意义之一:
cmd1 | cmd2 | cmd3cmd1失败不会被cmd3的成功掩盖

关闭:set +o pipefail


5.PIPESTATUS:逐节查退出码

Bash 在管道执行后填充PIPESTATUS数组,下标从 0 起):

false|true|falseecho"whole=$?"echo"pipestatus=${PIPESTATUS[*]}"# whole=0(默认,最后 true)# pipestatus=1 0 1
set-opipefailfalse|true|falseecho"whole=$?"# 1echo"0=${PIPESTATUS[0]}"# 1echo"1=${PIPESTATUS[1]}"# 0echo"2=${PIPESTATUS[2]}"# 1

注意

  • PIPESTATUS在下一条命令执行后会被覆盖;要保存就及时赋值:
grepfoo f|sortstatus=("${PIPESTATUS[@]}")echo"grep=${status[0]}sort=${status[1]}"
  • if&&里跑管道后,PIPESTATUS仍可用(紧接的下一条语句前)。

6. 与set -e的配合

场景行为要点
set -e+ 管道管道整体失败时退出(失败定义见上)
pipefail仅最后命令失败才使管道失败
cmd || true故意吃掉失败(S04-02)
set-euopipefailgreppattern"$f"|process# grep 无匹配(退出 1)→ 整管道失败 → 脚本退出

若「无匹配不算错误」:

greppattern"$f"|process||true# 或ifgreppattern"$f"|process;then...

7. stderr 与管道

默认:只有stdout进管道;stderr仍去终端(或外层重定向)。

# 只要 stdout 进 wc;grep 的错误信息仍可能在终端greppattern missing|wc-l

把 stderr 也并入管道:

greppattern missing2>&1|wc-l

Bash 还提供|&(同2>&1 |):

cmd|&other

读脚本:日志里「错误混进下游」时,看是否有2>&1


8. 多节管道与重定向

grepERROR app.log|sort|uniq-c|sort-nr

$?/PIPESTATUS每一节都有对应下标。

重定向作用在整个管道(常见写法):

grepERROR app.log|teereport.txt|wc-l# tee 同时写文件和 stdout,wc 统计行数
cmd1|cmd2>out.log2>&1# 只对整条管道末尾 cmd2 的 stdout/stderr 重定向规则见 S09-01# 更稳妥:{ cmd1 | cmd2; } >all.log 2>&1

复杂组合时,用{ ...; }分组(S09-03 子 shell 详讲)。


9. 读脚本时的常见模式

9.1 过滤 + 统计
awk'$3 > 500 {print}'access.log|wc-l
9.2 检查中间步骤(无 pipefail 时)
curl-fsS"$url"|jq.# 有 pipefail:curl 失败 → 管道失败# 无 pipefail:jq 解析空输入也可能 exit 0,危险
9.3| while read与退出码(两个问题)
n=0grep.f|whileread-rline;do((n++));doneecho"$n"# 子 shell:n 仍为 0(S07-02)grep.f|sort|whileread-rline;do...;done# sort 失败 + pipefail → 脚本可能退出;n 的问题仍要用 < f 解决
9.4 显式检查第一节
set-euopipefail{false;echogen;}|head-n1# 有 pipefail 时 false 导致管道失败

或不用 pipefail、手动查PIPESTATUS[0](脚本片段里少见)。


10. 对照表

问题默认pipefail
false | true$?01
grep 无匹配 | wc$?常为 0非 0
set -e是否因左端失败退出
查每一节PIPESTATUSPIPESTATUS

11. 注意点

说明
不要用$?猜第一节默认只有最后一节
PIPESTATUS及时保存下一条命令会清掉
SIGPIPE读者提前关闭,写者可能非 0
交互命令管道右端读终端时要小心(少见)
可移植pipefailPIPESTATUS为 Bash 等扩展;纯 sh 见 S13

读脚本检查清单

  • 脚本是否开了set -o pipefail(或等价)?
  • grep | ...左端失败是否会被掩盖?
  • 是否需要2>&1 |把错误也送进管道?
  • 是否误用| while read又在循环外用累加变量?
  • 检查每一节时是否用了PIPESTATUS而不是$?

练习

判断题

  1. A | B中,A 完全结束后 B 才开始。
  2. 默认情况下false | true$?为 0。
  3. 开启pipefail后,false | true$?为非 0。
  4. PIPESTATUS[0]表示管道第一个命令的退出码。
  5. 管道会把左边命令的 stderr 默认送进右边。
参考答案
  1. 错(并行;读者满或退出时写者可能 SIGPIPE)。
  2. 对。
  3. 对。
  4. 对。
  5. 错(默认只有 stdout)。

实操题

运行并写出$?${PIPESTATUS[*]}(Bash):

bash-c'false | true; echo exit=$?; echo pipe=${PIPESTATUS[*]}'bash-c'set -o pipefail; false | true; echo exit=$?; echo pipe=${PIPESTATUS[*]}'
参考答案

无 pipefail:exit=0pipe=1 0
有 pipefail:exit=1pipe=1 0

改错题

#!/usr/bin/env bashset-egrepERROR"$LOG"|mail-salert admin@example.comecho"alert sent"

grep无匹配时仍打印alert sent。如何改?

参考
set-euopipefailgrepERROR"$LOG"|mail-salert admin@example.comecho"alert sent"

或业务上允许无匹配:

ifgrepERROR"$LOG"|mail-salert admin@example.com;thenecho"alert sent"elseecho"no ERROR or mail failed">&2fi

下一篇预告

S09-03:《子 Shell 与命令替换:()$()》—$(cmd)`cmd`、子 shell( )与管道的区别。