
$ 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 9
e 10
foi?
$ printf '\r' | unbuffer -p od -An -w1 -vtc
\n
Por que foi \r
alterado 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 sleep
ter lido)?
Aliás, também:
$ echo test | unbuffer -p grep foo && echo found foo
found foo
Como foi grep
encontrado 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 od
comandos 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-cmd
há algum tipo de comando de filtragem/transformação, other-cmd
o stdout de é com buffer de linha, mas cmd
o 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, cmd
mesmo que seu stdout esteja indo para um canal.
Para isso, ele inicia cmd
em um pseudoterminal e encaminha o que vem desse pseudoterminal para o pipe. Então cmd
pensa que está conversando com um usuário novamente e faz o buffer de linha.
unbuffer
na verdade está escrito em expect
. Isso éum script de exemplo no expect
código-fonte, geralmente incluído no expect
pacote 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 unbuffer
comando é trivial para escrever expect
. Brincando, oINSETOSseção da unbuffer
pá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, unbuffer
também suporta uma -p
opçã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 expect
que é 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/null
não enviou os erros para /dev/null
, stderr foi mesclado com stdout.
Agora, unbuffer
não lê nada do seu próprio stdin e não envia nada para o stdin do cmd
.
Isso significa que A | unbuffer cmd | B
não vai funcionar.
É aí que entra a opção -p
(for p
ipe). Como visto no código, with -p
, unbuffer
usa interact
em vez de expect
como o loop ativo que processa os dados provenientes dos diferentes canais.
Somente com a expect
instrução, expect
(o programa/biblioteca TCL) lê o que vem do pseudoterminal (ou seja, o que cmd
escreve no lado escravo por meio de seu stdout ou stderr, por exemplo) e apenas envia para seu próprio stdout.
Com interact
, expect
faz isso, mas também:
- envia o que lê de sua própria entrada padrão para o pseudo terminal (para que
cmd
possa ler lá) - e também, se
unbuffer
stdin for um dispositivo terminal,interact
coloca-o noraw
modo com localecho
desabilitado.
Isso é bom porque a saída de in A | unbuffer -p cmd | B
, A
pode ser lida como entrada, cmd
mas significa algumas coisas:
unbuffer
configura o pseudoterminal interno comset stty_init "-echo"
, mas não noraw
modo. 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 é comoexpect
'sinteract
deve ser usado, mas nãounbuffer
), tudo bem, pois o host é colocado emraw
modo, o que significa apenas que o processamento é movido do terminal host para o pseudo-incorporado. terminal, exceto pelo fato deecho
estar 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 deprintf '\3'
) aciona um SIGINT e encerra o comando, qualquer byte 0x19 (printf '\23'
) interrompe o fluxo.icrnl
não estar desabilitado explica por que\r
's foram alterados para\n
's.Ele não faz o
stty -opost
que faria sem-p
. Isso explica por que a\n
saída de bycmd
foi alterada para\r\n
. E quando a entrada é um dispositivo terminal, o fato de colocá-lo emraw
, portanto, comopost
desativado explica a saída do terminal desconfigurada quando os caracteres de nova linha gerados porod
não são transformados em\r\n
.o pseudo-terminal interno ainda tem o editor de linha habilitado, então nada será enviado
cmd
a menos que haja um caractere\r
or\n
vindo da entrada, o que explica por queprintf foo | unbuffer -p cat
nã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\n
sã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
unbuffer
stdin, ele não pode encaminhar essas informações paracmd
. Então inseq 10 | od -vtc
, depois deseq
terminar,od
ainda está aguardando mais entradas do pseudoterminal que nunca chegarão. Em vez disso, nesse ponto, tudo é demolido eod
eliminado (a página de manual menciona essa limitação).
Para seu próprio propósito, seria muito melhor unbuffer
colocar o pseudo-tty incorporado no raw -echo
modo e deixar o dispositivo terminal host (se houver) sozinho. No entanto, expect
não suporta realmente esse modo de operação, não foi projetado para isso.
Agora, se unbuffer
se 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 sh
para restaurar o stdin e o stderr originais (transmitidos pelo shell de chamada via fds 4 e 5; não usando fd 3 como expect
acontece 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 cmd
passe por um pseudo-terminal.
socat
també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 zsh
shell ainda possui suporte integrado para pseudo-ttys, e uma unbuffer
funçã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 socat
abordagem (a menos que você use as opções ctty
e setid
) em uma nova sessão. Portanto, agora, se esses s "fixos" unbuffer
forem iniciados em segundo plano na sessão do terminal host, a cmd
leitura 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 unbuffer
para 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 cat
retornos de apenas 4.096 bytes porque isso é o máximo cat
solicitado 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 unbuffer
script, mas isso não ajudará você a obter os dados.