针对少量数据的精益解决方案

针对少量数据的精益解决方案

来自 bash 代码

command1 | tee >(command2) | command3

我想捕获command2in的输出和invar2的输出。command3var3

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

正如前面提到的,这个解决方案目前还不完整,所以我很震惊,但原理是合理的,并且与上面的答案类似。不过,我将引导您阅读有关该主题的一些好文章,并在工作允许时返回此答案。

在协进程中设置变量的示例

深入了解协进程和命名管道

用途和陷阱

相关内容