¿Por qué unbuffer -p altera su entrada?

¿Por qué unbuffer -p altera su entrada?
$ 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 9y 10fue?

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

¿Por qué se \rcambió 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 sleepno lo leí)?

Por cierto, también:

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

¿Cómo es que greplo encontré foopero 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 odcomandos deben adaptarse allí y obtengo 2321 (todos los caracteres BEL allí) en lugar de 4095 arriba).

Respuesta1

unbufferes 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-cmdhay algún tipo de comando de filtrado/transformación, other-cmdla salida estándar de 'tiene un búfer de línea, pero cmdla 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 cmdaunque su salida estándar vaya a una tubería.

Para hacer eso, comienza cmden un pseudoterminal y reenvía lo que proviene de ese pseudoterminal a la tubería. Entonces cmdpiensa que está hablando con un usuario nuevamente y almacena la línea en el búfer.

unbufferen realidad está escrito en expect. Esun script de ejemplo en el expectcódigo fuente, a menudo incluido en el expectpaquete suministrado por los sistemas operativos.

expectes una herramienta que se utiliza para realizar interacciones automáticas con aplicaciones de terminal utilizando pseudo-terminales, por lo que unbufferes trivial escribir ese comando expect. En broma, elINSECTOSLa sección de unbufferla 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, unbuffertambién admite una -popció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 expectque 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/nullno se enviaron los errores /dev/null, stderr está fusionado con stdout.

Ahora, unbufferno 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 | Bque 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-punbufferinteractexpect

expectSolo con la declaración, expect(el programa/biblioteca TCL) lee lo que proviene del pseudo-terminal (es decir, lo que cmdescribe 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 interacthace expecteso pero también:

  • envía lo que lee desde su propia entrada estándar al pseudo terminal (para cmdpoder leerlo allí)
  • y además, si unbufferla entrada estándar es un dispositivo terminal, interactlo pone en rawmodo con local echodeshabilitado.

Eso es bueno porque A | unbuffer -p cmd | Bla Asalida de in se puede leer como entrada, cmdpero significa algunas cosas:

  • unbufferconfigura el pseudo-terminal interno con set stty_init "-echo", pero no en rawmodo. 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 usar expect'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.interactunbufferrawecho^Cprintf '\3'printf '\23'icrnl\r\n

  • No hace lo stty -opostque de otro modo prescindiría -p. Eso explica por qué la \nsalida de cmdse cambia a \r\n. Y cuando la entrada es un dispositivo terminal, el hecho de que lo coloque en raw, por lo que con opostdeshabilitado explica la salida del terminal destrozada cuando los caracteres de nueva línea emitidos por odno 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 cmda menos que haya un carácter \ro \nproveniente de la entrada, lo que explica por qué printf foo | unbuffer -p catno 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 \nse 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 unbufferla entrada estándar de , no puede reenviar esa información a cmd. Entonces seq 10 | od -vtc, después de seqhaber terminado, odtodavía está esperando más información del pseudo-terminal que nunca llegará. En cambio, en ese punto, todo se derriba y odse elimina (la página de manual menciona esa limitación).

Para su propio propósito, sería mucho mejor si unbufferpusiera el pseudo-tty integrado en raw -echomodo y dejara el dispositivo terminal host (si lo hubiera) solo. Sin embargo, expecten realidad no es compatible con ese modo de operación, no ha sido diseñado para eso.

Ahora, si unbufferse 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 shpara 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 expectocurre 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 cmda través de un pseudo-terminal.

socattambié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 zshshell incluso tiene soporte incorporado para pseudo-ttys, y unbufferse 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 socatenfoque (a menos que use las opciones cttyy setid) en una nueva sesión. Entonces, si esos unbuffermensajes de correo electrónico "arreglados" se inician en segundo plano en la sesión del terminal host, cmdno 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 unbufferpara 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 catque devuelve solo 4096 bytes porque es la cantidad catsolicitada 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 unbufferscript, pero eso no lo ayudará a obtener los datos.

información relacionada