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


25

为什么我$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?

关于堆栈溢出的类似文章:在while循环内修改的变量不记得了
codeforester

Answers:


26

jsbillingsgeekosaur已经给出了正确的解释,但让我对此进行扩展。

在大多数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。

一个有用的习惯用法是在管道中包括while循环的延续(或管道右侧的任何内容,但while循环实际上在这里很常见):

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}

1
谢谢吉尔..那a = 0; a = 1 | a = 2给出了非常清晰的图景。不仅是内部状态的本地化,而且管道实际上不需要通过管道发送任何内容(除了退出代码(?)之外。)是对管道的有趣< <(locate -ber ^\.tag$)理解。由于最初的答案还不太清楚,还有geekosaur和glenn jackman的喜剧片,我确实设法使脚本运行了。非常清楚,尤其是对jsbillings的后续评论:)
Peter.O 2011年

感觉就像我通过管道传递到函数中一样,所以我将一些变量和测试移到了函数内部,效果很好,谢谢!
Aquarius

8

您遇到了可变范围问题。在管道右侧的while循环中定义的变量具有其自己的局部范围上下文,并且对变量的更改将在循环外部看不到。while循环本质上是一个子shell,它获得shell环境的COPY,并且对环境的任何更改都会在shell的末尾丢失。请参阅此StackOverflow问题

更新:我忽略了一个重要的事实,即while循环具有其自己的子外壳是由于它是管道的端点,因此我在回答中对此进行了更新。


@jsbillings ..好的,这解释了最后两个片段,但没有解释第一个片段,其中循环中设置的$ x的值继续为55(超出“ while”循环的范围)
Peter.O 2011年

5
@ fred.bear:它正在while循环中运行,并将其作为将其放入子shell的管道的末端。
geekosaur 2011年

2
这是bash进程替代起作用的地方。相反blah|blah|while read ...,您可以拥有while read ...; done < <(blah|blah)
glenn jackman 2011年

1
@geekosaur:感谢您填写我在回答中忽略的细节。
jsbillings 2011年

1
-1对不起,但是这个答案是错误的。它解释了这些东西如何在许多编程语言中工作,而不是在Shell中工作。下面的@Gilles正确了。
2011年

6

其他答案所述,管道的各部分在子外壳中运行,因此对主外壳不可见所做的修改。

如果仅考虑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也支持进程替换。但是,由于它们仍然在主外壳中运行管道的最后一部分,因此实际上并不需要使用它作为解决方法。


0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

它可以工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.