Wie speichere ich / dev / stdout als Ziel in einem Bash-Skript?

12

Ich habe ein bestimmtes Bash-Skript, das den ursprünglichen /dev/stdoutSpeicherort beibehalten soll, bevor der erste Dateideskriptor durch einen anderen Speicherort ersetzt wird.

Also schrieb ich natürlich so etwas

old_stdout=$(readlink -f /dev/stdout)

Und es hat nicht funktioniert. Sehr schnell verstehe ich, was das Problem war:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Es ist $()klar, dass die Ausführung in einer Unterschale erfolgt, die an die übergeordnete Shell weitergeleitet wird.

Die Frage ist also: Gibt es eine zuverlässige (auf Portabilität zwischen Linux-Distributionen beschränkte) Möglichkeit, den Speicherort /dev/stdoutals Zeichenfolge in einem Bash-Skript zu speichern ?

alexey.e.egorov
quelle
Das klingt ein bisschen nach einem XY-Problem . Was ist das zugrunde liegende Problem?
Kusalananda
Grundlegendes Problem ist ein bestimmtes Installationsskript, das in zwei Modi ausgeführt wird: Stille, in dem alle Ausgaben in Datei und ausführlich protokolliert werden, wobei nicht nur in Datei protokolliert, sondern auch alles auf dem Terminal gedruckt wird. In beiden Modi möchte das Skript jedoch mit dem Benutzer interagieren, dh auf dem Terminal drucken und die Benutzerantwort lesen. Deshalb dachte ich, dass das Speichern /dev/stdoutdas Problem beim Drucken von Nachrichten im unbeaufsichtigten Modus lösen würde. Alternativ kann jede andere Aktion, die eine Ausgabe erzeugt, umgeleitet werden, und es gibt eine ganze Reihe von Aktionen. Etwa 100-mal mehr als Benutzerinteraktionsnachrichten.
alexey.e.egorov
Die Standardmethode für die Interaktion mit dem Benutzer ist das Drucken über stderr. Dies ist zum Beispiel der Grund, warum Eingabeaufforderungen stderrstandardmäßig angezeigt werden.
Kusalananda
Leider stderrmuss das auch umgeleitet und gespeichert werden, da das Skript eine Reihe von externen Programmen aufruft und alle möglichen Fehlermeldungen gesammelt und protokolliert werden sollen.
alexey.e.egorov

Antworten:

14

Um einen Dateideskriptor zu speichern, duplizieren Sie ihn auf einem anderen fd. Das Speichern eines Pfads zur entsprechenden Datei reicht nicht aus. Sie müssen den Öffnungsmodus, die Öffnungsflags, die aktuelle Position in der Datei usw. speichern. Und natürlich würde das bei anonymen Pipes oder Sockets nicht funktionieren, da diese keinen Pfad haben. Was Sie speichern möchten, ist die offene Dateibeschreibung , auf die sich die FD bezieht, und das Duplizieren einer FD gibt tatsächlich eine neue FD an dieselbe offene Dateibeschreibung zurück .

Um einen Dateideskriptor mit einer Bourne-ähnlichen Shell auf einen anderen zu duplizieren, lautet die Syntax wie folgt:

exec 3>&1

Oben ist fd 1 auf fd 3 dupliziert.

Was auch immer fd 3 zuvor bereits geöffnet war, wird geschlossen. Beachten Sie jedoch, dass fds 3 bis 9 (normalerweise mehr, bis zu 99 mit yash) für diesen Zweck reserviert sind (und entgegen 0, 1 oder 2 keine spezielle Bedeutung haben) Shell weiß, dass sie nicht für interne Zwecke eingesetzt werden dürfen. Der einzige Grund, warum fd 3 zuvor geöffnet gewesen wäre, ist, dass Sie es in Skript 1 getan haben oder dass es vom Aufrufer durchgesickert ist.

Dann können Sie stdout in etwas anderes ändern:

exec > /dev/null

Und später, um stdout wiederherzustellen:

exec >&3 3>&-

( 3>&-um den Dateideskriptor zu schließen, den wir nicht mehr benötigen).

Das Problem dabei ist, dass außer in ksh jeder Befehl, den Sie danach ausführen, exec 3>&1das fd 3 erbt. Das ist ein fd-Leck. Im Allgemeinen keine große Sache, aber das kann zu Problemen führen.

kshsetzt das Close-On-Exec- Flag auf diesen FDS (für FDS über 2), aber andere Shells und andere Shells haben keine Möglichkeit, dieses Flag manuell zu setzen.

Die Arbeit für die andere Shell besteht darin, fd 3 für jeden einzelnen Befehl zu schließen, wie zum Beispiel:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Schwerfällig. Hier wäre der beste Weg, überhaupt nicht zu verwenden exec, sondern Befehlsgruppen umzuleiten:

{
  ls
  uname
} > file.log

Dort ist es die Shell, die dafür sorgt, dass stdout gespeichert und anschließend wiederhergestellt wird (und dies geschieht intern, indem es auf einem fd (über 9, über 99 für yash) mit gesetztem Flag close-on-exec dupliziert wird ).

