如何在不创建子shell的情况下将命令输出存储到变量中 [Bash <v4]

22

ksh 有一个非常有趣的构造来实现这一点,在此答案中详细说明:https://dev59.com/NGgu5IYBdhLWcg3w2alm#11172617

自从Bash 4.0以来,就有了一个内置的 mapfile 命令,可以解决这个问题:http://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html

但奇怪的是,它似乎不能与进程替换一起使用:

foo () { echo ${BASH_SUBSHELL}; }
mapfile -t foo_output <(foo) # FAIL: hang forever here
subshell_depth=${foo_output[0]} # should be 0

但是在Bash v3.2中如何实现这一点?

1
出于好奇,你会如何使用 mapfile?据我所知,在任何版本中都没有 bash 等效的命令。 - chepner
mapfile -t foo_output <(foo) - 这是一个进程替换 - foo 在一个全新的进程中运行,我认为这不是你想要的。请参阅我的答案附录。 - Digital Trauma
1
我不熟悉 mapfile,但是它似乎读取标准输入而不是文件,因此您可能需要编写 < <(cmd) 而不是仅使用 <(cmd),因为 <(cmd) 最终会被替换为像 /dev/fd/63 这样的内容(可以尝试 echo <(echo) 进行检查)。至少这种方式似乎不会挂起。 - Alice M.
4个回答

18

以下是另一种完成此任务的方法,与之前介绍的方式不同并且足够特别,需要单独回答。我认为这个方法不使用子shell和bash子进程:

ubuntu@ubuntu:~$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
ubuntu@ubuntu:~$ bar
0 8215
ubuntu@ubuntu:~$ mkfifo /tmp/myfifo
ubuntu@ubuntu:~$ exec 3<> /tmp/myfifo
ubuntu@ubuntu:~$ unlink /tmp/myfifo
ubuntu@ubuntu:~$ bar 1>&3
ubuntu@ubuntu:~$ read -u3 a
ubuntu@ubuntu:~$ echo $a
0 8215
ubuntu@ubuntu:~$ exec 3>&-
ubuntu@ubuntu:~$

这里的技巧是使用exec以FD读写模式打开FIFO,这似乎会使FIFO变为非阻塞状态。然后你可以将你的命令重定向到FD而不会被阻塞,然后读取FD。

请注意,FIFO将是有限大小的缓冲区,可能大约为4K,因此如果您的命令产生的输出超过此大小,它将再次被阻塞。


4
哇,这个虚拟文件描述符/先进先出技巧太棒了! 是的,看起来它确实很有效。你甚至可以定义一个“分配”函数:assign () { local var=$1; shift; "$@" > /tmp/myfifo; read ${var} < /tmp/myfifo; }。我测试过了,它可以工作:foo () { local b; assign b bar; echo $b; } - Lucas Cimon
请注意,它仅适用于单行输出,并且如果/tmp挂载在内存上,则可能比子 shell 更好,但如果没有挂载,则肯定更差,因为它会创建磁盘访问。 - EvgenKo423

5
这个问题在查找如何将任何“打印”命令的输出捕获到变量中时经常出现。因此,对于任何想要查找的人来说,这是可能的(自bash v3.1.0以来): printf -v VARIABLE_NAME "您需要的内容:%s" $ID 如果你为了速度而调整你的脚本,那么你可以使用在函数末尾设置某些全局变量的模式,而不仅仅是“echoing”它-请小心使用,有时会被批评为导致难以维护的代码。

确实,这不是对问题的回答,但很好知道。请注意,这适用于bash内置函数。GNU coreutils printf(我有8.3)不提供“-v”标志。 type -a printf以查看可用的printf。 builtin printf -v VARTOSET“put me in the var”显式调用内置函数。 - Jack Wasey

4

以下是我能想到的 - 它有点凌乱,但是 foo 在顶层 shell 上下文中运行,并且其输出在顶层 shell 上下文中提供的变量 a 中:

#!/bin/bash

foo () { echo ${BASH_SUBSHELL}; }

mkfifo /tmp/fifo{1,2}
{
    # block, then read everything in fifo1 into the buffer array
    i=0
    while IFS='' read -r ln; do
        buf[$((i++))]="$ln"
    done < /tmp/fifo1
    # then write everything in the buffer array to fifo2
    for i in ${!buf[@]}; do
        printf "%s\n" "${buf[$i]}"
    done > /tmp/fifo2
} &

foo > /tmp/fifo1
read a < /tmp/fifo2
echo $a

rm /tmp/fifo{1,2}

当然,这假设两件事:

  • 允许使用FIFO
  • 进行缓冲的命令组可以被放到后台

我测试过在这些 版本中可行:

  • 3.00.15(1)-release (x86_64-redhat-linux-gnu)
  • 3.2.48(1)-release (x86_64-apple-darwin12)
  • 4.2.25(1)-release (x86_64-pc-linux-gnu)

附录

我不确定在bash 4.x中的mapfile方法是否能够满足您的要求,因为进程替换<()会创建一个全新的bash进程(虽然不是在该bash进程内部创建一个bash子shell):

$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
$ bar
0 2636
$ mapfile -t bar_output < <(bar)
$ echo ${bar_output[0]}
0 60780
$ 

因此,尽管$BASH_SUBSHELL在这里为0,但这是因为它位于进程替换中新的shell进程60780的顶层。


非常有趣的答案,谢谢!我考虑过使用 /dev/shm,但命名管道是一个非常好的想法。不过还有几个问题:1)buf 是从哪里来的?2)为什么要在第一个循环中递增 i?另外,你对 mapfile 的解释非常完美! - Lucas Cimon
@LucasCimon - buf 只是我用来缓冲 fifo1 和 fifo2 之间流的 bash 数组名称。 - Digital Trauma
从 Shell 的角度来看,<(cmd) 看起来像一个文件。因此,在将重定向输入到 mapfile 时,您需要多加一个 <。这就是为令 mapfile 阻塞的原因 - 它实际上在 stdin 上阻塞。键入 ^D 即可取消阻塞。 - Digital Trauma
哦,我不知道数组不需要声明。 不过,你还需要一个后台进程持续运行。那么完全没有进程/子shell怎么办? - Lucas Cimon
索引数组不需要显式声明。关联数组需要使用 declare -A array。完全没有使用 fork 绝对会更具挑战性。 - Digital Trauma
@LucasCimon i 只是缓冲区中的索引。它被递增,以便我们可以将下一行写入缓冲区中的下一个数组元素。我想我本可以只将行附加到长字符串中,但数组方法似乎更清晰。 - Digital Trauma

2
最简单的方法是放弃函数并直接传递变量,例如:
declare -a foo_output
mapfile -t foo_output <<<${BASH_SUBSHELL}
subshell_depth=${foo_output[0]} # Should be zero.

否则,给定函数中的两个项:
foo () { echo "$BASH_SUBSHELL $BASHPID"; }

您可以使用以下任一命令之一(根据需要修改IFS),使用read指令:

cat < <(foo) | read subshell_depth pid # Two variables.
read -r subshell_depth pid < <(foo) # Two separate variables.
read -a -r foo_arr < <(foo) # One array.

或者使用readarray/mapfile(Bash >4):

mapfile -t foo_output < <(foo)
readarray -t foo_output < <(foo)

然后将输出转换回数组:
foo_arr=($foo_output)
subshell_depth=${foo_arr[0]} # should be 0

非常好的答案。我不明白为什么这个答案没有被接受。这个解决方案只适用于版本4吗?谢谢,@kenorb! - SomeStupid
3
进程替换 < <(cmd) 创建了一个子shell...它是一个特定的断开连接的子shell,但尽管如此..这可能就是为什么它不被“接受”的原因。 - Brian Chrisman

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