bash:如何从管道中分配变量?

7
在bash中,使用管道输入进行变量赋值的最有效方法是什么 - 只使用从左到右的语法? 假设管道的左侧是“seq 3”,因此我们需要:
seq 3 | x=<put some code here>

注意:这不是一个答案,尽管可能在功能上与之等效:

x=`seq 3`

由于seq 3不在管道的左侧,因此会出现错误。

对于这个问题,请忽略变量内存超限的可能性,尽管管道确实可能会导致这种情况。


1
哪个版本的Bash?在大多数情况下,这是不可能的,因为管道的右侧在子进程中运行,并且在管道完成时退出。请参见[BashFAQ#24](http://mywiki.wooledge.org/BashFAQ/024)。 - Charles Duffy
1
即使在支持此功能的bash版本中,它也是不可靠的,这取决于lastpipe特性,该特性仅在作业控制处于非活动状态时才可用。 - Charles Duffy
为什么不能使用 x=\seq 3`(或者x=$(seq 3)`) 这样的语句? - Ludonope
1
@Ludonope,...因为OP正在询问一个人为制造的问题的例子? :) - Charles Duffy
2
顺便说一下,这与https://dev59.com/s3E85IYBdhLWcg3wgDpI非常相关。 - Charles Duffy
@CharlesDuffy 是的,但实际上并没有什么区别,因为 x 只是会获取前一个命令的输出。 - Ludonope
6个回答

18

为了补充 Charles Duffy 的有用回答,并聚焦于在 bash使其实际运行

默认情况下,在 Bash v4.1- 中任何多段管道中的变量创建/修改都发生在 子shell,因此结果对调用 shell 不可见。

Bash v4.2+ 中,你可以设置选项lastpipe以使最后一个管道段在当前 Shell 中运行,从而在其中进行的变量创建/修改可见的。

为了在交互式shell中使其工作,你必须另外关闭作业控制set +m

以下是一个完整的示例(Bash v4.2+):

$ unset x; shopt -s lastpipe; set +m; seq 3 | x=$(cat); echo "$x"
1
2
3

话虽如此,

x=$(seq 3)

(现代等同于你的x = `seq 3`)更加简单-它符合POSIX标准,因此也适用于较旧版本的Bash,并且不需要调整全局选项。


不是最可移植的答案,但对于bash来说是最高效的。 - agc

14

这在BashFAQ#24中有详细介绍。

仅当引用该变量的代码也位于管道的右��时,您才可以可靠地使用在管道右侧收集的变量。

#!/bin/bash
echo "hello" | { read -r var; echo "Read value: $var"; }
echo "After the pipeline exited, var contains: $var"

典型的输出是:

Read value: hello
After the pipeline exited, var contains:

POSIX sh标准既不要求也不排除在同一个shell中执行管道的右侧,该shell后续执行其他命令。因此,一个shell可能会在同一个shell中执行第二行的read命令,以及第三行的echo命令 - 但是特别是bash不会这样做,除非启用了lastpipe shell选项。


4

管道输入

设置变量最简单的方法是通过read命令:

seq 3 | read -d '' x

这也适用于 zsh,但不适用于 kshksh 中的 -d 选项不允许使用 NUL)。

使用 x 的值的一种方法是在由管道创建的子shell中执行:

$ seq 3 | { read -d '' x; echo "$x"; }
1
2
3

请注意,read 命令的退出条件是失败(因为未找到“”字符)。更多详细信息可在Bash FAQ 24中找到。
在 ksh 和 zsh 中,该值可以在管道结束后使用(使用不在输入中的字符(但不是 NUL)使 ksh 也能正常工作)。
$ seq 3 | read -d ':' x
$ echo "$x"
1
2
3

那是因为在ksh (后来是zsh)中,管道上的最后一个命令会在与父Shell相同的进程中运行,保留了管道结束后变量的值。在bash中模仿这种功能的一种方法是使用lastpipe(仅在禁用作业控制(在非交互式脚本中或使用set +m时)时才起作用):
$ set +m; shopt -s lastpipe
$ seq 3 | read -d '' x
$ echo "$x"
1
2
3

捕获命令输出

使用 进程替换

$ read -d '' x < <(seq 3)
$ echo "$x"
1
2
3

或者,使用旧的 here-doc:
$ read -d '' x <<-Hello
> $(seq 3)
> Hello
$ echo "$x"
1
2
3

或者,使用printf:

$ printf -v x "$(seq 3)"
$ echo "$x"
1
2
3

1
@agc 请阅读2.2. 读取以NUL分隔的流以及其中的其他几个示例。 - user8017719
1
zsh 可以直接使用它而不需要创建子 shell。 - Fırat Küçük
拥有在父 shell 中运行管道中最后一个命令的第一个 shell 是 ksh。它保留了 x 的值。这个概念后来被带到了 zsh 和(设置一些选项)到了 bash。详细信息已添加到答案中。 - user8017719

1
如果你对“管道输入”和“从左到右语法”有一定的灵活性,你可以做到这一点。尝试以下操作:
< <(seq 3) read -r -d '' var

我完全不建议这样做。只需使用var=$(seq 3)


1
个人而言,我会把 seq 3 && printf '\0' 放在进程替换符号的内部——这样当 seq 执行成功时,read 就会返回 true。以当前的代码写法,由于缺少尾部分隔符,它将返回 false,从而触发 set -e、 ERR trap 等。 - Charles Duffy

1

一种(修改后的)方法,可能不是最好的,而且根据我们的看法可能无法满足OP的标准:

# load $x, echo it quoted, then unquoted.
seq 3 | { x=$(</dev/stdin)
         echo "$x"; echo $x; } 

输出:

1
2
3
1 2 3

这个cat变体适用于POSIX shell (yash, dash):

seq 3 | { n=$(cat /dev/stdin)
          echo "$n"; echo $n; }

0

您可以使用临时文件:

seq 3 >/var/tmp/agc-bashvar1; x=$(cat /var/tmp/agc-bashvar1 ); echo x is $x

以下是使用后的清理:

seq 3 >/var/tmp/agc-bashvar1; x=$(cat /var/tmp/agc-bashvar1 ); rm /var/tmp/agc-bashvar1

然后echo x is $x

原理:根据@charles-duffy的答案,您不能在管道的右侧直接接收内容来存储变量。因此,为什么不使用外部位置将其作为副作用存储(而不是无副作用的管道)。

确保临时文件名未被其他程序使用。


问题非常具体,需要涉及*管道(pipe)*。试想一种情况,即程序没有文件写入权限,或者存储资源较少,过于昂贵,或者比RAM不可靠得多。 - agc
1
我的答案可能会帮助到其他人。它符合上述要求:从左管道开始。否则,无法将变量从左侧传播到右侧。我的答案也比被接受的答案更具可移植性,后者基本上是一种hack(在MacOS上不起作用)。在实际情况下(例如安装等),使用临时文件可能更好并且更符合POSIX标准。这是一个合法的答案,不应该得到负面评价。 - Sohail Si
已经有可移植的答案,它们使用管道,例如:seq 3 | { n=$(cat /dev/stdin) ; echo "$n" ; echo $n ; } - agc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接