Anmerkung 1

Jetzt kann die Verwaltung dieser FDS 3 bis 9 mühsam und problematisch sein, wenn Sie sie ausgiebig oder in Funktionen verwenden, insbesondere wenn Ihr Skript Code von Drittanbietern verwendet, der diese FDS wiederum verwenden kann.

Einige Schalen ( zsh, bash, ksh93, alle Features (hinzugefügt vorgeschlagen von Oliver Kiddle vonzsh ) etwa zur gleichen Zeit im Jahr 2005 , nachdem sie unter ihren Entwicklern diskutiert wurden) eine alternative Syntax , um die erste freie fd über 10 statt zuzuordnen , die in diesem Fall hilft:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}
Stéphane Chazelas
quelle
Außerdem ist Ihr Code in gewissem Sinne falsch, dass fd 3 bereits belegt sein könnte, wie es der Fall ist, wenn ein Scrip von einem rc.localDienst ausgeführt wird, z exec {FD}>&1. Dies wird jedoch nur in Bash 4 unterstützt, was wirklich traurig ist. Das ist also nicht wirklich portabel.
alexey.e.egorov
@ alexey.e.egorov, siehe bearbeiten.
Stéphane Chazelas
Bash 3. * unterstützt diese Funktion nicht und diese Version wird in Centos 5 verwendet, das weiterhin unterstützt und verwendet wird. Und freie Deskriptoren zu finden und dann eval "exec $i>&1"ist eine Sache, die ich vermeiden möchte, weil es umständlich ist. Kann ich mich wirklich darauf verlassen, dass fds über 9 dann kostenlos wären?
alexey.e.egorov
@ alexey.e.egorov, nein, du schaust es rückwärts an. fds 3 bis 9 können kostenlos verwendet werden (und es liegt an Ihnen, sie zu verwalten, wie Sie möchten) und sind für diesen Zweck vorgesehen. fds über 9 kann von der Shell intern verwendet werden, und das Schließen kann schlimme Folgen haben. Die meisten Muscheln lassen Sie nicht verwenden. bashLass dich in den Fuß schießen.
Stéphane Chazelas
2
@ alexey.e.egorov, wenn beim Start Ihres Skripts einige FDS in (3..9) geöffnet sind, liegt das daran, dass Ihr Anrufer vergessen hat, sie zu schließen oder das Close-On-Exec-Flag auf sie zu setzen. Das nenne ich ein FD-Leck. Vielleicht hat der Anrufer beabsichtigt, diese FDS an Sie weiterzuleiten, damit Sie Daten von diesen FDS lesen und / oder darauf schreiben können, aber dann wissen Sie Bescheid. Wenn Sie nicht über sie Bescheid wissen, dann ist es Ihnen egal, dann können Sie sie frei schließen (beachten Sie, dass es nur den Prozess fd Ihres Skripts schließt, nicht den Ihres Aufrufers).
Stéphane Chazelas
3

Wie Sie sehen, ist Bash-Scripting keine normale Programmiersprache, in der Sie Dateideskriptoren zuweisen können.

Die einfachste Lösung besteht darin, eine Sub-Shell zu verwenden, um das auszuführen, was umgeleitet werden soll, damit die Verarbeitung auf die Top-Shell zurückgesetzt werden kann, deren Standard-E / A intakt ist.

Eine alternative Lösung wäre tty, das TTY-Gerät zu identifizieren und die E / A in Ihrem Skript zu steuern. Beispielsweise:

dev=$(tty)

und dann kannst du ..

echo message > $dev
Julie Pelletier
quelle
> Alternativ können Sie tty verwenden, um das TTY-Gerät zu identifizieren und die E / A in Ihrem Skript zu steuern. Wie macht man das?
alexey.e.egorov
1
Ich habe nur ein Beispiel in meine Antwort aufgenommen.
Julie Pelletier
1

$$ würde Ihnen die aktuelle Prozess-PID geben, im Falle einer interaktiven Shell oder eines Skripts die entsprechende Shell-PID.

So können Sie verwenden:

readlink -f /proc/$$/fd/1

Beispiel:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33
heemayl
quelle
1
Während es funktionsfähig ist, verursacht das Verlassen auf eine bestimmte /procStruktur Portabilitätsprobleme, ebenso wie die Verwendung, /dev/stdoutwie in der Frage erwähnt.
Julie Pelletier
1
@JuliePelletier Aufbauend auf einer bestimmten /procStruktur? Es würde auf jedem Linux funktionieren, das hat procfs..
Heemayl
1
Richtig, so können wir für Linux verallgemeinern, wie procfses fast immer der Fall ist, aber wir sehen oft Fragen zur Portabilität und eine gute Entwicklungsmethode beinhaltet die Berücksichtigung der Portabilität auf andere Systeme. bashkann auf einer Vielzahl von Betriebssystemen ausgeführt werden.
Julie Pelletier