Você pode usar uma combinação do GNU stdbuf e peedo moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
fazer xixi popen(3)essas 3 linhas de comando do shell e depois freada entrada efwrite nas três, que serão armazenadas em buffer até 1M.
A idéia é ter um buffer pelo menos tão grande quanto a entrada. Dessa maneira, mesmo que os três comandos sejam iniciados ao mesmo tempo, eles verão apenas a entrada de entrada quando os três comandos forem pee pcloseseqüenciais.
Em cada um pclose, peelibera o buffer para o comando e aguarda sua finalização. Isso garante que, enquanto aquelescmdx comandos não comecem a produzir nada antes de receberem qualquer entrada (e não bifurcem um processo que possa continuar produzindo após o retorno do pai), a saída dos três comandos não será intercalado.
Na verdade, é como usar um arquivo temporário na memória, com a desvantagem de os três comandos serem iniciados simultaneamente.
Para evitar iniciar os comandos simultaneamente, você pode escrever peecomo uma função shell:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Mas tome cuidado para que outras conchas zshfalhem na entrada binária com caracteres NUL.
Isso evita o uso de arquivos temporários, mas isso significa que toda a entrada é armazenada na memória.
De qualquer forma, você precisará armazenar a entrada em algum lugar, na memória ou em um arquivo temporário.
Na verdade, é uma pergunta bastante interessante, pois mostra o limite da ideia do Unix de ter várias ferramentas simples cooperando para uma única tarefa.
Aqui, gostaríamos de ter várias ferramentas para cooperar com a tarefa:
- um comando de origem (aqui
echo )
- um comando do dispatcher (
tee )
- alguns comandos de filtro (
cmd1, cmd2,cmd3 )
- e um comando de agregação (
cat).
Seria bom se todos pudessem rodar juntos ao mesmo tempo e trabalhar duro com os dados que devem processar assim que estiverem disponíveis.
No caso de um comando de filtro, é fácil:
src | tee | cmd1 | cat
Todos os comandos são executados simultaneamente, cmd1começa a coletar dados srcassim que estiverem disponíveis.
Agora, com três comandos de filtro, ainda podemos fazer o mesmo: iniciá-los simultaneamente e conectá-los aos tubos:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
O que podemos fazer com relativa facilidade com pipes nomeados :
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'
(acima disso } 3<&0é para contornar o fato de &redirecionar stdinde /dev/nulle usamos <>para evitar a abertura dos tubos para bloquear até a outra extremidade (cat ) também seja aberta)
Ou, para evitar pipes nomeados, um pouco mais dolorosamente com o 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'
Agora, a pergunta é: quando todos os programas forem iniciados e conectados, os dados fluirão?
Temos duas restrições:
tee alimenta todas as suas saídas na mesma taxa, para que ele possa enviar dados apenas na taxa do tubo de saída mais lento.
cat só começará a ler a partir do segundo tubo (tubo 6 no desenho acima) quando todos os dados tiverem sido lidos no primeiro (5).
O que isso significa é que os dados não fluirão no tubo 6 até a cmd1conclusão. E, como no caso tr b Bacima, isso pode significar que os dados também não fluirão no tubo 3, o que significa que não fluirão em nenhum dos tubos 2, 3 ou 4, pois são teealimentados na taxa mais lenta de todos os 3.
Na prática, esses canais têm um tamanho não nulo; portanto, alguns dados conseguirão passar e, pelo menos no meu sistema, posso fazê-lo funcionar até:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Além disso, com
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Temos um impasse, onde estamos nessa situação:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Enchemos os tubos 3 e 6 (64 kiB cada). teeleu que byte extra, ele alimentou-a cmd1, mas
- agora está bloqueado a escrita no tubo 3, à espera de
cmd2esvaziá-lo
cmd2não pode esvaziá-lo porque está bloqueado na escrita no tubo 6, esperando catesvaziá-lo
cat não pode esvaziá-lo porque está aguardando até que não haja mais entrada no canal 5.
cmd1não posso dizer que catnão há mais entrada porque ela está esperando por mais entradas tee.
- e
teenão posso dizer que cmd1não há mais entrada porque está bloqueada ... e assim por diante.
Temos um loop de dependência e, portanto, um impasse.
Agora, qual é a solução? Tubos maiores 3 e 4 (grandes o suficiente para conter toda srca produção) seriam suficientes . Podemos fazer isso, por exemplo, inserindo pv -qB 1Gentre teee cmd2/3onde podemos pvarmazenar até 1 G de dados aguardando cmd2e cmd3lendo-os. Isso significaria duas coisas:
- que está usando potencialmente muita memória e, além disso, duplicando-a
- está falhando em ter todos os 3 comandos cooperados porque
cmd2, na realidade, só começaria a processar dados quando o cmd1 terminasse.
Uma solução para o segundo problema seria aumentar também os tubos 6 e 7. Supondo que cmd2e cmd3produzindo tanto quanto eles consomem, isso não consumiria mais memória.
A única maneira de evitar a duplicação dos dados (no primeiro problema) seria implementar a retenção de dados no próprio despachante, ou seja, implementar uma variação teeque possa alimentar os dados na taxa da saída mais rápida (mantendo os dados para alimentar o mais lentos no seu próprio ritmo). Não é realmente trivial.
Portanto, no final, o melhor que podemos obter razoavelmente sem programação é provavelmente algo como (sintaxe 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