Wie analysiere ich diesen Befehl `{2> & 3“ $ @ ”&} 3> & 2 2> / dev / null`?

7

Vor einigen Wochen habe ich eine seltsame Antwort auf die Frage " (Wie) starte Aufgabe (n) stillschweigend im Hintergrund? " Gesehen . Diese Lösung scheint falsch zu sein ( siehe meine Antwort ), obwohl die Shell die Aufgabe im Hintergrund still zu starten scheint.

I. Problem: Können wir den Shell-Standardfehler tatsächlich umleiten?

Es gibt keine Erklärung für die vorgeschlagene Lösung und eine Analyse liefert keine verlässliche Antwort.

Unten sehen Sie das Code-Snippet.

# Run the command given by "$@" in the background
silent_background() {
   if [[ -n $BASH_VERSION ]]; then
      { 2>&3 "$@"& } 3>&2 2>/dev/null
   fi
}

Der problematische Befehl ist { 2>&3 "$@"& } 3>&2 2>/dev/null.

i) Analysieren

Die Befehlsgruppe ( { ... }) gibt zwei Umleitungen ( 3>&2und 2>/dev/null) für einen Befehl ( # Run the command given by "$@" in the background) an. Der Befehl ist eine asynchrone Liste ( cmd&) mit einer Umleitung ( 2>&3).

POSIX-Spezifikation

Jede Umleitung gilt für alle Befehle innerhalb des zusammengesetzten Befehls, die diese Umleitung nicht explizit überschreiben.

Die Standardfehlerumleitung 2>/dev/nullwird durch die der asynchronen Liste zugeordnete Umleitung überschrieben 2>&3.

Konkrete Fälle

Im ersten Fall wird der Standardfehler umgeleitet, /dev/nullwährend im zweiten Fall der Standardfehler des Befehls weiterhin an das Terminal angehängt wird.

prompt% { grep warning system.log& } 2>/dev/null
prompt% { 2>&3 grep warning system.log& } 3>&2 2>/dev/null
grep: system.log: No such file or directory

Unten sehen wir ähnliche Fälle. Im ersten Fall ist die Standardausgabe des Befehls weiterhin an das Terminal angehängt. Im zweiten Fall wird die Standardausgabeumleitung des Befehls geändert: >echo.txtwird von überschrieben >print.txt.

prompt% { >&3 echo some data...& } 3>&1 >echo.txt
[1] 3842
some data...
prompt% file echo.txt
echo.txt: empty
prompt% { >&3 echo some data...& } 3>print.txt >echo.txt
[1] 2765
prompt% file echo.txt
echo.txt: empty
prompt% cat print.txt
some data...

ii) Beobachtungen

Wie bereits erwähnt, scheint der Befehl im Hintergrund lautlos zu starten. Genauer gesagt wird die Benachrichtigung über einen Hintergrundjob, z. B. [1] 3842, nicht angezeigt.

Die vorherige Analyse impliziert, dass die Weiterleitungen überflüssig sind, da sie möglicherweise abgebrochen werden.

{ redir_3 cmd& } redir_1 redir_2ist äquivalent zu cmd&.

iii) Interpretation

In meinen Augen verbirgt das erwähnte Konstrukt die Benachrichtigung aufgrund einer Nebenwirkung.

Können Sie erklären, wie das passiert?

II. Hypothese (s)

Ilkkachus Antwort und Kommentare ermöglichten einige Fortschritte. Beachten Sie, dass jeder Prozess seine eigenen Dateideskriptoren hat.

  • Die Shell-Nachrichten können über den Shell-Standardfehler gesendet werden.

Ein weiterer Kontext: Eine asynchrone Liste, die in einer Subshell ausgeführt wird. Der Befehl gexistiert nicht.

prompt% ( g& )
g: command not found

Asynchrone Befehle, Befehle in Klammern, ..., werden in einer Subshell-Umgebung ausgeführt, die ein Duplikat der Shell-Umgebung ist ...

