Warum verstümmelt unbuffer -p seine Eingabe?

Warum verstümmelt unbuffer -p seine Eingabe?
$ seq 10 | unbuffer -p od -vtc
0000000   1  \n   2  \n   3  \n   4  \n   5  \n   6  \n   7  \n   8  \n

Wo sind 9Sie 10hin?

$ printf '\r' | unbuffer -p od -An -w1 -vtc
  \n

Warum wurde \res geändert in \n?

$ : | unbuffer -p printf '\n' | od -An -w1 -vtc
  \r
  \n
$ unbuffer -p printf '\n' | od -An -w1 -vtc
  \r
      \n

Was zum Teufel?

$ printf foo | unbuffer -p cat
$

Warum keine Ausgabe (und eine Verzögerung von einer Sekunde)?

$ printf '\1\2\3foo bar\n'  | unbuffer -p od -An -w1 -vtc
$

Warum keine Ausgabe?

$ (printf '\23'; seq 10000) | unbuffer -p cat

Warum bleibt es hängen und es wird keine Ausgabe ausgegeben?

$ unbuffer -p sleep 10

Warum kann ich nicht sehen, was ich eingebe (und warum wird es verworfen, obwohl sleepich es nicht gelesen habe)?

Übrigens auch:

$ echo test | unbuffer -p grep foo && echo found foo
found foo

Wie kommt es, grepdass es gefunden wurde foo, aber die Zeilen, die es enthalten, nicht gedruckt wurden?

$ unbuffer -p ls /x 2> /dev/null
ls: cannot access '/x': No such file or directory

Warum wurde der Fehler nicht an gemeldet /dev/null?

Siehe auchPufferung aufheben und alle Zeichen in eine Glocke umwandeln?

$ echo ${(l[10000][foo])} | unbuffer -p cat | wc -c
4095

Das ist mit:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux trixie/sid
Release:        n/a
Codename:       trixie
$ uname -rsm
Linux 6.5.0-3-amd64 x86_64
$ expect -c 'puts "expect [package require Expect] tcl [info patchlevel]"'
expect 5.45.4 tcl 8.6.13
$ /proc/self/exe --version
zsh 5.9 (x86_64-debian-linux-gnu)

Dasselbe unter Ubuntu 22.04 oder unter FreeBSD 12.4-RELEASE-p5 (außer dass oddort die Befehle angepasst werden müssen und ich 2321 (dort alle BEL-Zeichen) statt der oben genannten 4095 erhalte).

Antwort1

unbufferist ein Tool zum Deaktivieren der Pufferung, die einige Befehle durchführen, wenn ihre Ausgabe nicht an ein Terminalgerät geht.

Wenn ihre Ausgabe an ein Terminalgerät gesendet wird, gehen Befehle davon aus, dass ein tatsächlicher Benutzer aktiv auf die Ausgabe schaut, und senden sie daher, sobald sie verfügbar ist. Nun, nicht genau, sie senden sie zeilenbasiert, d. h. sie senden vollständige Zeilen, sobald sie zur Ausgabe bereit sind.

Wenn es nicht an ein Terminalgerät geht, z. B. wenn stdout eine normale Datei oder eine Pipe ist, senden sie es zur Optimierung in Blöcken. Das bedeutet weniger write()s und im Fall einer Pipe bedeutet das, dass der Leser am anderen Ende nicht so oft aufgeweckt werden muss, was weniger Kontextwechsel bedeutet.

Dies bedeutet jedoch, dass in:

cmd | other-cmd

Führen Sie es in einem Terminal aus, wo es other-cmdsich um eine Art Filter-/Transformationsbefehl handelt. other-cmdDie Standardausgabe von wird zeilenweise gepuffert, cmddie von jedoch vollständig gepuffert. Dies bedeutet, dass der interaktive Benutzer die Ausgabe von cmd(wie von transformiert other-cmd) nicht sofort sieht, wenn sie verfügbar ist, sondern verzögert und in großen Stapeln.

unbuffer cmd | other-cmd

Hilft, weil es eine zeilenbasierte Pufferung wiederherstellt, cmdobwohl die Standardausgabe an eine Pipe geht.

