Por que unbuffer -p altera sua entrada?

Por que unbuffer -p altera sua entrada?
$ seq 10 | unbuffer -p od -vtc
0000000   1  \n   2  \n   3  \n   4  \n   5  \n   6  \n   7  \n   8  \n

Para onde foi 9e 10foi?

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

Por que foi \ralterado para \n?

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

O que é isso?

$ printf foo | unbuffer -p cat
$

Por que nenhuma saída (e um atraso de um segundo)?

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

Por que nenhuma saída?

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

Por que ele trava sem saída?

$ unbuffer -p sleep 10

Por que não consigo ver o que digito (e por que é descartado mesmo sem sleepter lido)?

Aliás, também:

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

Como foi grepencontrado foo, mas não imprimiu as linhas que o contêm?

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

Por que o erro não foi para /dev/null?

Veja tambémUnbuffer convertendo todos os personagens em sino?

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

Isso é com:

$ 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)

O mesmo no Ubuntu 22.04 ou no FreeBSD 12.4-RELEASE-p5 (exceto que os odcomandos precisam ser adaptados lá, e eu recebo 2321 (todos os caracteres BEL lá) em vez de 4095 acima).

Responder1

unbufferé uma ferramenta para desabilitar o buffer que alguns comandos fazem quando sua saída não vai para um dispositivo terminal.

Quando a saída vai para um dispositivo terminal, os comandos assumem que há um usuário real olhando ativamente para a saída, então eles a enviam assim que ela estiver disponível. Bem, não exatamente, eles enviam com base em linhas, ou seja, enviam linhas concluídas assim que estiverem prontas para saída.

Quando não vai para um dispositivo terminal, como quando stdout é um arquivo normal ou um pipe, como otimização, eles o enviam em blocos. Isso significa menos write()s e, no caso de um pipe, significa que o leitor do outro lado não precisa ser acordado com tanta frequência, o que significa menos mudanças de contexto.

No entanto, isso significa que em:

cmd | other-cmd

executado em um terminal, onde other-cmdhá algum tipo de comando de filtragem/transformação, other-cmdo stdout de é com buffer de linha, mas cmdo de é com buffer completo, o que significa que o usuário interativo não vê a saída de cmd(conforme transformada por other-cmd) assim que pois está disponível, mas atrasado e em grandes lotes.

unbuffer cmd | other-cmd

Ajuda porque restaura um buffer baseado em linha, cmdmesmo que seu stdout esteja indo para um canal.

Para isso, ele inicia cmdem um pseudoterminal e encaminha o que vem desse pseudoterminal para o pipe. Então cmdpensa que está conversando com um usuário novamente e faz o buffer de linha.

unbufferna verdade está escrito em expect. Isso éum script de exemplo no expectcódigo-fonte, geralmente incluído no expectpacote fornecido pelos sistemas operacionais.

expecté uma ferramenta usada para realizar interações automáticas com aplicativos de terminal usando pseudo-terminais, de modo que o unbuffercomando é trivial para escrever expect. Brincando, oINSETOSseção da unbufferpágina de manual de tem:A página de manual é mais longa que o programa.E de fato, oprogramaé apenas:

#!/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 você pode ver e confirmado pela página de manual, unbuffertambém suporta uma -popção.

No unbuffer cmd, o pseudoterminal não está conectado apenas ao stdout do cmd, mas também ao seu stdin e stderr (lembre-se expectque é uma ferramenta destinada a interagir com comandos):

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

Isso explica por que unbuffer ls /x 2> /dev/nullnão enviou os erros para /dev/null, stderr foi mesclado com stdout.

Agora, unbuffernão lê nada do seu próprio stdin e não envia nada para o stdin do cmd.

Isso significa que A | unbuffer cmd | Bnão vai funcionar.

É aí que entra a opção -p(for pipe). Como visto no código, with -p, unbufferusa interactem vez de expectcomo o loop ativo que processa os dados provenientes dos diferentes canais.

Somente com a expectinstrução, expect(o programa/biblioteca TCL) lê o que vem do pseudoterminal (ou seja, o que cmdescreve no lado escravo por meio de seu stdout ou stderr, por exemplo) e apenas envia para seu próprio stdout.

Com interact, expectfaz isso, mas também:

  • envia o que lê de sua própria entrada padrão para o pseudo terminal (para que cmdpossa ler lá)
  • e também, se unbufferstdin for um dispositivo terminal, interactcoloca-o no rawmodo com local echodesabilitado.

