Sie könnten eine Kombination von GNU STDBUF und verwenden pee
von moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
Pee popen(3)
s diese 3 Shell-Kommandozeilen und dann fread
s die Eingabe und fwrite
s alles drei, die bis zu 1M gepuffert werden.
Die Idee ist, einen Puffer zu haben, der mindestens so groß ist wie die Eingabe. Auf diese Weise werden die drei Befehle zwar gleichzeitig gestartet, sie sehen jedoch nur Eingaben, wenn pee
pclose
die drei Befehle nacheinander ausgeführt werden.
Bei jedem pclose
, pee
leert den Puffer auf den Befehl und wartet auf seine Beendigung. cmdx
Dies stellt sicher, dass die Ausgabe der drei Befehle nicht erfolgt, solange diese Befehle noch keine Eingaben empfangen haben (und keinen Prozess auslösen, der nach der Rückkehr der übergeordneten Befehle möglicherweise fortgesetzt wird) verschachtelt.
In der Tat ist das ein bisschen wie das Verwenden einer temporären Datei im Speicher, mit dem Nachteil, dass die 3 Befehle gleichzeitig gestartet werden.
Um zu vermeiden, dass die Befehle gleichzeitig gestartet werden, können Sie pee
als Shell-Funktion schreiben :
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Achten Sie jedoch darauf, dass zsh
bei der Binäreingabe mit NUL-Zeichen andere Shells als fehlschlagen.
Das vermeidet die Verwendung temporärer Dateien, aber das bedeutet, dass die gesamte Eingabe im Speicher gespeichert wird.
In jedem Fall müssen Sie die Eingabe irgendwo im Speicher oder in einer temporären Datei speichern.
Eigentlich ist es eine interessante Frage, da sie uns die Grenzen der Unix-Idee aufzeigt, mehrere einfache Tools für eine einzige Aufgabe zusammenarbeiten zu lassen.
Hier möchten wir, dass mehrere Tools für die Aufgabe zusammenarbeiten:
- ein Quellbefehl (hier
echo
)
- ein Dispatcher-Befehl (
tee
)
- Einige Filterbefehle (
cmd1
, cmd2
, cmd3
)
- und einen Aggregationsbefehl (
cat
).
Es wäre schön, wenn sie alle zur gleichen Zeit laufen und hart an den Daten arbeiten könnten, die sie verarbeiten sollen, sobald sie verfügbar sind.
Bei einem Filterbefehl ist es einfach:
src | tee | cmd1 | cat
Alle Befehle werden gleichzeitig ausgeführt und cmd1
beginnen mit dem Munch von Daten, src
sobald diese verfügbar sind.
Mit drei Filterbefehlen können wir jetzt immer noch dasselbe tun: Starten Sie sie gleichzeitig und verbinden Sie sie mit Pipes:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Was wir mit Named Pipes relativ einfach machen können :
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'
(Über das } 3<&0
ist zu umgehen , dass &
Weiterleitungen stdin
von /dev/null
, und wir verwenden <>
, um das Öffnen der Rohre zu vermeiden, um zu blockieren, bis das andere Ende ( cat
) auch geöffnet hat)
Oder um zsh
Named Pipes zu vermeiden, etwas schmerzhafter mit 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'
Nun stellt sich die Frage: Wenn alle Programme gestartet und verbunden sind, fließen dann die Daten?
Wir haben zwei Einschränkungen:
tee
Alle Ausgaben werden mit derselben Rate eingespeist, sodass Daten nur mit der Rate der langsamsten Ausgabeleitung gesendet werden können.
cat
beginnt erst mit dem Lesen von der zweiten Pipe (Pipe 6 in der obigen Zeichnung), wenn alle Daten von der ersten Pipe (5) gelesen wurden.
Das bedeutet, dass die Daten in Pipe 6 erst cmd1
nach Abschluss fließen . Und wie im tr b B
obigen Fall kann dies bedeuten, dass Daten auch nicht in Pipe 3 fließen, was bedeutet, dass sie in keinem der Pipes 2, 3 oder 4 fließen, da tee
Feeds mit der langsamsten Rate von allen 3 erfolgen.
In der Praxis haben diese Pipes eine Größe ungleich Null, so dass einige Daten durchkommen, und auf meinem System kann ich zumindest erreichen, dass es funktioniert bis zu:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Darüber hinaus mit
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Wir haben eine Sackgasse, in der wir uns in dieser Situation befinden:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Wir haben die Rohre 3 und 6 gefüllt (jeweils 64 kB). tee
hat gelesen , dass zusätzliches Byte, um es zu gefüttert hat cmd1
, aber
- Es ist jetzt blockiert, auf Pipe 3 zu schreiben, da es darauf wartet
cmd2
, es zu leeren
cmd2
kann es nicht leeren, da es blockiert ist und darauf wartet cat
, es zu leeren
cat
kann es nicht leeren, da es wartet, bis keine Eingabe mehr in Pipe 5 erfolgt.
cmd1
Ich kann nicht sagen, dass cat
es keine weiteren Eingaben mehr gibt, da es selbst auf weitere Eingaben von wartet tee
.
- und
tee
kann nicht sagen, dass cmd1
es keine Eingabe mehr gibt, weil sie blockiert ist ... und so weiter.
Wir haben eine Abhängigkeitsschleife und damit einen Deadlock.
Was ist nun die Lösung? Größere Pipes 3 und 4 (groß genug, um die gesamte src
Ausgabe aufzunehmen) würden dies tun. Wir könnten das zum Beispiel tun, indem wir pv -qB 1G
zwischen tee
und cmd2/3
wo pv
bis zu 1 GB Daten einfügen, die darauf warten cmd2
und cmd3
sie lesen. Das würde jedoch zwei Dinge bedeuten:
- Das verbraucht möglicherweise viel Speicher und dupliziert ihn darüber hinaus
- Das bedeutet, dass nicht alle drei Befehle zusammenarbeiten, da
cmd2
die Datenverarbeitung in der Realität erst beginnen würde, wenn cmd1 fertig ist.
Eine Lösung für das zweite Problem wäre, die Rohre 6 und 7 ebenfalls zu vergrößern. Wenn Sie dies voraussetzen cmd2
und cmd3
so viel Leistung produzieren, wie sie verbrauchen, würde dies nicht mehr Speicher verbrauchen.
Die einzige Möglichkeit, das Duplizieren der Daten zu vermeiden (im ersten Problem), besteht darin, die Aufbewahrung der Daten im Dispatcher selbst zu implementieren. Dies ist eine Variation davon tee
, die Daten mit der Geschwindigkeit der schnellsten Ausgabe zuführen kann (Halten von Daten zum Zuführen der Daten) langsamer in ihrem eigenen Tempo). Nicht wirklich trivial.
Das Beste, was wir vernünftigerweise ohne Programmierung erreichen können, ist wahrscheinlich so etwas wie (Zsh-Syntax):
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