Die Prozessersetzungsausgabe ist nicht in der richtigen Reihenfolge

16

Das

echo one; echo two > >(cat); echo three; 

Befehl gibt eine unerwartete Ausgabe aus.

Ich habe folgendes gelesen: Wie wird die Prozessersetzung in bash implementiert? und viele andere Artikel zum Thema Prozesssubstitution im Internet, verstehen aber nicht, warum es sich so verhält.

Erwartete Ausgabe:

one
two
three

Wirkliche Leistung:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Auch diese beiden Befehle sollten aus meiner Sicht gleichwertig sein, aber sie tun es nicht:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Warum denke ich, sollten sie gleich sein? Denn beides verbindet die seqAusgabe mit der catEingabe über die anonyme Pipe - Wikipedia, Prozessersetzung .

Frage: Warum verhält es sich so? Wo ist mein Fehler? Die umfassende Antwort ist erwünscht (mit Erläuterung, wie das bashgeht, unter der Haube).

MiniMax
quelle
2
Auch wenn es auf den ersten Blick nicht so klar ist, handelt es sich tatsächlich um ein Duplikat des Wartens auf Prozess-in-Prozess-Ersetzungen, auch wenn der Befehl ungültig ist
Stéphane Chazelas,
2
Eigentlich wäre es besser, wenn diese andere Frage als Duplikat zu dieser Frage markiert würde, da diese Frage mehr auf den Punkt bringt. Deshalb habe ich dort meine Antwort abgeschrieben.
Stéphane Chazelas

Antworten:

21

Ja, in bashlike in ksh(woher die Funktion stammt) wird nicht auf die Prozesse innerhalb der Prozessersetzung gewartet (bevor der nächste Befehl im Skript ausgeführt wird).

Für <(...)einen ist das normalerweise in Ordnung wie in:

cmd1 <(cmd2)

Die Shell wartet darauf cmd1und cmd1wartet normalerweise darauf, cmd2bis das Dateiende in der ersetzten Pipe abgelesen wird. Das Dateiende tritt normalerweise auf, wenn die Pipe cmd2stirbt. Das ist der gleiche Grund , mehrere Schalen (nicht bash) nicht warten , stören cmd2in cmd2 | cmd1.

Dies cmd1 >(cmd2)ist jedoch in der Regel nicht der Fall, da in der Regel mehr cmd2darauf gewartet cmd1wird, bis das Programm beendet ist.

Das ist darin behoben, zshdass cmd2dort gewartet wird (aber nicht, wenn Sie es als schreiben cmd1 > >(cmd2)und cmd1es nicht eingebaut ist, verwenden Sie es {cmd1} > >(cmd2)stattdessen wie dokumentiert ).

kshwaitWartet nicht standardmäßig, sondern lässt dich mit dem eingebauten Programm darauf warten (es stellt auch die PID in zur Verfügung $!, obwohl das nicht hilft, wenn du das tust cmd1 >(cmd2) >(cmd3))

rc(mit der cmd1 >{cmd2}Syntax), mit der kshAusnahme, dass Sie die Pids aller Hintergrundprozesse mit erhalten können $apids.

es(auch mit cmd1 >{cmd2}) wartet auf cmd2like in zshund wartet auch auf cmd2in <{cmd2}Bearbeitung befindliche Weiterleitungen.

bashcmd2Stellt die PID von (oder genauer gesagt von der Subshell, da sie cmd2in einem untergeordneten Prozess dieser Subshell ausgeführt wird, obwohl dies der letzte Befehl dort ist) in zur Verfügung $!, lässt Sie jedoch nicht darauf warten.

Wenn Sie verwenden müssen bash, können Sie das Problem umgehen, indem Sie einen Befehl verwenden, der auf beide Befehle wartet:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Das macht beides cmd1und cmd2hat ihre fd 3 zu einer Pfeife geöffnet. catWartet auf das Dateiende am anderen Ende, wird also normalerweise nur beendet, wenn beide beendet sind cmd1und cmd2tot sind. Und die Shell wartet auf diesen catBefehl. Sie können dies als ein Netz betrachten, um die Beendigung aller Hintergrundprozesse &abzufangen (Sie können es für andere Dinge verwenden, die im Hintergrund gestartet wurden , z. B. für Coprocs oder sogar für Befehle, die selbst im Hintergrund ausgeführt werden, vorausgesetzt, sie schließen nicht alle ihre Dateideskriptoren, wie es Daemons normalerweise tun ).

