Praktische Verwendung zum Verschieben von Dateideskriptoren

16

Laut der Bash-Manpage:

Der Umleitungsoperator

   [n]<&digit-

Verschiebt den Dateideskriptor digitin den Dateideskriptor noder die Standardeingabe (Dateideskriptor 0), falls nnicht angegeben. digitwird nach dem Duplizieren auf geschlossen n.

Was bedeutet es, einen Dateideskriptor in einen anderen zu "verschieben"? Was sind die typischen Situationen für eine solche Praxis?

QUentin
quelle

Antworten:

14

3>&4-ist eine ksh93-Erweiterung, die auch von bash unterstützt wird, und das ist die Abkürzung für 3>&4 4>&-: 3 zeigt jetzt auf die Stelle, an der 4 verwendet wurde, und 4 ist jetzt geschlossen.

Typische Verwendung ist in Fällen, in denen Sie eine Kopie dupliziert stdinoder stdoutgespeichert haben und diese wiederherstellen möchten, wie in:

Angenommen, Sie möchten das stderr eines Befehls (und nur stderr) erfassen, während Sie stdout in einer Variablen alleine lassen.

Befehlsersetzung var=$(cmd), erzeugt eine Pipe. Das schreibende Ende der Pipe wird cmdzu stdout (Dateideskriptor 1) und das andere Ende wird von der Shell gelesen, um die Variable zu füllen.

Nun, wenn Sie wollen , stderrum die Variable gehen, könnten Sie tun: var=$(cmd 2>&1). Jetzt gehen sowohl fd 1 (stdout) als auch 2 (stderr) zur Pipe (und schließlich zur Variablen), was nur die Hälfte dessen ist, was wir wollen.

