tee + cat: benutze eine Ausgabe mehrmals und verkette dann die Ergebnisse

18

Wenn ich einen Befehl aufrufe, echokann ich beispielsweise die Ergebnisse dieses Befehls in mehreren anderen Befehlen mit verwenden tee. Beispiel:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

Mit cat kann ich die Ergebnisse mehrerer Befehle sammeln. Beispiel:

cat <(command1) <(command2) <(command3)

Ich möchte in der Lage sein, beide Dinge gleichzeitig zu tun, damit ich teediese Befehle auf der Ausgabe von etwas anderem (zum Beispiel dem, das echoich geschrieben habe) aufrufen und dann alle ihre Ergebnisse auf einer einzigen Ausgabe mit sammeln kann cat.

Es ist wichtig , die Ergebnisse zu halten , um, bedeutet dies die Linien in der Ausgabe command1, command2und command3sollte nicht miteinander verflochten werden, aber bestellt als die Befehle sind (wie es mit geschieht cat).

Es gibt vielleicht bessere Optionen als catund teeaber das sind die, die ich bisher kenne.

Ich möchte die Verwendung temporärer Dateien vermeiden, da die Ein- und Ausgabe möglicherweise sehr umfangreich ist.

Wie könnte ich das machen?

PD: Ein weiteres Problem besteht darin, dass dies in einer Schleife geschieht, was den Umgang mit temporären Dateien erschwert. Dies ist der aktuelle Code, den ich habe, und er funktioniert für kleine Testfälle, aber er erzeugt Endlosschleifen beim Lesen und Schreiben aus der Aux-Datei, wie ich es nicht verstehe.

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

Lesen und Schreiben in Auxfile scheinen sich zu überschneiden und alles explodieren zu lassen.

Trylks
quelle
2
Wie groß reden wir? Ihre Anforderungen zwingen dazu, alles im Gedächtnis zu behalten. Um die Ergebnisse in der richtigen Reihenfolge zu halten, muss Befehl1 zuerst ausgeführt werden (also hat er vermutlich die gesamte Eingabe gelesen und die gesamte Ausgabe gedruckt), bevor Befehl2 und Befehl3 überhaupt mit der Verarbeitung beginnen können (es sei denn, Sie möchten ihre Ausgabe zuerst auch im Speicher sammeln).
Frostschutz
Sie haben Recht, Eingabe und Ausgabe von Befehl2 und Befehl3 sind zu groß, um im Speicher zu bleiben. Ich hatte erwartet, dass Swap besser funktionieren würde als die Verwendung temporärer Dateien. Ein weiteres Problem, das ich habe, ist, dass dies in einer Schleife geschieht, und das macht den Umgang mit Dateien noch schwieriger. Ich verwende eine einzelne Datei, aber aus irgendeinem Grund überlappen sich das Lesen und Schreiben der Datei, wodurch sie unendlich wächst. Ich werde versuchen, die Frage zu aktualisieren, ohne Sie mit zu vielen Details zu langweilen.
Trylks
4
Sie müssen temporäre Dateien verwenden. entweder für die Eingabe echo HelloWorld > file; (command1<file;command2<file;command3<file)oder für die Ausgabe echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. Genau so funktioniert es - tee kann Eingaben nur aufteilen, wenn alle Befehle parallel arbeiten und verarbeitet werden. Wenn ein Befehl inaktiv ist (weil Sie keine Verschachtelung wünschen), werden einfach alle Befehle blockiert, um zu verhindern, dass der Speicher mit Eingaben
gefüllt wird

Antworten:

27

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:

  1. Das verbraucht möglicherweise viel Speicher und dupliziert ihn darüber hinaus
  2. 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
Stéphane Chazelas
quelle
Sie haben Recht, der Deadlock ist das größte Problem, das ich bisher festgestellt habe, um die Verwendung temporärer Dateien zu vermeiden. Diese Dateien scheinen ziemlich schnell zu sein. Ich weiß nicht, ob sie irgendwo zwischengespeichert werden. Ich hatte Angst vor Festplattenzugriffszeiten, aber sie scheinen bisher vernünftig zu sein.
Trylks
6
Ein Extra +1 für die schöne ASCII-Kunst :-)
Kurt Pfeifle
3

Was Sie vorschlagen, kann nicht einfach mit einem vorhandenen Befehl ausgeführt werden und macht sowieso wenig Sinn. Die ganze Idee von Pipes ( |unter Unix / Linux) ist, dass in cmd1 | cmd2die cmd1Ausgabe (höchstens) geschrieben wird, bis ein Speicherpuffer voll ist, und dann cmd2Daten aus dem Puffer (höchstens) gelesen werden, bis er leer ist. Das heißt, cmd1und cmd2gleichzeitig ausgeführt, es ist nie erforderlich, dass mehr als eine begrenzte Datenmenge "im Flug" zwischen ihnen ist. Wenn Sie mehrere Eingänge mit einem Ausgang verbinden möchten, wenn einer der Leser hinter den anderen zurückbleibt, halten Sie entweder die anderen an (was ist der Grund dafür, parallel zu laufen?) Oder Sie verwahren den Ausgang, den der Nachzügler noch nicht gelesen hat (Was nützt es dann, keine Zwischendatei zu haben?) komplexer.

In meinen fast 30 Jahren Erfahrung mit Unix kann ich mich an keine Situation erinnern, die für eine solche Pipe mit mehreren Ausgängen wirklich von Vorteil gewesen wäre.

Sie können heute mehrere Ausgaben in einem Stream kombinieren, nur nicht in irgendeiner Weise verschachtelt (wie sollten die Ausgaben von cmd1und cmd2verschachtelt werden? Eine Zeile nach der anderen? Schreiben Sie abwechselnd 10 Bytes? Alternative "Absätze", die irgendwie definiert sind? Und wenn einer dies nicht tut?). lange nichts schreiben - all das ist kompliziert zu handhaben). Es ist geschehen, zB durch (cmd1; cmd2; cmd3) | cmd4die Programme cmd1, cmd2und cmd3einer nach dem anderen ausgeführt werden, wird die Ausgabe als Eingabe gesendet cmd4.

vonbrand
quelle
3

Für Ihr überlappendes Problem können Sie unter Linux (und mit bashoder zshaber nicht mit ksh93) Folgendes tun:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

Beachten Sie die Verwendung von (...)anstelle von {...}, um bei jeder Iteration einen neuen Prozess zu erhalten, damit ein neues fd 3 auf ein neues verweist auxfile. < /dev/fd/3ist ein Trick, um auf diese jetzt gelöschte Datei zuzugreifen. Es wird nicht auf anderen Systemen als Linux funktionieren, auf denen < /dev/fd/3es ähnlich ist, dup2(3, 0)und daher würde fd 0 im Nur-Schreib-Modus mit dem Cursor am Ende der Datei geöffnet sein.

Um die Verzweigung für die verschachtelte Funktion zu vermeiden, können Sie Folgendes schreiben:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

Die Shell würde sich bei jeder Iteration darum kümmern , die fd 3 zu sichern. Die Dateideskriptoren würden Ihnen jedoch früher ausgehen.

Sie werden feststellen, dass dies effizienter ist als:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

Verschachteln Sie also nicht die Umleitungen.

Stéphane Chazelas
quelle