Beachten Sie, dass es dank des oben erwähnten verschwendeten Subshell-Prozesses auch dann funktioniert, wenn cmd2fd 3 geschlossen wird (Befehle tun dies normalerweise nicht, aber einige mögen sudooder sshtun dies). Zukünftige Versionen von bashkönnen eventuell die Optimierung dort wie in anderen Shells durchführen. Dann brauchen Sie etwas wie:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Um sicherzustellen, dass es noch einen zusätzlichen Shell-Prozess gibt, bei dem fd 3 geöffnet ist und auf diesen sudoBefehl wartet .

Beachten Sie, dass catnichts gelesen wird (da die Prozesse nicht auf ihre fd 3 schreiben). Es ist nur für die Synchronisation da. Es wird nur ein read()Systemaufruf ausgeführt, der am Ende nichts zurückgibt.

Sie können das Ausführen tatsächlich vermeiden, catindem Sie eine Befehlsersetzung verwenden, um die Pipesynchronisierung durchzuführen:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Diesmal ist es die Shell cat, die aus der Pipe liest, deren anderes Ende auf fd 3 von cmd1und offen ist cmd2. Wir verwenden eine variable Zuweisung, damit der Exit-Status von cmd1in verfügbar ist $?.

Oder Sie können die Prozessersetzung von Hand durchführen und dann sogar die Ihres Systems verwenden, shda dies zur Standard-Shell-Syntax werden würde:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

obwohl zur Kenntnis , wie bereits festgestellt , dass nicht alle shImplementierungen für warten würden , cmd1nachdem cmd2beendet (obwohl das besser ist , als umgekehrt). Diese Zeit $?enthält den Exit-Status von cmd2; obwohl bashund zshmake cmd1‚s Exit - Status in ${PIPESTATUS[0]}und $pipestatus[1]jeweils (siehe auch die pipefailOption in einigen Schalen so $?kann berichten , das Versagen von Rohrkomponenten außer den letzten)

Beachten Sie, dass yashähnliche Probleme mit seinem Prozess hat Umleitung Funktion. cmd1 >(cmd2)würde dort geschrieben cmd1 /dev/fd/3 3>(cmd2)werden. Aber es cmd2wird nicht darauf gewartet, und Sie können auch nicht waitdarauf warten, und die pid wird auch nicht in der $!Variablen verfügbar gemacht . Sie würden die gleichen Workarounds wie für verwenden bash.

