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

huo*_*sto 69 bash pipe while-loop piping

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

echo first second | read A B ; echo $A-$B 
Run Code Online (Sandbox Code Playgroud)

结果是:

-
Run Code Online (Sandbox Code Playgroud)

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

echo first second | while read A B ; do echo $A-$B ; done
Run Code Online (Sandbox Code Playgroud)

似乎工作,结果是:

first-second
Run Code Online (Sandbox Code Playgroud)

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

F. *_*uri 63

如何对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
Run Code Online (Sandbox Code Playgroud)

不会给出预期的结果!:

  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
Run Code Online (Sandbox Code Playgroud)

计算TOTAL的地方不能在主脚本中重用.

倒叉

通过使用 Process Substitution,Here DocumentsHere Strings,你可以反转fork:

在这里串

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

echo $B
second
Run Code Online (Sandbox Code Playgroud)

这里的文件

while read A B;do
    echo $A-$B
    C=$A-$B
  done << eodoc
first second
third fourth
eodoc
first-second
third-fourth
Run Code Online (Sandbox Code Playgroud)

循环之外:

echo : $C
: third-fourth
Run Code Online (Sandbox Code Playgroud)

这里的命令

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
Run Code Online (Sandbox Code Playgroud)

现在您可以$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
  }
Run Code Online (Sandbox Code Playgroud)

会给:

  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
Run Code Online (Sandbox Code Playgroud)

注意:$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
Run Code Online (Sandbox Code Playgroud)

这将有效,但我(个人)不喜欢这样,因为这不是标准的,也无助于使脚本可读.同样禁用作业控制对于访问此行为似乎很昂贵.

注意: 默认情况下,仅在交互式会话中启用作业控制.因此在普通脚本中不需要.set +m

set +m如果在脚本中运行或在脚本中运行,那么在脚本中忘记会创建不同的行为.这不会使这容易理解或调试......

  • 这里有一个注意事项 - 有一些shell支持为管道的其他组件创建子shell,并使用现有的shell作为右侧.这是bash中的"shopt -s lastpipe"; ksh88和ksh93的行为类似于开箱即用. (2认同)

pbh*_*bhd 21

首先,执行此管道链:

echo first second | read A B
Run Code Online (Sandbox Code Playgroud)

然后

echo $A-$B
Run Code Online (Sandbox Code Playgroud)

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

echo first second | (read A B ; echo $A-$B)
Run Code Online (Sandbox Code Playgroud)

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

  • 顺便说一下,你不需要子壳.你可以简单地使用分组. (3认同)

小智 18

一个更清洁的解决方案......

read -r a b < <(echo "$first $second")
echo "$a $b"
Run Code Online (Sandbox Code Playgroud)

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

  • 这应该真的得到更多的选票! (2认同)