为什么我的变量在一个“while read”循环中是本地变量,但在另一个看似相似的循环中却不是?

为什么我的变量在一个“while read”循环中是本地变量,但在另一个看似相似的循环中却不是?

为什么我$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 中获得此行为lastpipeshopt -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; }

  1. 重定向输入流程替代:

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"
    

    命令 in 的输出<(...)看起来就像是一个命名管道。

  2. 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 中运行管道的最后一部分,因此实际上没有必要将其用作解决方法。

相关内容