为什么我$x
从下面的代码片段中得到不同的值?
#!/bin/bash
x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x
# x=55 .. I'd expect this result
x=1
cat junk | while read var ; do x=55 ; done
echo x=$x
# x=1 .. but why?
x=1
echo fred | while read var ; do x=55 ; done
echo x=$x
# x=1 .. but why?
答案1
正确的解释已经给出了杰斯比林斯和极客龙,但让我稍微扩展一下。
在大多数 shell(包括 bash)中,管道的每一端都在子 shell 中运行,因此 shell 内部状态的任何更改(例如设置变量)仍然仅限于管道的该段。您可以从子 shell 获得的唯一信息是它输出的内容(到标准输出和其他文件描述符)及其退出代码(0 到 255 之间的数字)。例如,以下代码片段打印 0:
a=0; a=1 | a=2; echo $a
在 ksh(从 AT&T 代码派生的变体,而不是 pdksh/mksh 变体)和 zsh 中,管道中的最后一项在父 shell 中执行。 (POSIX 允许这两种行为。)因此上面的代码片段会打印 2。您可以通过设置选项在现代 bash 中获得此行为lastpipe
(shopt -s lastpipe
在交互式 shell 中,您还需要使用 禁用作业控制set +m
)。
一个有用的习惯用法是在管道中包含 while 循环的延续(或者管道右侧的任何内容,但 while 循环实际上在这里很常见):
cat junk | {
while read var ; do x=55 ; done
echo x=$x
}
答案2
您遇到了变量范围问题。在管道右侧的 while 循环中定义的变量有自己的局部作用域上下文,并且在循环外部不会看到对变量的更改。 while循环本质上是一个子shell,它得到一个复制shell 环境的所有更改都会在 shell 结束时丢失。看到这个StackOverflow问题。
更新:我忽略了一个重要的事实,即 while 循环及其自己的子 shell 是由于它是管道的端点,我在答案中更新了这一点。
答案3
作为其他答案中提到过,管道的各个部分在子 shell 中运行,因此在子 shell 中进行的修改对主 shell 不可见。
如果我们只考虑 Bash,除了结构之外还有另外两种解决方法cmd | { stuff; more stuff; }
:
重定向输入流程替代:
while read var ; do x=55 ; done < <(echo fred) echo "$x"
命令 in 的输出
<(...)
看起来就像是一个命名管道。该
lastpipe
选项使 Bash 像 ksh 一样工作,并在主 shell 进程中运行管道的最后一部分。尽管它仅在禁用作业控制时才有效,即不在交互式 shell 中:bash -c ' shopt -s lastpipe echo fred | while read var ; do x=55 ; done; echo "$x" '
或者
bash -O lastpipe -c ' echo fred | while read var ; do x=55 ; done; echo "$x" '
ksh 和 zsh 当然也支持进程替换。但由于无论如何他们都会在主 shell 中运行管道的最后一部分,因此实际上没有必要将其用作解决方法。