您可以结合使用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
为了避免使用命名管道,使用zsh
coproc会更加痛苦:
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