78

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

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构造中的命令实际上是在同一个 shell 中执行的,echo而不是在子 shell 中?

4

4 回答 4

74

如何对标准输入执行循环并将结果存储在变量中

(以及其他)下,当您通过 管道将某些内容传递给另一个命令时|,您将隐式创建一个fork,这是一个子shell,它是当前会话的子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

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

倒置前叉

通过使用 Process SubstitutionHere DocumentsHere Strings,您可以反转分叉:

这里的字符串

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因此,如果在控制台中运行或在脚本中运行,那么在脚本中被遗忘会产生不同的行为。这不会使这易于理解或调试......

于 2012-12-07T13:25:30.787 回答
28

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

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

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

于 2016-06-17T16:49:58.940 回答
23

首先,执行此管道链:

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)

于 2012-12-07T13:35:42.660 回答
3

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

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

“进入while”情况的差异是一种错觉。同样的规则在那里适用:循环是管道的后半部分,所以它在一个子shell中,但整个循环在同一个子shell中,所以进程的分离不适用。

于 2014-09-25T23:20:13.890 回答