Você pode usar uma combinação do GNU stdbuf e pee
do 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 fread
a 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
pclose
seqüenciais.
Em cada um pclose
, pee
libera 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 pee
como 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 zsh
falhem 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, cmd1
começa a coletar dados src
assim 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 stdin
de /dev/null
e 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 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'
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 cmd1
conclusão. E, como no caso tr b B
acima, 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 tee
alimentados 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). tee
leu que byte extra, ele alimentou-a cmd1
, mas
- agora está bloqueado a escrita no tubo 3, à espera de
cmd2
esvaziá-lo
cmd2
não pode esvaziá-lo porque está bloqueado na escrita no tubo 6, esperando cat
esvaziá-lo
cat
não pode esvaziá-lo porque está aguardando até que não haja mais entrada no canal 5.
cmd1
não posso dizer que cat
não há mais entrada porque ela está esperando por mais entradas tee
.
- e
tee
não posso dizer que cmd1
nã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 src
a produção) seriam suficientes . Podemos fazer isso, por exemplo, inserindo pv -qB 1G
entre tee
e cmd2/3
onde podemos pv
armazenar até 1 G de dados aguardando cmd2
e cmd3
lendo-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 cmd2
e cmd3
produzindo 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 tee
que 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