Stéphane Chazelas
quelle
Zuerst habe ich es versucht echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, dann auf vereinfacht echo one; echo two > >(cat) | cat; echo three;und es gibt auch Werte in der richtigen Reihenfolge aus. Sind all diese Deskriptormanipulationen 3>&1 >&4 4>&-notwendig? Außerdem verstehe ich das nicht >&4 4>&- wir werden stdoutzum vierten fd umgeleitet , schließen dann den vierten fd und verwenden 4>&1ihn dann erneut . Warum brauchte es und wie funktioniert es? Vielleicht sollte ich eine neue Frage zu diesem Thema erstellen?
MiniMax
1
@MiniMax, aber da haben Sie Einfluss auf die Standardausgabe von cmd1und cmd2. Der springende Punkt beim kleinen Tanz mit dem Dateideskriptor ist, die ursprünglichen wiederherzustellen und nur die zusätzliche Pipe für das Warten zu verwenden, anstatt auch die Ausgabe der Befehle zu kanalisieren.
Stéphane Chazelas
@MiniMax Es hat eine Weile gedauert, bis ich verstanden habe, dass ich die Pfeifen vorher nicht auf einem so niedrigen Niveau hatte. Ganz rechts 4>&1wird ein Dateideskriptor (fd) 4 für die Befehlsliste der äußeren geschweiften Klammern erstellt, der dem Standardwert der äußeren geschweiften Klammern entspricht. Die inneren Zahnspangen haben stdin / stdout / stderr automatisch eingerichtet, um eine Verbindung zu den äußeren Zahnspangen herzustellen. 3>&1Führt jedoch dazu, dass fd 3 mit dem Standard der äußeren Klammern verbunden wird. >&4Lässt das stdout der inneren Zahnspange mit dem fd 4 der äußeren Zahnspange verbunden werden (der zuvor erstellte). 4>&-schließt fd 4 von den inneren Klammern ab (Da stdout der inneren Klammern bereits mit fd 4 der äußeren Klammern verbunden ist).
Nicholas Pipitone
@MiniMax Der verwirrende Teil war der Teil von rechts nach links, 4>&1der zuerst ausgeführt wird, bevor die anderen Umleitungen ausgeführt werden, damit Sie ihn nicht "erneut verwenden 4>&1". Insgesamt sendet die innere Klammer Daten an ihre Standardausgabe, die mit der angegebenen FD 4 überschrieben wurde. Das fd 4, das der inneren Zahnspange gegeben wurde, ist das fd 4 der äußeren Zahnspange, das dem ursprünglichen stdout der äußeren Zahnspange entspricht.
Nicholas Pipitone
Bash fühlt sich so an 4>5, als würde "4 geht zu 5" bedeuten, aber wirklich "fd 4 wird mit fd 5 überschrieben". Und vor der Ausführung werden fd 0/1/2 automatisch verbunden (zusammen mit jedem fd der äußeren Hülle), und Sie können sie überschreiben, wie Sie möchten. Das ist zumindest meine Interpretation der Bash-Dokumentation. Wenn Sie verstehen etwas anderes aus diesem , lmk.
Nicholas Pipitone
4

Sie können den zweiten Befehl an einen anderen catweiterleiten. Dieser wartet, bis die Eingabe-Pipe geschlossen wird. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Kurz und einfach.

==========

So einfach es auch scheint, hinter den Kulissen spielt sich viel ab. Sie können den Rest der Antwort ignorieren, wenn Sie nicht daran interessiert sind, wie dies funktioniert.

Wenn Sie haben echo two > >(cat); echo three, >(cat)wird durch die interaktive Shell gegabelt und läuft unabhängig von echo two. Somit echo twoendet und wird dann echo threeausgeführt, aber bevor das >(cat)Ziel erreicht ist. Wenn bashDaten >(cat)abgerufen werden, von denen sie nicht erwartet wurden (ein paar Millisekunden später), erhalten Sie die sofortige Situation, in der Sie die Newline-Taste drücken müssen, um zum Terminal zurückzukehren (so, als ob ein anderer Benutzer mesgSie bearbeitet hätte ).

Es werden jedoch echo two > >(cat) | cat; echo threezwei Subshells erzeugt (gemäß der Dokumentation des |Symbols).

Eine Unterschale mit dem Namen A ist für echo two > >(cat)und eine Unterschale mit dem Namen B ist für cat. A wird automatisch mit B verbunden (A's Standard ist B's Standard). Dann echo twound >(cat)beginnen Sie mit der Ausführung. >(cat)'s stdout ist auf A's stdout gesetzt, was B's stdin entspricht. Nach dem echo twoBeenden wird A beendet und die Standardausgabe geschlossen. Hält >(cat)jedoch immer noch den Verweis auf B's ​​stdin. Das catstdin der Sekunde hält das stdin von B und das catwird nicht beendet, bis es ein EOF sieht. Ein EOF wird nur dann gegeben, wenn niemand mehr die Datei im Schreibmodus geöffnet hat, also >(cat)blockiert stdout die zweite cat. B bleibt auf diese Sekunde warten cat. Da echo twowird >(cat)irgendwann ein EOF abgegeben, also>(cat)Leert den Puffer und beendet das Programm. Niemand hält catmehr die Standardanzeige von B / Sekunde , daher catliest die zweite eine EOF (B liest die Standardanzeige überhaupt nicht, es ist ihm egal). Dieser EOF bewirkt, dass der zweite catseinen Puffer leert, seine Standardausgabe schließt und beendet, und dann wird B beendet, weil er beendet wurde catund B darauf gewartet hat cat.

