为什么 "echo foo | read a ; echo $a" 的效果与预期不符?

8

我在FreeBSD、GNU/Linux和Solaris下使用不同的shell都遇到了这个问题。这让我头疼了一个多小时,所以我决定在这里发布这个问题。


如果'foo'是一个常量,那么a=foo是最好的选择。所以,这不是你真正的问题;它只是你真正问题的过度简化版本。 - Jonathan Leffler
这个是按预期运行的——你只是期望了错误的事情 ;) - user3850
8个回答

15

由于管道的存在,read 命令在它自己的子 shell 中被执行。

echo foo | while read a; do echo $a; done

它将会按照你期望的方式运行。


1
你知道吗,我曾经遇到过完全相同的问题,也想到了完全相同的解决方案(只执行一次的while循环),但从未意识到它为什么有效。谢谢。 - Paul Tomblin
我仍然无法使用它,因为 $a 在 while 块之外失去了其值! echo foo | while read a; do echo $a; done ; echo $a - Diomidis Spinellis
1
你应该告诉我们更多关于你的问题。你最初的问题只是"为什么这个不工作"(已经得到回答),但如果我们更了解你的问题,我们可能可以更好地帮助你。 - Bombe
echo foo | { read a; echo $a; } 不使用 while 也可以完成相同的操作。 - user3850
这些都会将“foo”分配给子shell中的变量,但提问者希望在父shell中分配一个变量。 - cifer
显示剩余3条评论

7

备选方案:

echo foo | (read a ; echo $a)

编辑:

如果您需要在子shell之外使用$a,则必须反转命令:

read a < <(echo foo); echo $a

这样读操作将在当前进程中执行。


然而,这不允许我在子shell之外使用该值。 - Diomidis Spinellis
1
不错,但请注意<()不是POSIX语法。 - daf
读取 <<<"foo"; echo $a 也可以工作,并且不像 <() 那样产生子shell和命名管道。 - Asfand Qazi

3

我不认为这个已经被提供过:

a=`echo foo`
echo $a

当然,如果您想要读取多个值,您必须按照Bombe的建议去做。 - Arthur Reutenauer

3

只是提供信息;在ksh中,它按预期工作;另请参阅http://kornshell.com/doc/faq.html,第III节(shell编程问题),Q13:

Q13.    What is $bar after, echo foo | read bar?
A13.    The is foo.  ksh runs the last component of a pipeline
        in the current process.  Some shells run it as a subshell
        as if you had invoked it as  echo foo | (read bar).

如果在bash中设置了lastpipe选项(使用shopt -s lastpipe),它将类似地在当前shell而不是子shell中运行管道的最后一个命令。 - Chris Dodd

1
| 是一种进程间通信操作符。因此,隐式地,必须创建一个子进程来解析和评估其一侧或另一侧的表达式。Bash 和旧的 Bourne shell 在运算符的右侧(从管道中读取)创建一个子进程,这意味着在那里设置的任何变量仅在该进程退出时(即在此代码示例中的分号处)才存在于范围内。
zsh 和较新版本的 Korn shell(至少从 '93 开始,但可能甚至可以追溯到 ksh '88)会在管道的另一侧(写入其中)创建子进程。因此,它们将按照问题作者的意图工作。当然,“预期”显然是高度主观的。了解管道的性质应该使人们预计其行为以实现特定的方式。
我不知道 Posix、Spec 1170 或 SuS 或任何其他已发布的标准中是否有任何具体规定要求一种或另一种语义。但是,在实践中,很明显您不能依赖于这种行为;尽管您可以轻松地在脚本中进行测试。

POSIX不规定管道的哪一侧由主shell处理。因此,这是未定义的行为。来自http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_12 此外,多命令管道中的每个命令都在子shell环境中执行;作为扩展,任何或所有命令都可以在当前环境中执行。所有其他命令将在当前shell环境中执行。 - jlliagre

0

我想出了一种解决方案,它不会在子shell中隐藏变量值,并且可以处理多个值。

set `echo foo bar`
A=$1
B=$2

2
如果你要使用“set”,请始终使用“set --”以确保“-foo”不会让你出问题。 - Jonathan Leffler

-1
根据Kernighan和Pike的《Unix编程环境》(第159页)所述,“shell内置命令中没有一个(与控制流原语,如for不同)可以使用>和<进行重定向”,“这可能被描述为shell中的一个错误”。这似乎解释了a)为什么像以下代码这样的代码:
ls *.c |
while read file
do
   echo $a
done

通常情况下没有问题,但是从文件重定向时不一致。


不,它没有解释清楚。你的问题只是一个作用域问题。 - user3850
你可以解释一下答案中涉及的作用域吗? - Diomidis Spinellis
在Bash中,管道符号会导致以下命令在子shell中执行。使用read设置的环境变量会在子shell退出时(即在read和echo之间的分号处)被销毁,因此不可用于下一个命令。 - user3850
这是一个很好的解释,如果改为“管道导致以下内置命令在子shell中执行”。(作为管道一部分的外部命令绝对不会在子shell中运行。) - Diomidis Spinellis
1
你说得没错,非内置命令可以改变环境。关于man页面也是对的,我不知道这一点,本以为管道中的命令是直接从父shell生成的。 - Diomidis Spinellis
显示剩余2条评论

-2

read 命令期望在新行上输入

while read a; do echo $a; done
foo
bar
^D

大致符合您的要求


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接