Eine Subshell erbt den Wert ihres Standardfehlerstroms von ihrer übergeordneten Shell.

Daher sollten sich im vorherigen Fall ihre Standardfehlerströme auf das Terminal beziehen. Die Jobbenachrichtigung wird jedoch nicht angezeigt, während die Shell-Fehlermeldung wahrscheinlich im Shell-Standardfehler angezeigt wird.

Fólkvangr
quelle
Ich denke, dass alles, was es tut, NICHT print [n] nnnnnzum Terminal ist. Wobei $ n $ und $ nnnnn $ Zahlen sind.
Strg-Alt-Delor
Sie stellen also fest, dass die Lösung in der Antwort, auf die Sie verweisen, funktioniert, aber Sie behaupten immer noch, dass sie falsch erscheint? Bedeutet das nicht, dass Ihre Intuition über diese Lösung vielleicht falsch ist und tatsächlich gut funktioniert? Vielleicht sollten Sie die Lösung daher zunächst nicht als falsch bezeichnen?
Ilkkachu
Beachten Sie, dass sich Ihr ( ( g & ) )Beispiel von dem oben angeforderten unterscheidet, da in Klammern eine Unterschale eingerichtet ist und keine Unterschale vorhanden ist { 2>&3 "$@"& } 3>&2 2>/dev/null.
Ilkkachu
@ Fólkvangr, die Klammer ()startet immer noch eine Unterschale, auch wenn es nur ein Paar gibt. Die Zahnspangen {}nicht. Soweit ich fork()weiß, ist Bash immer beim Starten einer Subshell, und das macht Hintergrundprozesse, die von Subshells gestartet werden, etwas anders, da sie keine untergeordneten Elemente des Haupt-Shell-Prozesses sind. Sie werden auch nicht zB in der Ausgabe jobsvon der Haupt-Shell usw. angezeigt (der Punkt der Frage, auf die Sie verlinken). Anscheinend macht sich Bash auch in diesem Fall nicht die Mühe, die Job-ID-Zeile auszudrucken. Das ist eine ganz andere Situation als beim { foo & }Umleitungstanz.
Ilkkachu
@ Fólkvangr, na ja, das hast du getan. Obwohl ich Probleme habe zu sehen, was Sie durch einen Vergleich damit erreichen wollen. Die Idee von { 2>&3 "$@" & } 3>&2 2>/dev/nullist, die Job-ID-Ausgabezeile loszuwerden, die die Subshell ( "$@" & )überhaupt nicht druckt ...
ilkkachu

Antworten:

10

Sie haben den wichtigsten Punkt verpasst. Die Shell-Umleitung wird in der Reihenfolge von links nach rechts angewendet.

Im:

{ 2>&3 "$@"& } 3>&2 2>/dev/null

Der gesamte Gruppenbefehl wird ausgeführt mit:

  • Dateideskriptor 3 => Standardfehler, der zu diesem Zeitpunkt terminal ist .
  • Dateideskriptor 2 (Standardfehler) => / dev / null

Wenn also ein Befehl innerhalb der Gruppierung ausgeführt wird:

  • Standardfehler => Dateideskriptor 3, der auf das Terminal zeigt.

Wenn Sie also "$@"&etwas mit dem Standardfehler drucken, wird die Ausgabe auf das Terminal gedruckt.


Für Ihre konkreten Fälle:

{ grep warning system.log& } 2>/dev/null

{ grep warning system.log& }Läufe mit Standardfehler wird angezeigt /dev/null. grepüberschreibt keine Umleitung, daher ist der Standardfehler derselbe mit {...}und wird umgeleitet /dev/null, wenn Sie keine Ausgabe an das Terminal erhalten haben.

Im:

{ 2>&3 grep warning system.log& } 3>&2 2>/dev/null

grepDer Standardfehler wird an den Dateideskriptor 3 umgeleitet, der wie oben erläutert auf das Terminal zeigt, sodass Sie eine Ausgabe an das Terminal erhalten.

