来自 bash 代码
command1 | tee >(command2) | command3
我想捕获command2
in的输出和invar2
的输出。command3
var3
command1
受 I/O 限制,其他命令成本高昂,但可以在command1
完成之前开始工作。
command2
和的输出顺序command3
不固定。所以我尝试在中使用文件描述符
read -r var2 <<< var3=(command1 | tee >(command2 >&3) | command3) 3>&1
或者
{read -u 3 -r var2; read -r var3} <<< command1 | tee >(command2 >&3) | command3
但没有成功。
有没有办法让这三个命令并行运行,将结果存储在不同的变量中而不创建临时文件?
答案1
那么您想将 cmd1 的输出通过管道传输到 cmd2 和 cmd3 中,并将 cmd2 和 cmd3 的输出获取到不同的变量中吗?
那么看来您需要来自 shell 的两个管道,一个连接到 cmd2 的输出,一个连接到 cmd3 的输出,并且 shell 使用select()
/poll()
从这两个管道读取。
bash
不会这样做,你需要一个更高级的 shell,比如zsh
.zsh
没有原始接口pipe()
,但如果在 Linux 上,您可以使用/dev/fd/x
在常规管道上充当命名管道的事实,并使用与以下位置类似的方法使用 shell 重定向读取/写入同一文件描述符
#! /bin/zsh -
cmd1() seq 20
cmd2() sed 's/1/<&>/g'
cmd3() tr 0-9 A-J
zmodload zsh/zselect
zmodload zsh/system
typeset -A done out
{
cmd1 > >(cmd2 >&3 3>&-) > >(cmd3 >&5 5>&-) 3>&- 5>&- &
exec 4< /dev/fd/3 6< /dev/fd/5 3>&- 5>&-
while ((! (done[4] && done[6]))) && zselect -A ready 4 6; do
for fd (${(k)ready[(R)*r*]}) {
sysread -i $fd && out[$fd]+=$REPLY || done[$fd]=1
}
done
} 3> >(:) 5> >(:)
printf '%s output: <%s>\n' cmd2 "$out[4]" cmd3 "$out[6]"
答案2
如果我很好地理解您的所有要求,您可以通过bash
为每个命令创建一个未命名管道,然后将每个命令的输出重定向到其各自的未命名管道,最后将其管道中的每个输出检索到一个单独的变量中来实现这一点。
因此,解决方案可能是这样的:
: {pipe2}<> <(:)
: {pipe3}<> <(:)
command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2=$(while read -ru ${pipe2} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)
var3=$(while read -ru ${pipe3} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)
exec {pipe2}<&- {pipe3}<&-
这里要特别注意的是:
- 该结构的使用
<(:)
;这是一个未记录的 Bash 打开“未命名”管道的技巧 - 使用简单的
echo EOF
方法来通知while
循环不再有输出。这是必要的,因为仅关闭未命名管道(通常会结束任何while read
循环)是没有用的,因为这些管道是双向的,即用于写入和读取。我不知道如何将它们打开(或转换)为通常的几个文件描述符,一个是读取端,另一个是写入端。
在这个例子中,我使用了纯bash方法(除了使用tee
)来更好地阐明使用这些无名管道所需的基本算法,但是您可以用几个代替sed
循环来完成这两个分配while
,与var2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"
变量 2 及其各自的变量 3一样,用相当少的输入产生相同的结果。也就是说,整个事情是:
针对少量数据的精益解决方案
: {pipe2}<> <(:)
: {pipe3}<> <(:)
command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"
var3="$(sed -ne '/^EOF$/q;p' <&${pipe3})"
exec {pipe2}<&- {pipe3}<&-
为了显示目标变量,请记住通过清除 IFS 来禁用分词,如下所示:
IFS=
echo "${var2}"
echo "${var3}"
否则你会在输出时丢失换行符。
上面确实看起来是一个相当干净的解决方案。不幸的是,它只能用于不太高的输出,并且您的里程可能会有所不同:在我的测试中,我在大约 530k 的输出上遇到了问题。如果你在 4k 的限制(非常保守)之内,你应该没问题。
该限制的原因在于这样的事实:两个这样的作业,即命令替换语法,是同步操作,这意味着第二个分配仅在第一个分配完成后运行,而相反,tee
同时提供两个命令,并在任何命令填满其接收缓冲区时阻塞所有命令。陷入僵局。
此问题的解决方案需要稍微复杂的脚本,以便同时清空两个缓冲区。为此,while
在两个管道上循环会派上用场。
适用于任何数据量的更标准的解决方案
更标准的 Bashism 是这样的:
declare -a var2 var3
while read -r line ; do
case "${line}" in
cmd2:*) var2+=("${line#cmd2:}") ;;
cmd3:*) var3+=("${line#cmd3:}") ;;
esac
done < <(
command1 | tee >(command2 | stdbuf -oL sed -re 's/^/cmd2:/') >(command3 | stdbuf -oL sed -re 's/^/cmd3:/') > /dev/null
)
在这里,您将两个命令中的行多路复用到单个标准“stdout”文件描述符上,然后随后将输出合并到每个相应变量上进行多路分解。
特别注意:
- 使用索引数组作为目标变量:这是因为在存在大量输出的情况下,仅附加到普通变量就会变得非常慢
- 使用
sed
命令在每个输出行前面添加字符串“cmd2:”或“cmd3:”(分别),以便脚本了解每行属于哪个变量 - 为命令输出设置行缓冲的必要用途
stdbuf -oL
:这是因为这里的两个命令共享相同的输出文件描述符,因此,如果它们碰巧流式传输,它们很容易在最典型的竞争条件下覆盖彼此的输出同时输出数据;行缓冲输出有助于避免这种情况 - 另请注意,只有每个链的最后一个命令才需要使用 stdbuf,即直接输出到共享文件描述符的命令,在本例中是在每个 commandX 的输出前面添加其区分前缀的 sed 命令
正确显示此类索引数组的一种安全方法如下:
for ((i = 0; i < ${#var2[*]}; i++)) ; do
echo "${var2[$i]}"
done
当然你也可以直接使用"${var2[*]}"
:
echo "${var2[*]}"
但当线路很多时,效率不是很高。
答案3
我发现一些似乎效果很好的东西:
exec 3<> <(:)
var3=$(command1 | tee >(command2 >&3) | command3)
var2=$(while IFS= read -t .01 -r -u 3 line; do printf '%s\n' "$line"; done)
<(:)
它的工作原理是为文件描述符 3设置一个匿名管道并将command2
其输出通过管道传输。捕获文件描述符 3var3
的输出并读取最后一行,直到 0.01 秒内没有收到任何新数据。command3
它仅适用于最多 65536 字节的输出,其中command2
似乎由匿名管道缓冲。
我不喜欢解决方案的最后一行。我宁愿一次性读入所有内容,而不是等待 0.01 秒,而是在缓冲区为空时立即停止。但我不知道有什么更好的办法。
答案4
从 4.0 开始,这是可以实现的。 bash 添加了一个 shell 保留字 coproc。它分叉作为子进程跟随到后台的命令(通常不允许传递变量。),但是它创建一个数组(默认为 COPROC 但可以命名)。 ${COPROC[0]} 连接到子进程${COPROC的标准输入1这些进程可以作为作业进行操作,是异步的,因此您可以使用 tee 通过管道连接到两个协进程,并将它们输出到一个单独的文件,然后通过调用“$ {协程第一1} ${协程第二个1}”这很棒,因为它甚至不需要位于管道内。
bash coproc command_1 { command1 arg1 arg2 arg3 >> command_1_output.txt } coproc command_2 { command2 arg1 arg2 arg3 >> command_2_output.txt } othercommand |三通 >^${command_11}" >&"${command_21}" 读取 -r results1 <&"${command_1[0]" 读取 -r resluts2 <&"${command_2[0]" echo "$result1 $result2" | command3 >>合并结果.txt
正如前面提到的,这个解决方案目前还不完整,所以我很震惊,但原理是合理的,并且与上面的答案类似。不过,我将引导您阅读有关该主题的一些好文章,并在工作允许时返回此答案。