Sie könnten eine Kombination von GNU STDBUF und verwenden peevon moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
Pee popen(3)s diese 3 Shell-Kommandozeilen und dann freads die Eingabe und fwrites 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 pclosedie drei Befehle nacheinander ausgeführt werden.
Bei jedem pclose, peeleert den Puffer auf den Befehl und wartet auf seine Beendigung. cmdxDies 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 peeals 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 zshbei 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 cmd1beginnen mit dem Munch von Daten, srcsobald 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<&0ist zu umgehen , dass &Weiterleitungen stdinvon /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 zshNamed 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 cmd1nach Abschluss fließen . Und wie im tr b Bobigen 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 teeFeeds 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). teehat 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
cmd2kann 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.
cmd1Ich kann nicht sagen, dass cates keine weiteren Eingaben mehr gibt, da es selbst auf weitere Eingaben von wartet tee.
- und
teekann nicht sagen, dass cmd1es 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 srcAusgabe aufzunehmen) würden dies tun. Wir könnten das zum Beispiel tun, indem wir pv -qB 1Gzwischen teeund cmd2/3wo pvbis zu 1 GB Daten einfügen, die darauf warten cmd2und cmd3sie 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
cmd2die 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 cmd2und cmd3so 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