Puoi usare una combinazione di GNU stdbuf e pee
di moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
fai pipì su popen(3)
quelle 3 righe di comando della shell e poi fread
s l'input e fwrite
s tutte e tre, che saranno bufferizzate fino a 1M.
L'idea è di avere un buffer almeno grande quanto l'input. In questo modo, anche se i tre comandi vengono avviati contemporaneamente, vedranno l'ingresso solo quando pee
pclose
i tre comandi vengono eseguiti in sequenza.
Su ciascuno pclose
, pee
svuota il buffer sul comando e attende la sua conclusione. Ciò garantisce che fino a quando questi cmdx
comandi non iniziano a emettere nulla prima di aver ricevuto alcun input (e non fork un processo che potrebbe continuare a essere emesso dopo che il loro genitore è tornato), l'output dei tre comandi non sarà intercalati.
In effetti, è un po 'come usare un file temporaneo in memoria, con lo svantaggio che i 3 comandi vengono avviati contemporaneamente.
Per evitare di avviare i comandi contemporaneamente, è possibile scrivere pee
come una funzione shell:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Ma attenzione che shell diverse da quelle zsh
fallirebbero per l'input binario con caratteri NUL.
Ciò evita di utilizzare file temporanei, ma ciò significa che l'intero input è archiviato in memoria.
In ogni caso, dovrai archiviare l'input da qualche parte, in memoria o in un file temporaneo.
In realtà, è una domanda piuttosto interessante, in quanto ci mostra il limite dell'idea Unix di avere diversi strumenti semplici cooperare per un singolo compito.
Qui, vorremmo che diversi strumenti collaborassero all'attività:
- un comando sorgente (qui
echo
)
- un comando dispatcher (
tee
)
- alcuni comandi filtro (
cmd1
, cmd2
, cmd3
)
- e un comando di aggregazione (
cat
).
Sarebbe bello se potessero funzionare tutti insieme contemporaneamente e fare il loro duro lavoro sui dati che dovrebbero elaborare non appena saranno disponibili.
Nel caso di un comando di filtro, è facile:
src | tee | cmd1 | cat
Tutti i comandi vengono eseguiti contemporaneamente, cmd1
inizia a sgranocchiare i dati src
non appena sono disponibili.
Ora, con tre comandi di filtro, possiamo ancora fare lo stesso: avviarli contemporaneamente e collegarli con i tubi:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Cosa che possiamo fare relativamente facilmente con le pipe denominate :
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'
(sopra } 3<&0
è per aggirare il fatto che i &
reindirizzamenti stdin
da /dev/null
, e usiamo <>
per evitare l'apertura dei tubi per bloccare fino a quando non cat
si apre anche l'altra estremità ( ))
O per evitare pipe nominate, un po 'più dolorosamente con 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'
Ora, la domanda è: una volta che tutti i programmi sono stati avviati e collegati, i dati fluiranno?
Abbiamo due controindicazioni:
tee
alimenta tutti i suoi output alla stessa velocità, quindi può inviare i dati solo alla velocità del suo tubo di uscita più lento.
cat
inizierà la lettura dalla seconda pipe (pipe 6 nel disegno sopra) solo quando tutti i dati sono stati letti dalla prima (5).
Ciò significa che i dati non fluiranno nel tubo 6 fino al cmd1
termine. E, come nel caso di quanto tr b B
sopra, ciò può significare che i dati non fluiranno neanche nella tubazione 3, il che significa che non fluiranno in nessuna delle tubazioni 2, 3 o 4 poiché si tee
alimenta alla velocità più lenta di tutte e 3.
In pratica quelle pipe hanno una dimensione non nulla, quindi alcuni dati riusciranno a passare, e almeno sul mio sistema, posso farlo funzionare fino a:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Oltre a ciò, con
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Abbiamo un punto morto, in cui siamo in questa situazione:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Abbiamo riempito i tubi 3 e 6 (64 kB ciascuno). tee
ha letto quel byte extra, lo ha alimentato cmd1
, ma
- ora è bloccato scrivendo sulla pipe 3 mentre sta aspettando
cmd2
di svuotarlo
cmd2
impossibile svuotarlo perché è bloccato nella scrittura sulla pipe 6, in attesa cat
di svuotarlo
cat
non può svuotarlo perché è in attesa fino a quando non ci sarà più input sulla pipe 5.
cmd1
non posso dire che non cat
ci sono più input perché è in attesa di ulteriori input da tee
.
- e
tee
non posso dire che cmd1
non c'è più input perché è bloccato ... e così via.
Abbiamo un ciclo di dipendenza e quindi un deadlock.
Ora, qual è la soluzione? I tubi più grandi 3 e 4 (abbastanza grandi da contenere tutta src
l'uscita) lo farebbero. Potremmo farlo ad esempio inserendo pv -qB 1G
tra tee
e cmd2/3
dove pv
potrebbero archiviare fino a 1G di dati in attesa cmd2
e cmd3
leggerli. Ciò significherebbe due cose però:
- che utilizza potenzialmente molta memoria e, inoltre, la duplica
- questo non riesce a far cooperare tutti e 3 i comandi perché
cmd2
in realtà inizierebbe a elaborare i dati solo quando cmd1 è terminato.
Una soluzione al secondo problema sarebbe quella di ingrandire anche i tubi 6 e 7. Supponendo che cmd2
e cmd3
produrre tanto output quanto consumano, ciò non consumerebbe più memoria.
L'unico modo per evitare la duplicazione dei dati (nel primo problema) sarebbe implementare la conservazione dei dati nel dispatcher stesso, ovvero implementare una variazione in tee
grado di alimentare i dati alla velocità dell'output più veloce (mantenendo i dati per alimentare il quelli più lenti al loro ritmo). Non proprio banale.
Quindi, alla fine, il meglio che possiamo ragionevolmente ottenere senza programmazione è probabilmente qualcosa di simile (sintassi 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