Dazu startet es cmdin einem Pseudoterminal und leitet das, was von diesem Pseudoterminal kommt, an die Pipe weiter. Es cmddenkt also, es spreche wieder mit einem Benutzer und führt eine Zeilenpufferung durch.

unbufferist eigentlich in geschrieben expect. Es istein Beispielskript im expectQuellcode, oft im expectLieferumfang der Betriebssysteme enthalten.

expectist ein Tool, das zur Durchführung automatischer Interaktionen mit Terminalanwendungen unter Verwendung von Pseudoterminals verwendet wird, sodass das unbufferSchreiben dieses Befehls in trivial ist expect. Scherzhaft gesagt,FEHLERDer Abschnitt der unbufferManpage von enthält:Die Manpage ist länger als das Programm.Und tatsächlich, dieProgrammist nur:

#!/bin/sh
# -*- tcl -*-
# The next line is executed by /bin/sh, but not tcl \
exec tclsh8.6 "$0" ${1+"$@"}

package require Expect


# -*- tcl -*-
# Description: unbuffer stdout of a program
# Author: Don Libes, NIST

if {[string compare [lindex $argv 0] "-p"] == 0} {
    # pipeline
    set stty_init "-echo"
    eval [list spawn -noecho] [lrange $argv 1 end]
    close_on_eof -i $user_spawn_id 0
    interact {
        eof {
            # flush remaining output from child
            expect -timeout 1 -re .+
            return
        }
    }
} else {
    set stty_init "-opost"
    set timeout -1
    eval [list spawn -noecho] $argv
    expect
    exit [lindex [wait] 3]
}

Wie Sie sehen und wie durch die Manpage bestätigt wird, unbufferwird auch eine -pOption unterstützt.

In unbuffer cmdist das Pseudoterminal nicht nur mit der Standardausgabe von cmd verbunden, sondern auch mit dessen Standardeingabe und Standardderr (denken Sie daran, dass es expectsich um ein Tool handelt, das zur Interaktion mit Befehlen vorgesehen ist):

$ tty; unbuffer readlink /proc/self/fd/{0..2}
/dev/pts/14
/dev/pts/15
/dev/pts/15
/dev/pts/15

Das erklärt, warum unbuffer ls /x 2> /dev/nulldie Fehler nicht an gesendet wurden /dev/null, stderr wird mit stdout zusammengeführt.

Jetzt unbufferliest es nichts von seinem eigenen Standardeingang und sendet nichts für den Standardeingang von cmd.

Das heißt, A | unbuffer cmd | Bes wird nicht funktionieren.

Hier kommt die Option -p(für pipe) ins Spiel. Wie im Code zu sehen ist, verwendet mit -panstelle unbuffervon interactals expectaktive Schleife, die die von den verschiedenen Kanälen kommenden Daten verarbeitet.

Mit der expectAnweisung allein expectliest (das Programm/die TCL-Bibliothek), was vom Pseudoterminal kommt (also cmdbeispielsweise, was auf der Slave-Seite über dessen Standardausgabe oder Standardausgabe geschrieben wird) und sendet es einfach an seine eigene Standardausgabe.

Mit interactfunktioniert expectdas aber auch:

  • sendet das, was es von seiner eigenen Standardeingabe liest, an das Pseudoterminal (so dass cmdes dort gelesen werden kann)
  • und außerdem, wenn unbufferes sich bei der Standardeingabe um ein Terminalgerät handelt, interactwird es in rawden Modus mit echodeaktiviertem lokalen Gerät versetzt.