Wenn wir das tun var=$(cmd 2>&1-)( Abkürzung für var=$(cmd 2>&1 >&-), geht jetzt nur noch cmdstderr zur Pipe, aber fd 1 ist geschlossen. Wenn cmdversucht wird, eine Ausgabe zu schreiben, die mit einem EBADFFehler zurückkommt , wird beim Öffnen einer Datei die erste freie fd abgerufen und der geöffneten Datei zugewiesen, es sei stdoutdenn, der Befehl schützt davor! Nicht das, was wir wollen.

Wenn wir wollen, dass die Standardressource cmdin Ruhe gelassen wird, dh auf dieselbe Ressource verweist, auf die sie außerhalb der Befehlsersetzung zeigt, müssen wir diese Ressource irgendwie in die Befehlsersetzung einbinden. Dafür können wir eine Kopie von stdout außerhalb der Befehlsersetzung erstellen, um sie in das Innere zu bringen.

{
  var=$(cmd)
} 3>&1

Was ist eine sauberere Art zu schreiben:

exec 3>&1
var=$(cmd)
exec 3>&-

(was auch den Vorteil hat, fd 3 wiederherzustellen, anstatt es am Ende zu schließen).

Dann auf dem {(oder den exec 3>&1) und bis zu der }, fd beide 1 und 3 auf die gleiche Ressource ursprünglich FD1 gezeigt wird. fd 3 zeigt auch auf diese Ressource innerhalb der Befehlsersetzung (die Befehlsersetzung leitet nur die fd 1, stdout um). Also oben haben cmdwir für die FDS 1, 2, 3:

  1. das rohr zu var
  2. unberührt
  3. Das Gleiche wie das, was 1 außerhalb der Befehlsersetzung anzeigt

Wenn wir es ändern zu:

{
  var=$(cmd 2>&1 >&3)
} 3>&1-

Dann wird es:

  1. Das Gleiche wie das, was 1 außerhalb der Befehlsersetzung anzeigt
  2. das rohr zu var
  3. Das Gleiche wie das, was 1 außerhalb der Befehlsersetzung anzeigt

Jetzt haben wir, was wir wollten: stderr geht zur Pipe und stdout bleibt unberührt. Allerdings lecken wir diesen fd 3 zu cmd.

Während Befehle (gemäß Konvention) davon ausgehen, dass die FDS 0 bis 2 geöffnet und Standardeingabe, -ausgabe und -fehler sind, nehmen sie nichts von anderen FDS an. Höchstwahrscheinlich werden sie diese fd 3 unberührt lassen. Wenn sie einen anderen Dateideskriptor benötigen, führen sie einfach einen aus, open()/dup()/socket()...der den ersten verfügbaren Dateideskriptor zurückgibt. Wenn sie (wie ein Shell-Skript, das dies tut exec 3>&1) fdspeziell verwenden müssen, weisen sie es zuerst zu (und in diesem Prozess wird die von unserem fd 3 gehaltene Ressource durch diesen Prozess freigegeben).

Es ist eine gute Praxis, das fd 3 zu schließen, da cmdes nicht verwendet wird, aber es ist keine große Sache, wenn wir es zugewiesen lassen, bevor wir anrufen cmd. Die Probleme können sein: dass cmd(und möglicherweise andere Prozesse, die es hervorbringt) ein fd weniger zur Verfügung hat. Ein potenziell schwerwiegenderes Problem besteht darin, dass die Ressource, auf die fd verweist, möglicherweise von einem Prozess cmdim Hintergrund gehalten wird. Es kann ein Problem sein, wenn diese Ressource eine Pipe oder ein anderer Kommunikationskanal zwischen Prozessen ist (wie wenn Ihr Skript ausgeführt wird als script_output=$(your-script)), da dies bedeutet, dass der Prozess, der vom anderen Ende liest, bis dahin niemals das Dateiende sieht Hintergrundprozess wird beendet.

Also hier ist es besser zu schreiben:

{
  var=$(cmd 2>&1 >&3 3>&-)
} 3>&1

Womit bashkann gekürzt werden:

{
  var=$(cmd 2>&1 >&3-)
} 3>&1

Um die Gründe zusammenzufassen, warum es selten verwendet wird:

  1. Es ist kein Standard und nur syntaktischer Zucker. Sie müssen ein paar Tastenanschläge sparen, um Ihr Skript weniger portabel und für Leute, die an diese ungewöhnliche Funktion nicht gewöhnt sind, weniger offensichtlich zu machen.
  2. Die Notwendigkeit, den Original-FD nach dem Duplizieren zu schließen, wird oft übersehen, da wir die meiste Zeit nicht unter der Konsequenz leiden, also tun wir dies einfach >&3anstelle von >&3-oder >&3 3>&-.

Der Beweis, dass es nur selten verwendet wird, ist, wie Sie herausgefunden haben, dass es sich um einen Schwindel handelt . In Bash compound-command 3>&4-oder any-builtin 3>&4-Blätter fd 4 geschlossen, auch nachdem compound-commandoder any-builtinzurückgekehrt ist. Ein Patch zur Behebung des Problems ist jetzt (19.02.2013) verfügbar.

Stéphane Chazelas
quelle
Danke, jetzt weiß ich, was ein FD bewegt. Ich habe 4 grundlegende Fragen zum 2. Snippet (und zu fds im Allgemeinen): 1) In cmd1 machen Sie 2 (stderr) zu einer Kopie von 3. Was wäre, wenn der Befehl diese 3 fd intern verwenden würde? 2) Warum funktionieren 3> & 1 und 4> & 1? Das Duplizieren von 3 und 4 wird nur in diesen beiden Cmds wirksam. Ist die aktuelle Shell auch betroffen? 3) Warum schließen Sie 4 in cmd1 und 3 in cmd2? Diese Befehle verwenden nicht die genannten FDS, oder? 4) Was passiert im letzten Snippet, wenn ein fd zu einem nicht existierenden dupliziert wird (in cmd1 und cmd2), ich meine zu 3 bzw. 4?
Quentin
@Quentin, ich habe ein einfacheres Beispiel verwendet und etwas mehr gehofft, dass es jetzt weniger Fragen aufwirft als beantwortet. Wenn Sie noch Fragen haben, die nicht direkt mit der fd moving-Syntax zusammenhängen, sollten Sie eine separate Frage stellen
Stéphane Chazelas,
{ var=$(cmd 2>&1 >&3) ; } 3>&1-Ist es nicht ein Tippfehler beim Schließen von 1?
Quentin
@Quentin, es ist ein Tippfehler, dass ich glaube nicht , dass ich es schließen beabsichtigt, jedoch macht es keinen Unterschied , da nichts in den geschweiften Klammern Anwendungen , die 1 fd (es war der ganze Sinn es zu duplizieren 3 bis fd: weil das Original 1 sonst wäre innen nicht zugänglich $(...)).
Stéphane Chazelas
1
@Quentin Beim Eintreten {...}zeigt fd 3 auf das, was fd 1 zum Zeigen verwendet hat, und fd 1 wird geschlossen. Beim Eintreten $(...)wird fd 1 auf das Rohr gesetzt, das speist $var, dann für cmd2 auf das Rohr und dann für 1 auf welche 3 Punkte zu, das ist die äußere 1. Die Tatsache, dass 1 danach geschlossen bleibt, ist ein Fehler in der Bash, ich werde es melden. ksh93, von dem diese Funktion stammt, hat diesen Fehler nicht.
Stéphane Chazelas
4

