侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 160 篇文章
  • 累计收到 7 条评论

管道里的 while 循环,为什么改不动外面的变量

2026-10-7 / 0 评论 / 5 阅读

管道里的 while 循环,为什么改不动外面的变量

一段脚本从文件里逐行读,循环里每读一行计数加一,循环结束后把计数打出来。计数是 0,可循环体明明跑过三遍,循环里也老老实实打了三行日志。把同一个脚本改成从文件重定向进循环,计数立刻变成 3。差别不在写法顺不顺眼,在这段循环跑在哪个 shell 里。

为什么循环外读到的是 0

== 0. 环境 ==
  bash: 3.2.57(1)-release
  zsh : 5.9
  样本文件行数: 3

== 1. bash:cat 管道进 while,循环里在数,循环外看结果 ==
    循环内第 1 次读到 alpha
    循环内第 2 次读到 beta
    循环内第 3 次读到 gamma
  循环外的 n=0

管道两端各起一个进程。bash 执行管道时,为了不污染当前 shell 的变量,会给最后一段再套一层子 shell,循环就跑在那里面。循环里加到 3 的那份 n 是子 shell 的,主 shell 里那份从头到尾是 0。

循环里能打出三行日志,容易让人以为变量也改过来了。那三行本身就是在子 shell 里打的,打印正常,变量不共享。

换成重定向为什么就好了

== 2. bash:换成重定向进 while ==
  循环外的 n=3

重定向只是把文件接到标准输入上,没有新进程,循环体在当前 shell 里跑,改的就是同一份变量。这是最省事的改法,代价是它只能读文件或者设备,接不了上游命令的输出。

要让上游命令的输出进循环,进程替换也可以:把命令写在 <(...) 里当成文件名传给重定向,循环仍然在当前 shell 里。

== 4. bash:用进程替换当输入 ==
  循环外的 n=3

== 5. bash:管道两端的进程号 ==
  主脚本的 PID=3152
  直接跑 sh,它的父进程=3152(就是主脚本)
  管道里那一层组命令,父进程=3163(和主脚本不是一个号)
  管道里 $$ 不会变,仍然是主脚本的号

== 6. zsh:同一条管道 ==
  zsh 管道版本 n=3
  zsh 重定向版本 n=3

别的 shell 不是这个行为

zsh 的管道最后一段默认就在当前 shell 里执行,同样一条管道,zsh 给出的计数是 3。也就是说这段脚本从 bash 换到 zsh,原来错的写法会自己变对,反过来也会让人以为写法没问题。

bash 后来加过一个开关,让管道最后一段在当前 shell 里跑。本机的 bash 是 3.2.57,试这个开关时直接报错:

== 3. bash:试一下 shopt -s lastpipe ==
  本机 bash 不认识 lastpipe 这个选项:invalid shell option name

macOS 自带的 bash 就停在这个版本,这个开关在它上面不存在。要在本机用,得自己装一个新版 bash,路径还得写绝对路径才能避免用回系统那个。

怎么看出自己在不在子 shell 里

$$ 帮不上忙。它在子 shell 里不会变成子 shell 的进程号,仍然打印主脚本的号,拿它做判断会得出「我在主 shell 里」的结论。比较父进程号能看出来:

== 5. bash:管道两端的进程号 ==
  主脚本的 PID=3152
  直接跑 sh,它的父进程=3152(就是主脚本)
  管道里那一层组命令,父进程=3163(和主脚本不是一个号)
  管道里 $$ 不会变,仍然是主脚本的号

直接跑一个 sh,它的父进程就是主脚本的号。管道里包一层组命令再跑 sh,父进程变成另一个号,中间多出来的那一层就是子 shell。这个办法在排查「变量为什么没传出去」的时候很好用,一看父进程号就知道有没有多一层。

循环里 break 会怎样

== 9. 循环里 break 能不能提前结束整条管道 ==
  循环外 n=0(管道内的 break 只结束子 shell)

循环里 break 只结束子 shell 里那层循环,主 shell 的计数还是 0。同样的道理,管道里收集的文件列表也带不出来:

== 7. 换一个场景:用管道收集文件名 ==
  管道写法收集到:''
  进程替换写法收集到:' two.txt one.txt'

管道那版收集到的是空串,进程替换那版收到了两个文件名。这一类「循环里攒点东西,循环外用」的写法,全是同一个原因。

退出码也会跟着骗人

== 8. 退出码:管道返回的是最后一段的状态 ==
  (exit 3) | (exit 0) 的 $?=0
  开 pipefail 之后 $?=3

管道的退出码取的是最后一段的状态,前面那段失败不会体现出来。开了 pipefail 之后,任意一段非零就会体现出来。这两件事常一起出现:用管道收集结果、又在管道前面跑了可能失败的命令。

落到写法上的两条

要收集结果,输入用重定向或者进程替换,循环就在当前 shell 里跑,变量和数组都能带出来。必须在管道里处理的话,把结果通过标准输出传出去,交给下一段处理,别指望在循环外读变量。

判断一段脚本属于哪一类,有个很快的办法:在循环体里改一个变量,循环结束立刻打印它。打印出来是初始值,这段循环就在子 shell 里。这次测试跑下来,同样一段逐行计数,重定向版本给的是对的。需要收集结果的循环,我不再写成管道结尾。

    🤞 分享