Das ist insofern gut A | unbuffer -p cmd | B, Aals dass die Ausgabe von als Eingabe von gelesen werden kann, cmdbedeutet aber ein paar Dinge:

  • unbufferkonfiguriert das interne Pseudoterminal mit set stty_init "-echo", aber nicht im rawModus. Insbesondere sind isig(die Handhabung von ^C( \3) / ^Z/ ^\), ixon(Flusssteuerung, ^Q/ ^S( \23)) nicht deaktiviert. Wenn die Eingabe ein Terminalgerät ist (so soll expect's interactverwendet werden, aber nicht unbuffer), ist das in Ordnung, da das Hostgerät in den rawModus gesetzt wird, was lediglich bedeutet, dass die Verarbeitung vom Hostterminal zum eingebetteten Pseudoterminal verschoben wird, abgesehen von der Tatsache, dass echoin beiden deaktiviert ist, sodass Sie nicht sehen können, was Sie eingeben. Aber wenn es kein Terminalgerät ist, bedeutet das, dass zum Beispiel jedes 0x3-Byte ( ^C) in der Eingabe (wie bei der Verarbeitung der Ausgabe von printf '\3') ein SIGINT auslöst und den Befehl beendet, jedes 0x19-Byte ( printf '\23') stoppt den Fluss. icrnlDass es nicht deaktiviert ist, erklärt, warum \r's in 's geändert werden \n.

  • Es führt nicht das aus stty -opost, was es sonst ohne tut -p. Das erklärt, warum die \nvon ausgegebenen cmdin geändert werden \r\n. Und wenn die Eingabe ein Terminalgerät ist, erklärt die Tatsache, dass es dieses in einfügt raw, also mit opostdeaktiviertem , die verstümmelte Terminalausgabe, wenn die von ausgegebenen Zeilenumbruchzeichen odnicht in umgewandelt werden \r\n.

  • Im internen Pseudoterminal ist der Zeileneditor noch aktiviert, daher wird nichts gesendet, cmdes sei denn, es kommt ein \roder \nZeichen von der Eingabe, was erklärt, warum printf foo | unbuffer -p catnichts gedruckt wird.

    Und da dieser Zeileneditor eine Begrenzung für die Größe der Zeilen hat, die er bearbeiten kann (4095 auf meinem System (Linux),ein Fünftel der TTY-Geschwindigkeit¹ unter FreeBSD), dann hat man das gleiche Problem wiePufferung aufheben und alle Zeichen in eine Glocke umwandeln?: Dasselbe passiert, wie wenn Sie versuchen, in einer einfachen Anwendung wie eine zu lange Zeile über die Tastatur einzugeben cat. Unter Linux werden alle Zeichen nach dem 4094. ignoriert , aber \nakzeptiert und die Zeile übermittelt; unter FreeBSD werden nach der Eingabe von 38400/5 Zeichen alle weiteren abgelehnt (sogar \n) und es wird ein BEL an das Terminal gesendet². Das erklärt, warum Sie dort 2321 BELs erhalten (10001 - 38400/5).

  • Die EOF-Behandlung ist bei Pseudoterminalgeräten umständlich. Wenn EOF auf unbufferdem Standardeingang von gesehen wird, kann es diese Information nicht an weiterleiten cmd. Daher wartet in seq 10 | od -vtc, nachdem seqes beendet wurde, odimmer noch auf weitere Eingaben vom Pseudoterminal, die nie kommen werden. Stattdessen wird an diesem Punkt alles abgebaut und odbeendet (die Manpage erwähnt diese Einschränkung).

Für seinen eigenen Zweck wäre es viel besser, unbufferdas eingebettete Pseudo-TTY in raw -echoden Modus zu versetzen und das Host-Terminalgerät (falls vorhanden) in Ruhe zu lassen. expectDieser Betriebsmodus wird jedoch nicht wirklich unterstützt, da es nicht dafür entwickelt wurde.

Wenn unbufferes nun darum geht, die Pufferung von stdout aufzuheben, gibt es keinen Grund, warum stdin und stderr berührt werden sollten.

Wir können das Problem folgendermaßen umgehen:

unbuffer() {
  command unbuffer sh -c 4<&0 5>&2 '
    exec <&4 4<&- 2>&5 5>&- "$@"' sh "$@"
}

Dadurch shwerden die ursprünglichen Standardeingaben und Standardfehler wiederhergestellt (von der aufrufenden Shell über FDS 4 und 5 weitergegeben; es wird nicht FDS 3 verwendet, wie expectdies bei der expliziten internen Verwendung geschieht).

Dann:

$ echo test | unbuffer readlink /proc/self/fd/{0..2} 2> /dev/null | cat
pipe:[184479]
/dev/pts/16
/dev/null

Nur die Standardausgabe wird ungepuffert an das Pseudoterminal gesendet.

Und alle anderen Probleme verschwinden:

$ unbuffer ls /x 2> /dev/null
$ printf '\r'  | unbuffer od -An -w1 -vtc
  \r
$ : | unbuffer printf '\n' | od -An -w1 -vtc
  \n
$ unbuffer printf '\n' | od -An -w1 -vtc
  \n
$ printf foo | unbuffer cat
foo
$ printf '\1\2\3foo bar\n' | unbuffer od -An -w1 -vtc
 001
 002
 003
   f
   o
   o

   b
   a
   r
  \n
$ (printf '\23'; seq 10000) | unbuffer cat -vte | head
^S1$
2$
3$
4$
5$
6$
7$
8$
9$
10$
$ unbuffer sleep 10
I see what I type
$ I see what I type
zsh: command not found: I
$ echo test | unbuffer grep foo || echo not found
not found
$ echo ${(l[10000][foo])} | unbuffer cat | wc -c
10001

Auch die Installation expect(die einen TCL-Interpreter erfordert) scheint etwas übertrieben, wenn Sie lediglich die Standardausgabe cmdüber ein Pseudoterminal durchführen müssen.

socatkann es auch tun:

$ echo test | socat -u system:'readlink /proc/self/fd/[0-2]; wc -c',pty,raw - 2> /dev/null | cat
pipe:[187759]
/dev/pts/17
/dev/null
5

(es protokolliert den Beendigungsstatus bei Fehlern, gibt den Beendigungsstatus des Befehls aber ansonsten nicht weiter).

Die zshShell hat sogar integrierte Unterstützung für Pseudo-TTYs, und unbuffermit wenig Aufwand ließe sich hier eine Funktion schreiben:

zmodload zsh/zpty
zmodload zsh/zselect
unbuffer() {
  {
    return "$(
      exec 6>&1 >&5 5>&-
      # here fds go:
      #  0,3: orig stdin
      #    1: orig stdout
      #  2,4: orig stderr
      #    5: closed
      #    6: to return argument
      zpty -b unbuffer '
        stty raw
        exec <&3 3<&- 2>&4 4>&-
        # here fds go:
        #     0: orig stdin
        #     1: pseudo unbuffering tty
        #     2: orig stderr
        # 3,4,5: closed
        #     6: to return argument
        "$@" 6>&-
        echo "$?" >&6 
      '
      fd=$REPLY
      until
        zselect -r $fd
        zpty -r unbuffer
        (( $? == 2 ))
      do
        continue
      done
    )"
  } 3<&0 4>&2 5>&1
}