Eine Einschränkung davon ist, dass Bash auch eine Subshell für erzeugt >(cat)! Aus diesem Grund werden Sie das sehen

echo two > >(sleep 5) | cat; echo three

Wartet immer noch 5 Sekunden vor der Ausführung echo three, auch wenn sleep 5B's Standard nicht hält. Dies liegt daran, dass eine verborgene Subshell, auf die C gespawnt hat, >(sleep 5)wartet sleepund C Bs Standard enthält. Sie können sehen, wie

echo two > >(exec sleep 5) | cat; echo three

sleepWartet jedoch nicht, da B nicht die Standardeinstellung enthält und es keine Geister-Subshell C gibt, die die Standardeinstellung von B enthält (exec erzwingt den Ruhezustand, um C zu ersetzen, anstatt C zu forken und darauf warten zu lassen sleep). Unabhängig von dieser Einschränkung,

echo two > >(exec cat) | cat; echo three

führt die Funktionen weiterhin ordnungsgemäß in der zuvor beschriebenen Reihenfolge aus.

Nicholas Pipitone
quelle
Wie bei der Konvertierung mit @MiniMax in den Kommentaren zu meiner Antwort festgestellt, hat dies jedoch den Nachteil, dass die Standardausgabe des Befehls beeinträchtigt wird und die Ausgabe eine zusätzliche Zeit lang gelesen und geschrieben werden muss.
Stéphane Chazelas
Die Erklärung ist nicht korrekt. Awartet nicht auf den cateingebrachten >(cat). Wie ich in meiner Antwort erwähnt, der Grund , warum echo two > >(sleep 5 &>/dev/null) | cat; echo threegibt threenach 5 Sekunden, da aktuelle Versionen basheinen zusätzlichen Shell - Prozesses anfallende Abfallmenge, >(sleep 5)dass darauf wartet , sleepund dieser Prozess ist immer noch stdout zu dem , pipewelche den zweiten verhindert catvon Abschluss. Wenn Sie es durch ersetzen echo two > >(exec sleep 5 &>/dev/null) | cat; echo three, um diesen zusätzlichen Prozess zu eliminieren, werden Sie feststellen, dass es sofort zurückkehrt.
Stéphane Chazelas
Es macht eine verschachtelte Unterschale? Ich habe versucht, in die Bash-Implementierung zu schauen, um es herauszufinden. Ich bin mir ziemlich sicher, dass echo two > >(sleep 5 &>/dev/null)das Minimum eine eigene Subshell bekommt. Ist es ein nicht dokumentiertes Implementierungsdetail, das dazu führt sleep 5, dass auch eine eigene Subshell erstellt wird? Wenn es dokumentiert ist, wäre es eine legitime Möglichkeit, es mit weniger Zeichen zu erledigen (es sei denn, es gibt eine enge Schleife, ich glaube nicht, dass jemand Leistungsprobleme mit einer Subshell oder einer Katze bemerkt). Wenn es nicht dokumentiert ist, funktioniert Rippen, netter Hack, in zukünftigen Versionen jedoch nicht.
Nicholas Pipitone
$(...), <(...)Sie beinhalten in der Tat eine Subshell, aber ksh93 oder zsh würde den letzten Befehl in dieser Subshell im gleichen Prozess, nicht ausgeführt bash, weshalb es gibt noch einen anderen Prozess das Rohr geöffnet und halten sleepläuft ein das Rohr nicht offen halten. Zukünftige Versionen von bashkönnen eine ähnliche Optimierung implementieren.
Stéphane Chazelas
1
@ StéphaneChazelas Ich habe meine Antwort aktualisiert und denke, dass die aktuelle Erklärung der kürzeren Version korrekt ist, aber Sie scheinen die Implementierungsdetails der Shells zu kennen, damit Sie sie überprüfen können. Ich denke, diese Lösung sollte im Gegensatz zum Dateideskriptortanz verwendet werden, da sie selbst unter den execBedingungen wie erwartet funktioniert.
Nicholas Pipitone