模块: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-n1002. 并行,不是「先跑完再跑」
yes|head-n3yes持续输出head -n 3读满 3 行后退出yes可能收到SIGPIPE被终止
读脚本:管道两端同时在跑;读者提前结束时,写者可能失败(broken pipe),这在set -o pipefail下会影响$?。
3. 默认退出码:只看最后一个
false|trueecho$?# 0| 命令 | 退出码 |
|---|---|
false | 1 |
true | 0 |
管道整体$? | 0(默认取true) |
grep-qNOMATCH file.txt|echookecho$?# 0 — grep 失败,但 echo 成功读脚本陷阱:管道前面grep、find、curl失败了,只要最后一条是true、tee、wc成功,默认$?仍为 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 | cmd3里cmd1失败不会被cmd3的成功掩盖。
关闭:set +o pipefail。
5.PIPESTATUS:逐节查退出码
Bash 在管道执行后填充PIPESTATUS(数组,下标从 0 起):
false|true|falseecho"whole=$?"echo"pipestatus=${PIPESTATUS[*]}"# whole=0(默认,最后 true)# pipestatus=1 0 1set-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-lBash 还提供|&(同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-l9.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的$? | 0 | 1 |
grep 无匹配 | wc的$? | 常为 0 | 非 0 |
set -e是否因左端失败退出 | 否 | 是 |
| 查每一节 | PIPESTATUS | PIPESTATUS |
11. 注意点
| 点 | 说明 |
|---|---|
不要用$?猜第一节 | 默认只有最后一节 |
PIPESTATUS及时保存 | 下一条命令会清掉 |
| SIGPIPE | 读者提前关闭,写者可能非 0 |
| 交互命令 | 管道右端读终端时要小心(少见) |
| 可移植 | pipefail、PIPESTATUS为 Bash 等扩展;纯 sh 见 S13 |
读脚本检查清单
- 脚本是否开了
set -o pipefail(或等价)? grep | ...左端失败是否会被掩盖?- 是否需要
2>&1 |把错误也送进管道? - 是否误用
| while read又在循环外用累加变量? - 检查每一节时是否用了
PIPESTATUS而不是$??
练习
判断题
A | B中,A 完全结束后 B 才开始。- 默认情况下
false | true的$?为 0。 - 开启
pipefail后,false | true的$?为非 0。 PIPESTATUS[0]表示管道第一个命令的退出码。- 管道会把左边命令的 stderr 默认送进右边。
- 错(并行;读者满或退出时写者可能 SIGPIPE)。
- 对。
- 对。
- 对。
- 错(默认只有 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=0,pipe=1 0
有 pipefail:exit=1,pipe=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( )与管道的区别。