为不同的命令重用管道数据

为不同的命令重用管道数据

我想对不同的应用程序使用相同的管道,例如:

cat my_file | {
  cmd1
  cmd2
  cmd3
}

Cmd1 应该消耗部分输入。 Cmd2 应该消耗另一部分,依此类推。

然而,每个命令都会消耗更多的输入,然后它读取的内容确实需要适当的缓冲。

例如:

yes | nl | { 
  head -n 10 > /dev/null
  cat 
} | head -n 10

从第 912 行而不是第 11 行输出。

Tee 不是一个好的选择,因为每个命令都应该消耗部分标准输入。

有没有一种简单的方法可以让它发挥作用?

答案1

您可以使用tee许多命令来复制命令来处理整个流:

( ( seq 1 10 | tee /dev/fd/5 | sed s/^/line..\ / >&4 ) 5>&1 | wc -l ) 4>&1 
line.. 1
line.. 2
line.. 3
line.. 4
line.. 5
line.. 6
line.. 7
line.. 8
line.. 9
line.. 10
10

或者使用 bash 逐行分割:

while read line ;do
    echo cmd1 $line
    read line && echo cmd2 $line
    read line && echo cmd3 $line
  done < <(seq 1 10)
cmd1 1
cmd2 2
cmd3 3
cmd1 4
cmd2 5
cmd3 6
cmd1 7
cmd2 8
cmd3 9
cmd1 10

最后有一种运行方式cmd1cmd2并且cmd3只有一次,1/3 的流为标准输入:

( ( ( seq 1 10 |
         tee /dev/fd/5 /dev/fd/6 |
           sed -ne '1{:a;p;N;N;N;s/^.*\n//;ta;}' |
           cmd1 >&4
     ) 5>&1 |
       sed -ne '2{:a;p;N;N;N;s/^.*\n//;ta;}' |
       cmd2 >&4
  ) 6>&1 |
    sed -ne '3{:a;p;N;N;N;s/^.*\n//;ta;}' |
    cmd3 >&4
) 4>&1 
command_1: 1
command_1: 4
command_1: 7
command_1: 10
Command-2: 2
Command-2: 5
Command-2: 8
command 3: 3
command 3: 6
command 3: 9

为了尝试这个,你可以使用:

alias cmd1='sed -e "s/^/command_1: /"' \
    cmd2='sed -e "s/^/Command_2: /"' \
    cmd3='sed -e "s/^/Command_3: /"'

如果在同一脚本上,要在不同进程上使用一个流,您可以这样做:

(
    for ((i=(RANDOM&7);i--;));do
        read line;
        echo CMD1 $line
      done
    for ((i=RANDOM&7;i--;));do
        read line
        echo CMD2 $line
      done
    while read line ;do
        echo CMD3 $line
      done
)
CMD1 1
CMD1 2
CMD1 3
CMD2 4
CMD2 5
CMD2 6
CMD2 7
CMD2 8
CMD2 9
CMD3 10

为此,您可能必须将单独的脚本转换为bash函数能够构建一个整体脚本。

另一种方法可能是确保每个脚本不会输出任何内容标准输出,而不是cat在每个脚本的末尾添加一个他们:

#!/bin/sh

for ((i=1;1<n;i++));do
   read line
   pRoCeSS the $line
   echo >output_log
 done

cat

最终命令可能如下所示:

seq 1 10 | cmd1 | cmd2 | cmd2

答案2

为了head -n 10能够从 stdin 上的管道读取 10 行而不是再读取一个字符,它必须一次读取一个字符,以便在最后一个换行符之后不读取任何内容。那将是低效的。

这就是read当 stdin 不可查找时 shell 内置函数所做的事情。

{
  head -n 10 > /dev/null
  cat
} < myfile

之所以有效,是因为head读取一大块数据并lseek返回到第 10 行末尾之后。这显然不能用管道来完成。

在最近的 GNU 或 FreeBSD 系统上,可以使用或stdio来告诉某些使用的应用程序一次读取一个字符。stdbuf -i1stdbuf -i0

然而,这不适用于 GNU head。不过它可以与 GNU 一起使用sed,所以你可以这样做:

seq 20 | {
  stdbuf -i0 sed -n 10q
  cat
}

或者,您可以做的是控制管道上的内容,以便一次最多只有一根线。

例如,在 Linux 上,您可以执行以下操作:

one_line_at_a_time() {
  perl -MTime::HiRes=usleep -pe '
    BEGIN{$|=1;open F, "<", "/dev/fd/1"; $r=""; vec($r,fileno(F),1) = 1}
    usleep(1000) while select($ro=$r,undef,undef,0)'
}
seq 20 | one_line_at_a_time | { head -n 10 > /dev/null; cat; }

perl脚本以读取模式打开“/dev/fd/1”,这在 Linux 上会导致连接到 fd 1 (stdout) 的管道的另一端被打开。这样,使用select,它可以在发送下一行之前检查管道中是否有东西(并休眠直到它被清空)。

当然,这也是非常低效的。

相关内容