cuonglm
quelle
Warum wird dann [1] 3842nicht angezeigt?
Choroba
@choroba Vielleicht kopiert / fügt das OP falsch ein oder Bash ändert sein Verhalten. Ich bin nicht sicher
cuonglm
Meinen Sie, Sie sehen es, wenn Sie die { 2>&3 grep warning system.log& } 3>&2 2>/dev/nullLinie laufen ? Ich nicht.
Choroba
1
@choroba, weil es an den stderr der Gruppe gesendet wird, den umgeleiteten . (Und zsh funktioniert einfach auf eine ganz andere Art und Weise, was ziemlich deutlich wird, weil die Antworten auf die verknüpfte Frage für jede Shell spezifisch sind.)
ilkkachu
@ilkkachu: Ein verwandter Trick besteht darin, einen Hintergrundprozess abzulehnen , indem er im Hintergrund einer Subshell gestartet wird . Parens statt Zahnspangen. ( sleep 10 & )druckt nichts und ist nicht in der jobsListe enthalten. Etwas schneller zu tippen sleep 10& disown -r. Möglicherweise benötigen Sie Umleitungen, um einen Befehlsblock zu vermeiden, wenn versucht wird, von stdin zu lesen.
Peter Cordes
5

In der interaktiven Bash, foo &läuft fooim Hintergrund und druckt seinen Job - ID und Prozess - ID an den Standardfehler des Shell.

foo 2>/dev/null &wird fooim Hintergrund ausgeführt, wobei der stderr an umgeleitet /dev/nullwird, druckt jedoch weiterhin die Job-ID und die Prozess-ID an den Standardfehler der Shell (nicht an den umgeleiteten stderr von foo).

{ foo & } 2>/dev/nullLeitet zuerst stderr an um /dev/null, dann wird das Innere von {}mit dieser Umleitung verarbeitet. Dann foowird im Hintergrund gestartet, und die Job-ID und die Prozess-ID werden auf den jetzt umgeleiteten stderr der Shell (dh bis /dev/null) gedruckt .

Das Handbuch von Bash impliziert, dass die Weiterleitungen außerhalb der Gruppe für die gesamte Gruppe gelten :

Wenn Befehle gruppiert werden, können Umleitungen auf die gesamte Befehlsliste angewendet werden.

Wir können überprüfen, ob die von der Gruppe angewendete Umleitung auch die Job-ID-Ausgabe umleitet:

$ {sleep 9 &} 2> temp
 [keine Ausgabe hier]
$ cat temp
[1] 8490

Wenn der Befehl schließlich beendet wird, erscheint der Hinweis darauf jedoch auf dem Terminal: (Oder besser gesagt, auf dem aktuellen stderr der Shell, was auch immer das ist.)

$ kill %1
[1]+  Terminated              sleep 99

In { ... } 3>&2 2>/dev/nullwird fd 3 dorthin umgeleitet, wo immer stderr auf jetzt zeigt, dann wird stderr dorthin umgeleitet /dev/null. Unter der Annahme, dass stderr ursprünglich auf das Terminal zeigte, ist das Ergebnis 3 -> terminal, 2 -> /dev/null. Diese Weiterleitungen gelten für die Gruppe.

Wenn wir { foo 2>&3 & }innerhalb der Gruppe haben, foowird im Hintergrund gestartet, wobei sein stderr auf den fd 3 der Gruppe zeigt, und die Job-ID und die Prozess-ID werden auf den stderr der Gruppe gedruckt.

Wenn Sie diese beiden zusammenfügen, zeigt fooder stderr auf die Stelle, auf die der fd 3 der Gruppe zeigt, dh auf den ursprünglichen stderr, und die Job-ID wird auf den fd 2 der Gruppe gedruckt, d /dev/null. H.

Leitet also in der Tat { 2>&3 foo & } 3>&2 2>/dev/nulldie von der Shell gedruckte Job-ID und Prozess-ID an um /dev/nullund leitet fooden Stderr um diese Umleitung herum weiter:

foo's stdout      -> group's fd 1 -> original stdout (never redirected)
foo's stderr      -> group's fd 3 -> original stderr
job id from group -> group's fd 2 -> /dev/null
ilkkachu
quelle
@ Fólkvangr, nun, das ist ein bisschen schwierig, ich glaube nicht, dass das Verhalten genau so dokumentiert ist ... Wie Sie in Ihrer Antwort Bashs Handbuch zitiert haben, heißt es nur, dass "[Bash] eine Zeile [mit dem Job ID etc.] ", es sagt nicht wo, genau es ist gedruckt. Die Tatsache, dass die Umleitung auf die zusammengesetzte Gruppe das Drucken der Job-ID beeinflusst, ist eigentlich nur eine empirische Beobachtung, aber es ist das Verhalten, auf das sich { 2>&3 foo & } 3>&2 2>/dev/nullTricks stützen. FWIW, Bash 3.2, Bash 4.4 und ksh93 sind in dieser Hinsicht ähnlich.
Ilkkachu
@ Fólkvangr, was die Fehlermeldung gbetrifft, dass der Befehl nicht ausgeführt werden kann, wird er nach dem Punkt gedruckt, an dem er g tatsächlich gestartet worden wäre , da der einzige Weg, um zu wissen, dass das Finale execvefehlschlägt, darin besteht, dass er einen Fehler zurückgibt. Und zu diesem Zeitpunkt sind alle Umleitungen für gbereits eingerichtet: Sie gmüssen gestartet worden sein und alle zu diesem Zeitpunkt eingerichteten Dateideskriptoren erhalten haben, wenn kein Fehler aufgetreten wäre. Die Job-ID und die Prozess-ID eines Hintergrundprogramms sind der übergeordneten Shell jedoch bekannt, bevor sie sich gselbst einrichtet.
Ilkkachu
@ Fólkvangr, ja, jeder Prozess hat seine eigenen Dateideskriptoren. Aber das macht nichts, was Sie vorher gesagt haben, falsch. Umleitungen können auf einen zusammengesetzten Befehl angewendet werden (entweder eine Befehlsliste { ... }, eine Subshell ( ... )oder if/ for/ while...), und die Shell muss die Dateideskriptoren entsprechend jonglieren. Außerdem scheinen alle Beweise darauf hinzudeuten, dass Bash tatsächlich die Job-ID-Nachricht in das zu diesem Zeitpunkt gültige stderr schreibt, das für den umgebenden zusammengesetzten Befehl festgelegt wurde. (Sie könnten if true; then somecmd & fi 2>/dev/nullauf die gleiche Weise wie verwenden { somecmd & } 2>/dev/null.)
ilkkachu
1

Sie vermissen, dass der {Befehl mit einem Nullwert ausgeführt stderrwird. Er startet die enthaltenen Hintergrundbefehle mit dem wiederhergestellten fd, aber er startet die Befehle und gibt die vom Befehl gestarteten Rückmeldungsnachrichten aus. Er wird mit dem Nullwert ausgeführt.

jthill
quelle
Was meinst du? Wenn Sie dies tun printf 2>/dev/null >&2 hello &, wird die vom Job gestartete Nachricht an das stderr der ausstellenden Umgebung und nicht an das stderr des gestarteten Jobs gesendet.
Bis zum
Das Dokument von zsh scheint über die Monitorausgabe an stderr zu lügen. Wenn Sie exec 2>/dev/nulleinen Job ausgeben und dann in den Hintergrund stellen, werden die Monitormeldungen weiterhin angezeigt. Dies gilt auch dann, wenn ich mit zsh 2>/dev/nullBash beginne .
Bis zum
Ich habe keine Ahnung was das heißt. Die Nachricht muss an einen fd gesendet werden. Das Dokument von zsh sagt, dass es es an stderr sendet, aber es sendet es nachweislich nicht an stderr. bashs doc sagt, dass es es an stderr sendet, und das tut es nachweislich.
Bis zum