
$ seq 10 | unbuffer -p od -vtc
0000000 1 \n 2 \n 3 \n 4 \n 5 \n 6 \n 7 \n 8 \n
¿Adónde fue 9
y 10
fue?
$ printf '\r' | unbuffer -p od -An -w1 -vtc
\n
¿Por qué se \r
cambió a \n
?
$ : | unbuffer -p printf '\n' | od -An -w1 -vtc
\r
\n
$ unbuffer -p printf '\n' | od -An -w1 -vtc
\r
\n
¿Qué carajo?
$ printf foo | unbuffer -p cat
$
¿Por qué no hay salida (y un retraso de un segundo)?
$ printf '\1\2\3foo bar\n' | unbuffer -p od -An -w1 -vtc
$
¿Por qué no hay salida?
$ (printf '\23'; seq 10000) | unbuffer -p cat
¿Por qué se bloquea sin salida?
$ unbuffer -p sleep 10
¿Por qué no puedo ver lo que escribo (y por qué se descarta aunque sleep
no lo leí)?
Por cierto, también:
$ echo test | unbuffer -p grep foo && echo found foo
found foo
¿Cómo es que grep
lo encontré foo
pero no imprimí las líneas que lo contienen?
$ unbuffer -p ls /x 2> /dev/null
ls: cannot access '/x': No such file or directory
¿Por qué no apareció el error /dev/null
?
Ver también¿Unbuffer convierte todos los personajes en campana?
$ echo ${(l[10000][foo])} | unbuffer -p cat | wc -c
4095
Eso es con:
$ 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)
Lo mismo en Ubuntu 22.04 o en FreeBSD 12.4-RELEASE-p5 (excepto que los od
comandos deben adaptarse allí y obtengo 2321 (todos los caracteres BEL allí) en lugar de 4095 arriba).
Respuesta1
unbuffer
es una herramienta para deshabilitar el almacenamiento en búfer que realizan algunos comandos cuando su salida no va a un dispositivo terminal.
Cuando su salida va a un dispositivo terminal, los comandos asumen que hay un usuario real mirando activamente la salida, por lo que la envían tan pronto como está disponible. Bueno, no exactamente, lo envían basado en líneas, es decir, envían líneas completas tan pronto como están listas para salir.
Cuando no va a un dispositivo terminal, como cuando la salida estándar es un archivo normal o una tubería, como optimización, lo envían en bloques. Eso significa menos write()
mensajes de correo electrónico y, en el caso de una tubería, significa que no es necesario despertar al lector en el otro extremo con tanta frecuencia, lo que significa menos cambios de contexto.
Sin embargo eso significa que en:
cmd | other-cmd
se ejecuta en una terminal, donde other-cmd
hay algún tipo de comando de filtrado/transformación, other-cmd
la salida estándar de 'tiene un búfer de línea, pero cmd
la de 'tiene un búfer completo, lo que significa que el usuario interactivo no ve la salida de cmd
(tal como la transforma other-cmd
) tan pronto ya que está disponible pero con retraso y en grandes lotes.
unbuffer cmd | other-cmd
Ayuda porque restaura un almacenamiento en búfer basado en líneas cmd
aunque su salida estándar vaya a una tubería.
Para hacer eso, comienza cmd
en un pseudoterminal y reenvía lo que proviene de ese pseudoterminal a la tubería. Entonces cmd
piensa que está hablando con un usuario nuevamente y almacena la línea en el búfer.
unbuffer
en realidad está escrito en expect
. Esun script de ejemplo en el expect
código fuente, a menudo incluido en el expect
paquete suministrado por los sistemas operativos.
expect
es una herramienta que se utiliza para realizar interacciones automáticas con aplicaciones de terminal utilizando pseudo-terminales, por lo que unbuffer
es trivial escribir ese comando expect
. En broma, elINSECTOSLa sección de unbuffer
la página de manual tiene:La página de manual es más larga que el programa.Y efectivamente, elprogramaes solo:
#!/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]
}
Como puede ver y según lo confirmado por la página de manual, unbuffer
también admite una -p
opción.
En unbuffer cmd
, el pseudo-terminal no solo está conectado a la salida estándar de cmd, sino que también está conectado a su entrada estándar y a su stderr (recuerde expect
que es una herramienta destinada a interactuar con comandos):
$ tty; unbuffer readlink /proc/self/fd/{0..2}
/dev/pts/14
/dev/pts/15
/dev/pts/15
/dev/pts/15
Eso explica por qué unbuffer ls /x 2> /dev/null
no se enviaron los errores /dev/null
, stderr está fusionado con stdout.
Ahora, unbuffer
no lee nada de su propia entrada estándar y no envía nada para la entrada estándar de cmd
.
Eso significa A | unbuffer cmd | B
que no funcionará.
Ahí es donde entra en juego la opción -p
(for ipe). Como se ve en el código, with , utiliza en lugar de como bucle activo que procesa los datos provenientes de los diferentes canales.p
-p
unbuffer
interact
expect
expect
Solo con la declaración, expect
(el programa/biblioteca TCL) lee lo que proviene del pseudo-terminal (es decir, lo que cmd
escribe en el lado esclavo a través de su salida estándar o stderr, por ejemplo), y simplemente lo envía a su propia salida estándar.
Con él interact
hace expect
eso pero también:
- envía lo que lee desde su propia entrada estándar al pseudo terminal (para
cmd
poder leerlo allí) - y además, si
unbuffer
la entrada estándar es un dispositivo terminal,interact
lo pone enraw
modo con localecho
deshabilitado.
Eso es bueno porque A | unbuffer -p cmd | B
la A
salida de in se puede leer como entrada, cmd
pero significa algunas cosas:
unbuffer
configura el pseudo-terminal interno conset stty_init "-echo"
, pero no enraw
modo. En particular,isig
(el manejo de^C
(\3
) //^Z
)^\
,ixon
(control de flujo,^Q
/^S
(\23
)) no están deshabilitados. Cuando la entrada es un dispositivo terminal (que es como se debe usarexpect
's , pero no ), está bien ya que el host se pone en modo, lo que solo significa que el procesamiento se mueve desde el terminal host al pseudo-incrustado. terminal, excepto por el hecho de que está deshabilitado en ambos por lo que no puedes ver lo que escribes. Pero cuando no es un dispositivo terminal, eso significa que, por ejemplo, cualquier byte 0x3 ( ) en la entrada (como cuando se procesa la salida de ) activa un SIGINT y finaliza el comando, cualquier byte 0x19 ( ) detiene el flujo. no estar deshabilitado explica por qué los 's se cambian a 's.interact
unbuffer
raw
echo
^C
printf '\3'
printf '\23'
icrnl
\r
\n
No hace lo
stty -opost
que de otro modo prescindiría-p
. Eso explica por qué la\n
salida decmd
se cambia a\r\n
. Y cuando la entrada es un dispositivo terminal, el hecho de que lo coloque enraw
, por lo que conopost
deshabilitado explica la salida del terminal destrozada cuando los caracteres de nueva línea emitidos porod
no se transforman en\r\n
.el pseudo-terminal interno todavía tiene el editor de líneas habilitado, por lo que no se enviará nada
cmd
a menos que haya un carácter\r
o\n
proveniente de la entrada, lo que explica por quéprintf foo | unbuffer -p cat
no imprime nada.Y dado que ese editor de líneas tiene un límite en el tamaño de la línea, puede editar (4095 en mi sistema (Linux),una quinta parte de la velocidad tty¹ en FreeBSD), terminas con el tipo de problema en¿Unbuffer convierte todos los personajes en campana?: sucede lo mismo que cuando intentas ingresar una línea demasiado larga en el teclado en una aplicación tonta como
cat
. En Linux, todos los caracteres después del 4094 se ignoran, pero\n
se aceptan y envían la línea; en FreeBSD, después de ingresar 38400/5 caracteres, cualquier exceso se rechaza (incluso\n
) y provoca que se envíe un BEL al terminal². Lo que explica por qué obtienes 2321 BEL allí (10001 - 38400/5).El manejo de EOF es complicado con dispositivos pseudoterminales. Cuando se ve EOF en
unbuffer
la entrada estándar de , no puede reenviar esa información acmd
. Entoncesseq 10 | od -vtc
, después deseq
haber terminado,od
todavía está esperando más información del pseudo-terminal que nunca llegará. En cambio, en ese punto, todo se derriba yod
se elimina (la página de manual menciona esa limitación).
Para su propio propósito, sería mucho mejor si unbuffer
pusiera el pseudo-tty integrado en raw -echo
modo y dejara el dispositivo terminal host (si lo hubiera) solo. Sin embargo, expect
en realidad no es compatible con ese modo de operación, no ha sido diseñado para eso.
Ahora, si unbuffer
se trata de eliminar el búfer de la salida estándar, no hay ninguna razón por la que deba tocar stdin y stderr.
De hecho, podemos solucionarlo haciendo:
unbuffer() {
command unbuffer sh -c 4<&0 5>&2 '
exec <&4 4<&- 2>&5 5>&- "$@"' sh "$@"
}
Eso se usa sh
para restaurar el stdin y stderr originales (transmitidos por el shell de llamada a través de fds 4 y 5; sin usar fd 3 como expect
ocurre con el uso explícito de ese internamente).
Entonces:
$ echo test | unbuffer readlink /proc/self/fd/{0..2} 2> /dev/null | cat
pipe:[184479]
/dev/pts/16
/dev/null
Solo la salida estándar va al pseudoterminal para eliminar el búfer.
Y todos los demás problemas desaparecen:
$ 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
Además, la instalación expect
(que requiere un intérprete TCL) parece un poco exagerada cuando todo lo que necesita es realizar la salida estándar cmd
a través de un pseudo-terminal.
socat
también puedes hacerlo:
$ 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
(Registra el estado de salida fallido, pero por lo demás no propaga el estado de salida del comando).
El zsh
shell incluso tiene soporte incorporado para pseudo-ttys, y unbuffer
se podría escribir una función con poco esfuerzo con:
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
}
Tenga cuidado con todos aquellos que terminan ejecutándose en una nueva terminal y, excepto el socat
enfoque (a menos que use las opciones ctty
y setid
) en una nueva sesión. Entonces, si esos unbuffer
mensajes de correo electrónico "arreglados" se inician en segundo plano en la sesión del terminal host, cmd
no se detendrá la lectura desde el terminal host. Por ejemplo, unbuffer cat&
terminará con un trabajo en segundo plano leyendo desde su terminal, causando estragos.
¹ Limitado a 65536. Elvelocidadpara un pseudo-terminal es irrelevante, pero tiene que haber uno anunciado y encuentro que es 38400 por defecto en el sistema FreeBSD en el que lo probé. Como la velocidad se copia de la del terminal que controla expect
, se puede hacer un stty speed 115200
(el valor máximo AFAICT) antes de llamar unbuffer
para ampliar ese búfer. Pero es posible que aún no obtenga la línea grande completa de 10000 caracteres. Eso esexplicado en el código del conductor. Encontrará unbuffer -p cat
que devuelve solo 4096 bytes porque es la cantidad cat
solicitada en su primera read()
llamada y el controlador tty devolvió la misma cantidad desde la línea de entrada.pero descarto el resto(!). Si reemplaza con unbuffer -p dd bs=65536
, obtendrá la línea completa (bueno, hasta 115200/5 bytes).
² puede evitar esos BEL reemplazándolos set stty_init "-echo"
con set stty_init "-echo -imaxbel"
en el unbuffer
script, pero eso no lo ayudará a obtener los datos.