Woher weiß ein Programm, ob stdout mit einem Terminal oder einer Pipe verbunden ist?

12

Ich habe Probleme beim Debuggen eines Segfault-Programms, da ich die Ausgabe direkt vor dem Segfault benötige. Diese geht jedoch verloren, wenn ich die Ausgabe an eine Datei weitergebe. Laut dieser Antwort: /unix//a/17339/22615 . Dies liegt daran, dass der Ausgabepuffer des Programms beim Anschließen an ein Terminal sofort gelöscht wird, beim Anschließen an eine Pipe jedoch nur an bestimmten Punkten. Ein paar Fragen hier:

  • Wie bestimmt ein Programm, mit welcher Standardausgabe es verbunden ist?

  • Wie bewirkt der Befehl "script" dasselbe Verhalten wie beim Schreiben auf ein Terminal?

  • Kann dies ohne den Skriptbefehl erreicht werden?

mowwwalker
quelle
Eine verwandte Frage ist unix.stackexchange.com/q/513926/5132 .
JdeBP

Antworten:

23

Angeben, ob ein Dateideskriptor auf ein Endgerät verweist

Ein Programm kann mithilfe der isatty()Standard-C-Funktion feststellen, ob einem tty-Gerät ein Dateideskriptor zugeordnet ist (darunter befindet sich im Allgemeinen ein harmloser tty-spezifischer ioctl()Systemaufruf, der mit einem Fehler zurückgegeben würde, wenn der fd nicht auf ein tty-Gerät verweist). .

Das Dienstprogramm [/ testkann dies mit seinem -tOperator tun .

if [ -t 1 ]; then
  echo stdout is open to a terminal
fi

Libc-Funktionsaufrufe auf einem GNU / Linux-System verfolgen:

$ ltrace [ -t 1 ] | cat
[...]
isatty(1)                                      = 0
[...]

Systemaufrufe verfolgen:

$ strace [ -t 1 ] | cat
[...]
ioctl(1, TCGETS, 0x7fffd9fb3010)        = -1 ENOTTY (Inappropriate ioctl for device)
[...]

Sagen, ob es auf eine Pfeife zeigt

Um festzustellen, ob ein fd mit einer Pipe / einem fifo verknüpft ist, können Sie den fstat()Systemaufruf verwenden , der eine Struktur zurückgibt, deren st_modeFeld den Typ und die Berechtigungen der auf diesem fd geöffneten Datei enthält. Das S_ISFIFO()Standard-C-Makro kann in diesem st_modeFeld verwendet werden, um zu bestimmen, ob es sich bei dem FD um eine Pipe / FIFO handelt.

Es gibt kein Standarddienstprogramm, das a ausführen kann fstat(), aber es gibt mehrere inkompatible Implementierungen eines statBefehls, die dies ausführen können. zsh's statbuiltin, mit stat -sf "$fd" +modedem der Modus als Zeichenfolgendarstellung zurückgegeben wird, deren erstes Zeichen den Typ darstellt ( pfür Pipe). GNU statkann dasselbe tun stat -c %A - <&"$fd", muss aber auch stat -c %F - <&"$fd"den Typ alleine melden . Mit BSD stat: stat -f %St <&"$fd"oder stat -f %HT <&"$fd".

Sagen, ob es suchbar ist

Anwendungen interessieren sich im Allgemeinen nicht dafür, ob stdout eine Pipe ist. Sie können sich darum kümmern, dass es suchbar ist (obwohl sie im Allgemeinen nicht entscheiden, ob sie puffern wollen oder nicht).

Um zu testen, ob ein fd suchbar ist (Pipes, Sockets, tty-Geräte sind nicht suchbar, reguläre Dateien und die meisten Blockgeräte im Allgemeinen), kann ein relativer lseek()Systemaufruf mit einem Offset von 0 (also harmlos) versucht werden . ddist ein Standarddienstprogramm, zu dem eine Schnittstelle gehört, das lseek()jedoch für diesen Test nicht verwendet werden kann, da Implementierungen überhaupt nicht aufgerufen würden, lseek()wenn Sie nach einem Offset von 0 fragen.

Die zshund ksh93Shells haben jedoch nach Operatoren gesucht:

$ strace -e lseek ksh -c ': 1>#((CUR))' | cat
lseek(1, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
ksh: 1: not seekable
$ strace -e lseek zsh -c 'zmodload zsh/system; sysseek -w current -u 1 0 || syserror'
lseek(1, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
Illegal seek

Pufferung deaktivieren

Der scriptBefehl verwendet ein Pseudo-Terminal-Paar, um die Ausgabe eines Programms zu erfassen, sodass das stdout (und stdin und stderr) des Programms ein Pseudo-Terminal-Gerät ist.

Wenn sich die Standardausgabe auf ein Endgerät bezieht, ist im Allgemeinen noch eine gewisse Pufferung vorhanden, die jedoch zeilenbasiert ist. printf/ putsund co schreiben erst etwas, wenn ein Zeilenumbruch ausgegeben werden soll. Bei anderen Dateitypen erfolgt die Pufferung in Blöcken (von wenigen Kilobytes).

Es gibt mehrere Optionen , um die Pufferung zu deaktivieren , die in einer Reihe von Fragen und Antworten hier diskutiert werden (Suche nach unbuffer oder STDBUF , kann nicht Redirect Schnitt Ausgabe einige Ansätze gibt) entweder durch einen Pseudo-Terminal wie die Verwendung kann erfolgen durch socat/ script/ expect/ unbuffer(ein expectSkript) / zsh's zptyoder durch Einfügen von Code in die ausführbare Datei, um die Pufferung durch GNUs oder FreeBSDs zu deaktivieren stdbuf.

Stéphane Chazelas
quelle
1
Super Antwort, vielen Dank dafür!
Mowwwalker
Ein anderer, Linux-spezifischer Ansatz besteht darin, das /procVerzeichnis zu durchsuchen und für jedes /proc/<integer>/Verzeichnis nach einem /proc/<integer>/fd/Dateideskriptor mit derselben Inode-Nummer in pipefs serverfault.com/q/48330/363611 zu suchen. Dies ist jedoch nur in Skripten nützlich, wenn die beschriebenen Syscalls nicht verwendet werden können in Stephanes Antwort, und ist eher eine Problemumgehung als die richtige Lösung IMHO
Sergiy Kolodyazhnyy
Auf BSD lseekwird dies auf Terminals und anderen Zeichengeräten erfolgreich sein und einfach einen Zähler zurücksetzen / setzen, der bei jedem erfolgreichen Lesen () erhöht wird. Ich weiß nicht, ob dies sie "suchbar" macht.
Mosvy
@mowwwalker Wenn diese Antwort Ihr Problem gelöst hat, nehmen Sie sich bitte einen Moment Zeit und akzeptieren Sie sie, indem Sie auf das Häkchen links klicken. Dadurch wird die Frage als beantwortet markiert und auf den Stack Exchange-Sites wird der Dank ausgesprochen.
Dessert