您可以结合使用GNU stdbuf和pee来自moreutils:
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
pee popen(3)是这3个shell命令行,然后fread是输入,然后是fwrite所有这三个,将被缓冲到1M。
这个想法是要有一个至少与输入一样大的缓冲区。这样,即使同时启动三个命令,它们也只会在pee pclose依次执行三个命令时看到输入输入。
对每个pclose,pee将缓冲区刷新到命令并等待其终止。这保证了只要这些cmdx命令在收到任何输入之前就不开始输出任何东西(并且不要派生一个可能在其父级返回后继续输出的过程),那么这三个命令的输出就不会交错。
实际上,这有点像在内存中使用临时文件,但缺点是同时启动3个命令。
为了避免同时启动命令,可以编写pee为shell函数:
pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
但是请注意,除zsh带有NUL字符的二进制输入外,其他外壳将失败。
这样可以避免使用临时文件,但这意味着整个输入都存储在内存中。
无论如何,您都必须将输入存储在内存或临时文件中的某个位置。
实际上,这是一个非常有趣的问题,因为它向我们展示了使几个简单的工具协作完成一项任务的Unix思想的局限性。
在这里,我们希望有几种工具可以配合完成这项任务:
- 源命令(此处echo)
- 调度程序命令(tee)
- 一些过滤器命令(cmd1,cmd2,cmd3)
- 和聚合命令(cat)。
如果他们都可以同时运行,并且对要立即处理的数据进行艰苦的工作,那就太好了。
对于一个过滤器命令,很简单:
src | tee | cmd1 | cat
所有命令同时运行,并在可用时立即cmd1开始对数据进行处理src。
现在,使用三个过滤器命令,我们仍然可以执行相同操作:同时启动它们并将它们与管道连接:
               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
我们可以使用命名管道相对容易地做到这一点:
pee() (
  mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
  { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
  eval "$1 < tee-cmd1 1<> cmd1-cat &"
  eval "$2 < tee-cmd2 1<> cmd2-cat &"
  eval "$3 < tee-cmd3 1<> cmd3-cat &"
  exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
(以上} 3<&0是解决从&重定向的事实,我们通常避免打开要阻塞的管道,直到另一端()也打开为止)stdin/dev/null<>cat
为了避免使用命名管道,使用zshcoproc会更加痛苦:
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    eval "coproc $cmd $ci $co"
    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
现在的问题是:一旦所有程序都启动并连接,数据是否会流动?
我们有两个矛盾:
- tee以相同的速率馈送所有输出,因此它只能以最慢的输出管道的速率发送数据。
- cat只有从第一个管道(5)读取了所有数据后,才会从第二个管道(上图中的管道6)开始读取。
这意味着直到cmd1完成,数据才不会在管道6中流动。并且,与上述情况一样tr b B,这可能意味着数据也不会在管道3中流动,这意味着数据也不会在管道2、3或4中的任何一个中流动,因为tee以所有3的最低速率进行馈送。
实际上,这些管道的大小为非空值,因此一些数据将设法通过,至少在我的系统上,我可以使其正常工作:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
除此之外,
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
我们处于这种情况下的僵局:
               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
我们已经填充了管道3和6(每个管道64kiB)。tee已读取该多余的字节,并将其馈送到cmd1,但
- 现在它正在等待将cmd2其清空,因此已阻止在管道3上进行写入
- cmd2无法清空它,因为它阻塞了在管道6上的写入,正在等待- cat清空它
- cat不能清空它,因为它一直等到管道5上没有更多输入。
- cmd1无法判断- cat没有更多输入,因为它正在等待的更多输入- tee。
- 并且tee无法判断cmd1没有更多输入,因为它已被阻止...依此类推。
我们有一个依赖循环,因此陷入僵局。
现在,有什么解决方案?较大的管道3和4(足以容纳所有src的输出)可以做到。我们能做到这一点,例如通过插入pv -qB 1G之间tee并cmd2/3在那里pv了可以存储到数据等待1G cmd2和cmd3阅读。不过,这将意味着两件事:
- 这可能会占用大量内存,而且将其复制
- 未能使所有3个命令配合使用,因为cmd2实际上只有在cmd1完成后才开始处理数据。
第二个问题的解决方案是将管道6和7也做得更大。假设这样做cmd2并cmd3产生与消耗一样多的输出,那将不会消耗更多的内存。
避免重复数据的唯一方法(在第一个问题中)是实现数据在调度程序本身中的保留,即对tee数据进行变体,以最快的输出速率馈送数据(保留数据以馈送数据)。较慢的按自己的步调)。并非微不足道。
因此,最后,我们无需编程就可以合理地获得最好的结果(Zsh语法):
max_hold=1G
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    if ((n)); then
      eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
    else
      eval "coproc $cmd $ci $co"
    fi
    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c