Linux shell bug? 管道中的变量赋值不起作用

3
这个问题的结尾为什么 FILE_FOUND 的值为0?
FILE_FOUND=0

touch /tmp/$$.txt

ls -1 /tmp/$$.* 2>/dev/null | while read item; do
    FILE_FOUND=1
    echo "FILE_FOUND = $FILE_FOUND"
done

echo "FILE_FOUND = $FILE_FOUND"

rm -f /tmp/$$.txt 2>/dev/null

在Unix系统中,FILE_FOUND的值保持为1(应该如此),但在Linux系统(RedHat,Cygwin等)中,它会跳回到0!

这是Linux shell的特性还是一个bug? :)

请帮忙解决。


2
你正在使用哪个Linux shell?有很多种,每种都有自己的“特色”。 - Stephen C
重复问题:变量的全局作用域 - glenn jackman
1
请注意,在管道中不需要使用ls命令的-1选项。 - glenn jackman
回复@pixelbeat回答的评论:要标记接受的答案,请单击您认为正确/最佳的答案下方的“勾号”或“对号”符号。 - Jonathan Leffler
谢谢Jonathan。我点击了那个勾号! - ExpertNoob1
6个回答

5

常见问题是因为您将内容传递到while循环中,因此在子shell中运行,无法将环境变量传回其父级。我猜测在"unix"中您正在运行不同的shell(ksh?)所以会有所不同。

可能不需要将内容传递到while循环中。您可以尝试使用这个习惯用语代替:

for item in /tmp/$$.*; do
    ....
done

如果必须使用子shell,则您需要对进程执行某些外部操作,例如:
touch /tmp/file_found

我不太确定。如果while周围有一个'(' ... ')',那么你肯定是正确的。 - Stephen C
2
这种令人惊讶的行为背后的原因是,当while/for/until循环作为管道的一部分运行时,它会在子Shell中运行。使用重定向或管道与循环时,不同的Shell表现也不同。 - Dennis Williamson
export FILE_FOUND=1 会有帮助吗? - PP.
2
一个 shell 脚本能否设置调用 shell 的环境变量? - pixelbeat

3
这是“bash” shell的一个“特性”。 “bash”手册明确指出:
每个管道中的命令都作为单独的进程(即在子shell中)执行。
用Korn shell(“ksh”)执行相同的结构将在同一进程中运行“while”(而不是在子shell中),因此会给出预期的答案。 因此,我检查了规范。 POSIX shell specification并不十分清楚,但它没有提到更改“shell执行环境”,因此我认为UNIX / Korn shell实现符合规范,而Bourne Again shell实现则不符合。 但是,“bash”并没有声称符合POSIX!

我花了一段时间才发现区别所在。 - Amarghosh
我认为这是问题中的一个错别字。没有“do”,Bash甚至不会执行该文件。 - falstro
@roe - 你说得对。我认为这真的是一个bash“特性”,但我仍在研究中。 - Stephen C
@Stephen C:可能是这样,但在Linux上它不会“跳回0”,因为那行代码永远不会被执行,所以这只是问题中的一个错别字(键盘上o紧挨着0)。 - falstro
@roe - 实际上,它会跳回去,因为变量在子shell中设置并显示(第一次)。这就是上面引用的句子的意思! - Stephen C

2

已经提到过,但由于你正在通过管道输入while循环,在一个子shell中运行整个while循环。我不确定你在“Unix”上使用的是哪种shell,我猜想是Solaris,但是无论平台如何,bash应该保持一致。

为了解决这个问题,你可以做很多事情,最常见的是以某种方式检查while循环的结果,例如:

result=`mycommand 2>/dev/null | while read item; do echo "FILE_FOUND"; done`

另一种常见的方法是使 while 循环产生有效的变量赋值,并直接评估它,从而查找 $result 中的数据。

eval `mycommand | while read item; do echo "FILE_FOUND=1"; done`

这将由您“当前”的shell进行评估,以分配给定的变量。

我假设您不只是想迭代文件,如果是这种情况,您应该执行:

for item in /tmp/$$.*; do
  # whatever you want to do
done

2

正如其他人所提到的,使用管道符号创建了额外的 shell。请尝试以下方式:

while read item; do
    FILE_FOUND=1
    echo "FILE_FOUND = $FILE_FOUND"
done < <(ls -1 /tmp/$$.* 2>/dev/null)

在这个版本中,while 循环在你的脚本 shell 中执行,而 ls 则在一个新的 shell 中执行(与你的脚本相反)。

假设用户的shell理解进程替换(在Linux上很可能) - glenn jackman
你真的试过这个吗?感觉 bash 会将 while 循环放在子 shell 中,以便在此处正确地进行管道操作。 - falstro

0

另一种方式是将结果带回来。

FILE_FOUND=0
result=$(echo "something" | while read item; do
    FILE_FOUND=1
    echo "$FILE_FOUND"

done )
echo "FILE_FOUND outside while = $result"

不行:你需要在大括号内部使用 $FILE_FOUND 的所有引用。 - glenn jackman

-2

嗯...你的脚本里是ls -1而不是l吧?


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