Isso é bom porque a saída de in A | unbuffer -p cmd | B, Apode ser lida como entrada, cmdmas significa algumas coisas:

  • unbufferconfigura o pseudoterminal interno com set stty_init "-echo", mas não no rawmodo. Em particular, isig(o tratamento de ^C( \3) / ^Z/ ^\), ixon(controle de fluxo, ^Q/ ^S( \23)) não estão desabilitados. Quando a entrada é um dispositivo terminal (que é como expect's interactdeve ser usado, mas não unbuffer), tudo bem, pois o host é colocado em rawmodo, o que significa apenas que o processamento é movido do terminal host para o pseudo-incorporado. terminal, exceto pelo fato de echoestar desabilitado em ambos para que você não possa ver o que digita. Mas quando não é um dispositivo terminal, isso significa que, por exemplo, qualquer byte 0x3 ( ^C) na entrada (como ao processar a saída de printf '\3') aciona um SIGINT e encerra o comando, qualquer byte 0x19 ( printf '\23') interrompe o fluxo. icrnlnão estar desabilitado explica por que \r's foram alterados para \n's.

  • Ele não faz o stty -opostque faria sem -p. Isso explica por que a \nsaída de by cmdfoi alterada para \r\n. E quando a entrada é um dispositivo terminal, o fato de colocá-lo em raw, portanto, com opostdesativado explica a saída do terminal desconfigurada quando os caracteres de nova linha gerados por odnão são transformados em \r\n.

  • o pseudo-terminal interno ainda tem o editor de linha habilitado, então nada será enviado cmda menos que haja um caractere \ror \nvindo da entrada, o que explica por que printf foo | unbuffer -p catnão imprime nada.

    E como esse editor de linha tem um limite no tamanho da linha, pode-se editar (4095 no meu sistema (Linux),um quinto da velocidade tty¹ no FreeBSD), você acaba com o tipo de problema emUnbuffer convertendo todos os personagens em sino?: acontece a mesma coisa que quando você tenta inserir uma linha muito longa no teclado em um aplicativo idiota como cat. No Linux, todos os caracteres após o 4094 são ignorados, mas \nsão aceitos e submetem a linha; no FreeBSD, após a inserção de 38400/5 caracteres, qualquer extra é recusado (mesmo \n) e faz com que um BEL seja enviado ao terminal². O que explica por que você obtém 2.321 BELs lá (10.001 - 38.400/5).

  • O manuseio de EOF é complicado com dispositivos pseudoterminais. Quando EOF é visto no unbufferstdin, ele não pode encaminhar essas informações para cmd. Então in seq 10 | od -vtc, depois de seqterminar, odainda está aguardando mais entradas do pseudoterminal que nunca chegarão. Em vez disso, nesse ponto, tudo é demolido e odeliminado (a página de manual menciona essa limitação).

Para seu próprio propósito, seria muito melhor unbuffercolocar o pseudo-tty incorporado no raw -echomodo e deixar o dispositivo terminal host (se houver) sozinho. No entanto, expectnão suporta realmente esse modo de operação, não foi projetado para isso.

Agora, se unbufferse trata de remover stdout do buffer, não há razão para que ele toque em stdin e stderr.

Na verdade, podemos contornar isso fazendo:

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

Isso é usado shpara restaurar o stdin e o stderr originais (transmitidos pelo shell de chamada via fds 4 e 5; não usando fd 3 como expectacontece com o uso explícito daquele internamente).

Então:

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

Apenas o stdout vai para o pseudo-terminal para ser removido do buffer.

E todos os outros problemas desaparecem:

$ 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

Além disso, a instalação expect(que requer um interpretador TCL) parece um pouco exagerada quando tudo o que você precisa é fazer com que o stdout cmdpasse por um pseudo-terminal.

socattambém pode fazer isso:

$ 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

(ele registra o status de saída de falha, mas por outro lado não propaga o status de saída do comando).

O zshshell ainda possui suporte integrado para pseudo-ttys, e uma unbufferfunção pode ser escrita com pouco esforço com:

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
}

Cuidado, todos eles acabam rodando em um novo terminal e exceto pela socatabordagem (a menos que você use as opções cttye setid) em uma nova sessão. Portanto, agora, se esses s "fixos" unbufferforem iniciados em segundo plano na sessão do terminal host, a cmdleitura do terminal host não será interrompida. Por exemplo, unbuffer cat&acabará com uma leitura de trabalho em segundo plano em seu terminal, causando estragos.


¹ Limitado a 65536. Ovelocidadepara um pseudo-terminal é irrelevante, mas deve haver um anunciado e acho que é 38400 por padrão no sistema FreeBSD em que testei. Como a velocidade é copiada daquela do terminal que controla expect, pode-se fazer um stty speed 115200(o valor máximo AFAICT) antes de chamar unbufferpara ampliar esse buffer. Mas você pode descobrir que ainda não obteve a linha grande completa de 10.000 caracteres. Isso éexplicado no código do driver. Você encontrará unbuffer -p catretornos de apenas 4.096 bytes porque isso é o máximo catsolicitado em sua primeira read()chamada, e o driver tty retornou a mesma quantidade da linha de entradamas descartou o resto(!). Se você substituir por unbuffer -p dd bs=65536, obterá a linha completa (bem, até 115200/5 bytes).

² você pode evitar esses BELs substituindo set stty_init "-echo"por set stty_init "-echo -imaxbel"no unbufferscript, mas isso não ajudará você a obter os dados.

informação relacionada