Beachten Sie, dass alle diese in einem neuen Terminal und mit Ausnahme des socatAnsatzes (es sei denn, Sie verwenden die Optionen cttyund ) in einer neuen Sitzung ausgeführt werden. Wenn diese „festen“ s jetzt also im Hintergrund in der Host-Terminalsitzung gestartet werden, wird das Lesen vom Host-Terminal nicht gestoppt. Beispielsweise wird am Ende ein Hintergrundjob von Ihrem Terminal lesen, was Chaos verursacht.setidunbuffercmdunbuffer cat&


¹ Begrenzt auf 65536. DieGeschwindigkeitfür ein Pseudoterminal ist irrelevant, aber es muss eines angekündigt werden und ich habe festgestellt, dass es auf dem FreeBSD-System, auf dem ich dies getestet habe, standardmäßig 38400 ist. Da die Geschwindigkeit von der des steuernden Terminals kopiert wird expect, kann man vor dem Aufruf ein stty speed 115200(den Maximalwert, soweit ich weiß) ausführen unbuffer, um diesen Puffer zu vergrößern. Aber Sie werden vielleicht feststellen, dass Sie trotzdem nicht die volle 10000 Zeichen lange Zeile erhalten. Das istim Treibercode erklärt. Sie werden feststellen, unbuffer -p catdass nur 4096 Bytes zurückgegeben werden, da dies die gleiche Menge ist, die catbeim ersten read()Aufruf angefordert wurde, und der TTY-Treiber die gleiche Menge von der Eingabezeile zurückgegeben hataber den Rest verworfen(!). Wenn Sie durch ersetzen unbuffer -p dd bs=65536, erhalten Sie die vollständige Zeile (also bis zu 115200/5 Bytes).

² Sie können diese BELs vermeiden, indem Sie im Skript set stty_init "-echo"durch ersetzen , aber das hilft Ihnen nicht dabei, an die Daten zu gelangen.set stty_init "-echo -imaxbel"unbuffer

verwandte Informationen