关于bash:为什么管道输入“读取”仅在输入“while read …”构造时有效?

Why piping input to “read” only works when fed into “while read …” construct?

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

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

结果是:

1
-

A和B都是空的。我读到了关于bash在子shell中执行管道命令的内容,这基本上阻止了一个人通过管道输入来读取。但是,以下内容:

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

似乎有效,结果是:

1
first-second

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


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

在bash(以及其他shell)下,当您使用|将某个内容发送到另一个命令时,您将隐式地创建一个fork,一个子shell,它是当前会话的子级,并且不能影响当前会话的环境。

因此:

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

不会给出预期的结果!:

1
2
3
4
5
6
7
  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

其中计算的汇总不能在主脚本中重用。

倒转叉子

通过使用bash进程替换、这里的文档或这里的字符串,您可以反转fork:

这里的字符串

1
2
3
4
5
6
read A B <<<"first second"
echo $A
first

echo $B
second

这里的文件

1
2
3
4
5
6
7
8
9
while read A B;do
    echo $A-$B
    C=$A-$B
  done << eodoc
first second
third fourth
eodoc

first-second
third-fourth

循环外:

1
2
echo : $C
: third-fourth

这里命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TOTAL=0
while read A B;do
    ((TOTAL+=A-B))
    printf"%3d - %3d = %4d -> TOTAL= %4d
"
$A $B $[A-B] $TOTAL
  done < <(
    printf"%s %s
"
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中创建一种脚本:

1
2
3
4
5
6
7
8
9
10
printf"%s %s
"
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
"
$A $B $[A-B] $TOTAL
    done
    echo"Out of the loop total:" $TOTAL
  }

将给予:

1
2
3
4
5
6
  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选项用来改变这种行为。但为此,我们必须首先禁用作业控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shopt -s lastpipe           # Set *lastpipe* option
set +m                      # Disabling job control
TOTAL=0
printf"%s %s
"
9 4 3 1 77 2 25 12 226 664 |
  while read A B;do
      ((TOTAL+=A-B))
      printf"%3d - %3d = %4d -> TOTAL= %4d
"
$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
echo first second | read A B

然后

1
echo $A-$B

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

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

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


更清洁的工作…

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

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


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

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

不同的情况下"管道进入时",是一个错觉。同样的规则在这里也适用:循环是管道的后半部分,所以它在子shell中,但是整个循环都在同一子shell中,所以流程分离不适用。