为什么管道输入到“ read”只有在输入到“ while read...”结构时才能工作?

我一直试图从程序输出中读入环境变量的输入,如下所示:

echo first second | read A B ; echo $A-$B

结果就是:

-

A 和 B 总是空的。我了解了 bash 在 sub-shell 中执行管道输入命令的情况,这基本上阻止了通过管道输入进行读取。不过,以下事项:

echo first second | while read A B ; do echo $A-$B ; done

似乎奏效了,结果是:

first-second

有人能解释一下这里的逻辑吗?是不是 while... done结构中的命令实际上是在与 echo相同的 shell 中执行的,而不是在子 shell 中?

37369 次浏览

如何做一个循环对 Stdin和得到结果存储在一个变量

(以及其他 )下,当您通过 |将某些内容传送到另一个命令时,您将隐式创建一个 叉子,这是一个子 shell,是当前会话的子级。子 shell 不能影响当前会话的环境。

所以这个:

TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done
echo final total: $TOTAL

不会给出预期的结果! :

  9 -   4 =    5 -> TOTAL=    5
3 -   1 =    2 -> TOTAL=    7
77 -   2 =   75 -> TOTAL=   82
25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
echo final total: $TOTAL
final total: 0

计算出的 总计无法在主脚本中重用。

把叉子倒过来

通过使用 程序替代此处文件这里,绳子,你可以反转叉:

这里有绳子

read A B <<<"first second"
echo $A
first


echo $B
second

此处文件

while read A B;do
echo $A-$B
C=$A-$B
done << eodoc
first second
third fourth
eodoc
first-second
third-fourth

圈外人士:

echo : $C
: third-fourth

这里命令

TOTAL=0
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done < <(
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664
)
9 -   4 =    5 -> TOTAL=    5
3 -   1 =    2 -> TOTAL=    7
77 -   2 =   75 -> TOTAL=   82
25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343


# and finally out of loop:
echo $TOTAL
-343

现在你可以在你的 主要剧本中使用 $TOTAL

通向 命令列表的管道

但是如果只针对 Stdin工作,你可以在 叉子中创建一种脚本:

printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | {
TOTAL=0
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done
echo "Out of the loop total:" $TOTAL
}

将给予:

  9 -   4 =    5 -> TOTAL=    5
3 -   1 =    2 -> TOTAL=    7
77 -   2 =   75 -> TOTAL=   82
25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
Out of the loop total: -343

注: $TOTAL不能在 主要剧本中使用(在最后一个右花括号 }之后)。

使用 最后一根管子 bash 选项

正如@CharlesDuffy 正确指出的那样,有一个 bash 选项用于改变这种行为。但是对于这个,我们必须首先 关闭 工作控制:

shopt -s lastpipe           # Set *lastpipe* option
set +m                      # Disabling job control
TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
while read A B;do
((TOTAL+=A-B))
printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
done


9 -   4 =    5 -> TOTAL= -338
3 -   1 =    2 -> TOTAL= -336
77 -   2 =   75 -> TOTAL= -261
25 -  12 =   13 -> TOTAL= -248
226 - 664 = -438 -> TOTAL= -686


echo final total: $TOTAL
-343

这将工作,但我(个人)不喜欢这一点,因为这不是 标准,不会帮助使脚本可读。禁用作业控制对于访问这种行为似乎也很昂贵。

注意: 工作控制在默认情况下只在 互动会话中启用,因此在普通脚本中不需要 set +m

因此,如果在控制台中运行或者在脚本中运行,脚本中遗忘的 set +m将创建不同的行为。这将不会使这个容易理解或调试..。

首先,执行这个管道链:

echo first second | read A B

那么

echo $A-$B

因为 read A B是在子 shell 中执行的,所以 A 和 B 会丢失。 如果你这样做:

echo first second | (read A B ; echo $A-$B)

然后在同一个子 shell 中执行 read A Becho $A-$B(参见 bash 的手册,搜索 (list))

您所看到的是进程之间的分离: read发生在一个子 shell 中——一个不能改变主进程中的变量的单独进程(echo命令稍后将在其中发生)。

管道(如 A | B)隐式地将每个组件放在一个子 shell (一个单独的进程)中,即使是对于通常在 shell 上下文中(在同一进程中)运行的内置组件(如 read)也是如此。

“ piping into while”的区别是一种错觉。这里也适用同样的规则: 循环是管道的后半部分,所以它在子 shell 中,但是 完整循环在 一样子 shell 中,所以不适用进程分离。

一个更干净的解决办法。

first="firstvalue"
second="secondvalue"
read -r a b < <(echo "$first $second")
echo "$a $b"

这样,read 就不会在子 shell 中执行(子 shell 一结束,就会清除变量)。相反,您要使用的变量在一个子 shell 中回显,该子 shell 会自动从父 shell 继承变量。