Dies bedeutet, dass auf dieselbe Stelle verwiesen wird, auf die der andere Dateideskriptor verweist. Sie müssen dies tun , sehr selten, abgesehen von der offensichtlichen getrennten Handhabung des Standardfehler Descriptor ( stderr, fd 2, /dev/stderr -> /proc/self/fd/2). Es kann in einigen komplexen Fällen nützlich sein.

Das Advanced Bash Scripting-Handbuch enthält das folgende Beispiel für eine längere Protokollebene und das folgende Snippet:

# Redirecting only stderr to a pipe.
exec 3>&1                              # Save current "value" of stdout.
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # Close fd 3 for 'grep' (but not 'ls').
#              ^^^^   ^^^^
exec 3>&-                              # Now close it for the remainder of the script.

In Source Mage's Sorcery verwenden wir es zum Beispiel, um verschiedene Ausgaben desselben Codeblocks zu unterscheiden:

  (
    # everything is set, so run the actual build infrastructure
    run_build
  ) 3> >(tee -a $C_LOG >> /dev/stdout) \
    2> >(tee -a $C_LOG 1>&2 > $VOYEUR_STDERR) \
     > >(tee -a $C_LOG > $VOYEUR_STDOUT)

Aus Gründen der Protokollierung wurde eine zusätzliche Prozessersetzung hinzugefügt (VOYEUR entscheidet, ob die Daten auf dem Bildschirm angezeigt oder nur protokolliert werden sollen), es müssen jedoch immer einige Meldungen angezeigt werden. Um dies zu erreichen, drucken wir sie in den Dateideskriptor 3 und behandeln sie dann speziell.

lynxlynxlynx
quelle
0

In Unix werden Dateien von Dateideskriptoren behandelt (kleine Ganzzahlen, z. B. Standardeingabe ist 0, Standardausgabe ist 1, Standardfehler ist 2; wenn Sie andere Dateien öffnen, wird ihnen normalerweise der kleinste nicht verwendete Deskriptor zugewiesen). Wenn Sie also die Inards des Programms kennen und die Ausgabe, die an Dateideskriptor 5 gesendet wird, an die Standardausgabe senden möchten, verschieben Sie Deskriptor 5 nach 1. Daher kommen die 2> errorsund Konstruktionen, 2>&1in die Fehler dupliziert werden sollen der Ausgabestream.

So wird es kaum benutzt (ich erinnere mich vage, dass ich es in meinen über 25 Jahren fast ausschließlicher Unix-Nutzung ein- oder zweimal im Zorn benutzt habe), aber wenn es gebraucht wird, ist es absolut notwendig.

vonbrand
quelle
Warum aber nicht den Dateideskriptor 1 wie folgt duplizieren: 5> & 1? Ich verstehe nicht, was es bringt, FD zu verschieben, da der Kernel es gleich schließen wird, nachdem ...
Quentin
Das dupliziert nicht 5 zu 1, sondern sendet 5 zu 1. Und bevor du fragst; ja, es gibt verschiedene notationen, die von einer vielzahl von vorläuferschalen aufgenommen wurden.
Vonbrand
Verstehe es immer noch nicht. Wenn " 5>&15" an "1" gesendet wird, was genau macht 1>&5-das dann neben dem Schließen von "5"?
Quentin