
$ 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 9
Sie 10
hin?
$ printf '\r' | unbuffer -p od -An -w1 -vtc
\n
Warum wurde \r
es 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 sleep
ich es nicht gelesen habe)?
Übrigens auch:
$ echo test | unbuffer -p grep foo && echo found foo
found foo
Wie kommt es, grep
dass 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 od
dort die Befehle angepasst werden müssen und ich 2321 (dort alle BEL-Zeichen) statt der oben genannten 4095 erhalte).
Antwort1
unbuffer
ist 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-cmd
sich um eine Art Filter-/Transformationsbefehl handelt. other-cmd
Die Standardausgabe von wird zeilenweise gepuffert, cmd
die 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, cmd
obwohl die Standardausgabe an eine Pipe geht.
Dazu startet es cmd
in einem Pseudoterminal und leitet das, was von diesem Pseudoterminal kommt, an die Pipe weiter. Es cmd
denkt also, es spreche wieder mit einem Benutzer und führt eine Zeilenpufferung durch.
unbuffer
ist eigentlich in geschrieben expect
. Es istein Beispielskript im expect
Quellcode, oft im expect
Lieferumfang der Betriebssysteme enthalten.
expect
ist ein Tool, das zur Durchführung automatischer Interaktionen mit Terminalanwendungen unter Verwendung von Pseudoterminals verwendet wird, sodass das unbuffer
Schreiben dieses Befehls in trivial ist expect
. Scherzhaft gesagt,FEHLERDer Abschnitt der unbuffer
Manpage 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, unbuffer
wird auch eine -p
Option unterstützt.
In unbuffer cmd
ist das Pseudoterminal nicht nur mit der Standardausgabe von cmd verbunden, sondern auch mit dessen Standardeingabe und Standardderr (denken Sie daran, dass es expect
sich 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/null
die Fehler nicht an gesendet wurden /dev/null
, stderr wird mit stdout zusammengeführt.
Jetzt unbuffer
liest es nichts von seinem eigenen Standardeingang und sendet nichts für den Standardeingang von cmd
.
Das heißt, A | unbuffer cmd | B
es wird nicht funktionieren.
Hier kommt die Option -p
(für p
ipe) ins Spiel. Wie im Code zu sehen ist, verwendet mit -p
anstelle unbuffer
von interact
als expect
aktive Schleife, die die von den verschiedenen Kanälen kommenden Daten verarbeitet.
Mit der expect
Anweisung allein expect
liest (das Programm/die TCL-Bibliothek), was vom Pseudoterminal kommt (also cmd
beispielsweise, was auf der Slave-Seite über dessen Standardausgabe oder Standardausgabe geschrieben wird) und sendet es einfach an seine eigene Standardausgabe.
Mit interact
funktioniert expect
das aber auch:
- sendet das, was es von seiner eigenen Standardeingabe liest, an das Pseudoterminal (so dass
cmd
es dort gelesen werden kann) - und außerdem, wenn
unbuffer
es sich bei der Standardeingabe um ein Terminalgerät handelt,interact
wird es inraw
den Modus mitecho
deaktiviertem lokalen Gerät versetzt.
Das ist insofern gut A | unbuffer -p cmd | B
, A
als dass die Ausgabe von als Eingabe von gelesen werden kann, cmd
bedeutet aber ein paar Dinge:
unbuffer
konfiguriert das interne Pseudoterminal mitset stty_init "-echo"
, aber nicht imraw
Modus. Insbesondere sindisig
(die Handhabung von^C
(\3
) /^Z
/^\
),ixon
(Flusssteuerung,^Q
/^S
(\23
)) nicht deaktiviert. Wenn die Eingabe ein Terminalgerät ist (so sollexpect
'sinteract
verwendet werden, aber nichtunbuffer
), ist das in Ordnung, da das Hostgerät in denraw
Modus gesetzt wird, was lediglich bedeutet, dass die Verarbeitung vom Hostterminal zum eingebetteten Pseudoterminal verschoben wird, abgesehen von der Tatsache, dassecho
in 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 vonprintf '\3'
) ein SIGINT auslöst und den Befehl beendet, jedes 0x19-Byte (printf '\23'
) stoppt den Fluss.icrnl
Dass 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\n
von ausgegebenencmd
in geändert werden\r\n
. Und wenn die Eingabe ein Terminalgerät ist, erklärt die Tatsache, dass es dieses in einfügtraw
, also mitopost
deaktiviertem , die verstümmelte Terminalausgabe, wenn die von ausgegebenen Zeilenumbruchzeichenod
nicht in umgewandelt werden\r\n
.Im internen Pseudoterminal ist der Zeileneditor noch aktiviert, daher wird nichts gesendet,
cmd
es sei denn, es kommt ein\r
oder\n
Zeichen von der Eingabe, was erklärt, warumprintf foo | unbuffer -p cat
nichts 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\n
akzeptiert 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
unbuffer
dem Standardeingang von gesehen wird, kann es diese Information nicht an weiterleitencmd
. Daher wartet inseq 10 | od -vtc
, nachdemseq
es beendet wurde,od
immer noch auf weitere Eingaben vom Pseudoterminal, die nie kommen werden. Stattdessen wird an diesem Punkt alles abgebaut undod
beendet (die Manpage erwähnt diese Einschränkung).
Für seinen eigenen Zweck wäre es viel besser, unbuffer
das eingebettete Pseudo-TTY in raw -echo
den Modus zu versetzen und das Host-Terminalgerät (falls vorhanden) in Ruhe zu lassen. expect
Dieser Betriebsmodus wird jedoch nicht wirklich unterstützt, da es nicht dafür entwickelt wurde.
Wenn unbuffer
es 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 sh
werden die ursprünglichen Standardeingaben und Standardfehler wiederhergestellt (von der aufrufenden Shell über FDS 4 und 5 weitergegeben; es wird nicht FDS 3 verwendet, wie expect
dies 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.
socat
kann 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 zsh
Shell hat sogar integrierte Unterstützung für Pseudo-TTYs, und unbuffer
mit 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 socat
Ansatzes (es sei denn, Sie verwenden die Optionen ctty
und ) 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.setid
unbuffer
cmd
unbuffer 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 cat
dass nur 4096 Bytes zurückgegeben werden, da dies die gleiche Menge ist, die cat
beim 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