为什么管道输入“读取”仅在输入“while read ...”构造时有效?

时间:2012-12-07 13:20:49

标签: bash while-loop pipe piping

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

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

结果是:

-

A和B都是空的。我读到了bash在子shell中执行管道命令,并且基本上阻止了管道输入读取。但是,以下内容:

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

似乎工作,结果是:

first-second

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

4 个答案:

答案 0 :(得分:61)

如何针对 stdin 进行循环并将结果存储在变量

(以及其他下),当您使用|管道输入其他命令时,您将隐含地创建 fork ,子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

如果计算 TOTAL 无法在主脚本中重复使用。

反转叉

通过使用 进程替换此处文档此处字符串,您可以反转fork:

这里是字符串

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 ,你可以在 fork 中创建一种脚本:

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无法在主脚本中使用(在右上方的大括号}之后)。

使用 lastpipe 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会产生不同的行为。这不会使这很容易理解或调试......

答案 1 :(得分:19)

首先,执行此管道链接:

echo first second | read A B

然后

echo $A-$B

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

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

然后read A Becho $A-$B都在相同的子shell中执行(请参阅bash的联机帮助页,搜索(list)

答案 2 :(得分:15)

更清洁的解决方法......

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

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

答案 3 :(得分:1)

你看到的是进程之间的分离:read出现在子shell中 - 一个单独的进程,它不能改变主进程中的变量(以后会发生echo命令)。

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

&#34;管道进入的情况的差异&#34;是一种幻觉。同样的规则也适用于:循环是管道的后半部分,所以它在子shell中,但整个循环在相同的子shell中,因此分离过程不适用。