Existe uma maneira de ler a saída do console de um comando sem redirecionar ou canalizar seu STDOUT/STDERR?
O problema com redirecionamentos ou pipes é que alguns comandos se comportam de forma diferente quando seus STDOUT e/ou STDERR são redirecionados, por exemplo, cor ou algum formato é removido, ou diferenças mais relevantes. Versões mais tput
antigas requerem STDOUT ou STDERR em um console regular para ler suas dimensões. No caso de pipes, adicionalmente, o comando originating perde a capacidade de controlar o shell de origem, por exemplo, exit
um script de dentro de uma função, que tem sua saída canalizada, não é possível.
O que eu quero conseguir é executar um comando para que ele imprima sua saída no console diretamente, seja capaz de matar/sair do shell, mas analisar/manipular e, no caso, registrar sua saída. tee
seria uma solução óbvia, mas sofre dos problemas mencionados.
Fiz algumas tentativas com read
, executando um loop que tenta ler os descritores de arquivo do comando ou /dev/stdout
ou /dev/tty
, mas em todos os casos, nem uma única linha da saída do comando mostrado é realmente read
.
Por exemplo
#!/bin/bash
apt update 2>&1 & pid=$!
while [[ -f /proc/$pid/fd/1 ]] && read -r line
do
echo "$line" >> ./testfile
done < /proc/$pid/fd/1
mas também executando o loop while em segundo plano e o comando em primeiro plano (IMO preferível), como:
#!/bin/bash
while read -r line
do
echo "$line" >> ./testfile
done < /dev/tty & pid=$!
apt update
kill $pid
mas em ambos os casos ./testfile
permanece vazio.
/dev/stdout
é o próprio descritor de arquivo STDOUT de cada processo, que não pode funcionar, é claro.
Provavelmente alguém tem uma idéia de como conseguir isso ou uma alternativa semelhante?
Nota preliminar
Nesta resposta, uso o termo "tty" para qualquer dispositivo terminal, geralmente para pts .
Abordagem simples
Existem ferramentas que você pode usar para capturar a saída de um comando enquanto ainda fornece um tty para ele:
unbuffer
,script
,expect
,screen
,tmux
,ssh
, possivelmente outros.Para um único comando externo, isso é relativamente fácil. Pegue
ls --color=auto
. Esta tentativa:"sofre dos problemas mencionados", como você notou. Mas isso:
imprime muito bem saídas coloridas e em colunas e armazena uma cópia completa no formato
./log
. Assim faz isso:embora neste caso haja um cabeçalho e um rodapé no arquivo, você pode gostar ou não.
Eu não vou detalhar
expect
,screen
outmux
. Como último recurso (quando nenhuma outra ferramenta estiver disponível), pode-se usarssh
depois de configurar o acesso SSH sem senha do localhost para si mesmo. Algo assim:(
${var@Q}
expande para o valor devar
quote em um formato que pode ser reutilizado como entrada; perfeito aqui. O shell que é executadossh
deve ser Bash, a sintaxe não é portátil.)unbuffer
parece a solução mais simples. O nome sugere que seu objetivo principal é desabilitar o buffer, mas cria um pseudo-terminal .Complicações
Você deseja capturar a saída também de uma função de shell, sem perder sua conexão com o shell principal que interpreta o script. Para isso a função deve ser executada no shell principal, a abordagem simples acima com uma ferramenta que executa algum comando externo não pode ser usada, a menos que o comando externo seja o script inteiro:
Obviamente, essa solução não é intrínseca ao script. Acho que você quer simplesmente executar
./the-script
e capturar a saída à medida que ela vai para o terminal. Portanto, o script precisa criar um tty "capturável" para si mesmo de alguma forma. Esta é a parte complicada.Solução possível
Uma possível solução é executar
e redirecionar descritores de arquivo
1
e (opcionalmente)2
do shell principal para o tty criado parasomething
.something
deve sentar-se silenciosamente e não fazer (quase) nada.Vantagens:
unbuffer … | tee … &
descritores de arquivo e fazer malabarismos para registrar a saída de diferentes partes do script em arquivos diferentes.Desvantagens:
O script deve
kill
unbuffer
ousomething
quando o registro não for mais necessário. Deve fazer isso quando sair normalmente ou por causa de um sinal. Se for morto à força, não será capaz de fazer isso. Talvezsomething
devesse verificar periodicamente se o processo principal ainda está lá e sair eventualmente. Há uma solução bacana comflock
(veja abaixo).something
precisa relatar seu tty para o shell principal de alguma forma. Apenas imprimir a saída detty
é uma possibilidade, o shell principal seria aberto./log
independentemente e recuperaria as informações. Depois disso, é apenas lixo no arquivo (e na tela do terminal original). O script pode truncar o arquivo, isso só funcionará comtee -a
(porquetee -a
vstee
é como>>
vs>
nesta minha resposta ). É melhorsomething
passar a informação por um canal separado: um arquivo temporário ou um fifo nomeado criado apenas para isso.Prova de conceito
O código a seguir precisa estar
unbuffer
associado aexpect
(no Debian:expect
pacote) eflock
(no Debian:util-linux
pacote).Notas:
save-stdout-stderr
erestore-stdout-stderr
use valores codificados7
e8
. Você não deve usar esses descritores para mais nada. Reconstrua essa funcionalidade, se necessário.create-logging-tty 21 ./log
é uma solicitação para criar um descritor de arquivo21
(número arbitrário) que seria um tty registrado em./log
(nome de caminho arbitrário). A função deve ser chamada do shell principal (não de um subshell) porque deve criar um descritor de arquivo para o shell principal.create-logging-tty
usaeval
para criar um descritor de arquivo com o número solicitado.eval
pode ser mau , mas aqui é seguro , a menos que você passe algum código de shell infeliz (ou desonesto) em vez de um número. A função não verifica se seu argumento é um número. É seu trabalho certificar-se de que é (ou adicionar um teste adequado).Em geral, não há tratamento de erros no exemplo, então talvez você queira adicionar alguns. Há
return 1
quando a função não pode obter um caminho para o tty recém-criado via fifo; ainda este status de saída da função não é tratado no código principal. Corrija isso e muito mais por conta própria. Em particular, você pode querer testar se o descritor desejado realmente leva a um tty ([ -t 21 ]
) antes de redirecionar qualquer coisa para ele.create-logging-tty
usa a{variable}<>…
sintaxe para criar um descritor de arquivo temporário, onde o shell escolhe um número não utilizado (10 ou maior) para ele e atribui o número ao arquivovariable
. Para garantir que isso não leve o número solicitado puramente por acaso, a função cria primeiro um descritor de arquivo com o número solicitado, antes de saber o tty para o qual o descritor deve eventualmente apontar. Com efeito, você pode solicitar qualquer número sensato e os internos da função não colidirão com nada.Se todo o seu script usa a
{variable}<>…
sintaxe ou similar, você pode não gostar da ideia de um número codificado como 21. Isso pode ser facilmente resolvido:Inside
unbuffer
tty
(comando) é usado para obter o caminho para tty fornecido porunbuffer
. Relata formalmentetty
seu stdin, gostaríamos de saber seu stdout. Não importa porque ambos apontam para o mesmo tty.Graças a
flock
não há necessidade de matarunbuffer
ou a casca que gera. É assim que funciona:save-stdout-stderr
bloqueia o fifo que ele criou, ele usa um descritor aberto para o fifo para isso. Notas:bash
) interpretando todo o script mantém o bloqueio.unbuffer
fará.unbuffer
reporta seu tty através do fifo e então ele tenta bloquear o fifo usando seu próprio descritor de arquivo 3. O ponto éflock
bloquear até obter o bloqueio.flock
underunbuffer
não está mais bloqueado. A execução vai para o segundoflock
que tenta travar o tty e os blocos.destroy-logging-tty
.flock
sobunbuffer
desbloqueia. O shell lá sai (liberando seus bloqueios automaticamente),unbuffer
destrói o tty e sai,tee
sai. Nenhuma manutenção é necessária.Se não bloquearmos o fifo, mas deixarmos o shell
unbuffer
travar o tty imediatamente, pode acontecer que ele obtenha o bloqueio antes do shell principal, então ele termina imediatamente. O shell principal não pode bloquear o tty antes de aprender o que é. Usando outro bloqueio e a sequência correta de bloqueio e desbloqueio, podemos ter certeza de queunbuffer
sai somente depois que o shell principal for concluído com o tty.A grande vantagem é: se o shell principal sair por qualquer motivo (incluindo
SIGKILL
) antes de ser executadodestroy-logging-tty
, o kernel liberará todos os bloqueios mantidos pelo processo de qualquer maneira. Isso significaunbuffer
que acabará por terminar, não haverá processo obsoleto.Você pode se perguntar se
tty
escrever no fifo pode bloquear até que a função leia a partir dele. Bom, basta abrir o fifo para leitura. Mesmo que o fifo nunca seja lido, um processo de escrita como o nossotty
poderá escrever vários (mil) bytes nele antes de ser bloqueado. O fifo é aberto para leitura no shell principal, mas mesmo que saia prematuramente há o shell dentro dounubffer
qual acaba de abrir o fifo para escrita e leitura. Isso não deve bloquear.A única sobra pode ser o fifo e seu diretório, se o shell principal for encerrado em um momento infeliz. Você pode suprimir ou prender certos sinais até que o momento infeliz passe; ou você pode
trap … EXIT
e limpar de dentro da armadilha. Ainda existem cenários (por exemplo,SIGKILL
) em que seu script simplesmente não pode fazer nada.Testei a solução com interativo
mc
e basicamente funcionou. Espere que a saída (./log
) de tais aplicativos contenha muitas sequências de controle. O log pode ser reproduzido, por exemplopv -qL 400 log
, em um terminal que não seja muito pequeno.Em meus testes
mc
reagiu aSIGWINCH
partir de seu terminal de controle (ou seja, o terminal principal, não o deunbuffer
) e redesenhou sua janela, mas usou o tamanho do terminal sendo sua saída (a deunbuffer
) e nunca mudou.Mesmo se
unbuffer
reagiuSIGWINCH
ou foi forçado a atualizar o tamanho, pode ser tarde demais,mc
pode já ter lido as dimensões antigas. Parece que essa atualização não acontece de qualquer maneira. Uma solução simples é evitar redimensionar o terminal.Problema mais amplo
O problema com
mc
e redimensionamento é devido a uma questão mais ampla. Você escreveu:A solução acima ou semelhante quando há outro tty cuja saída é registrada e impressa no console original certamente não é "diretamente".
mc
atualizaria corretamente seu tamanho se fosse impresso diretamente no terminal original.Normalmente você não pode imprimir diretamente em um terminal e registrar o que o terminal recebe, a menos que o próprio terminal suporte o registro. Pseudoterminais criados por
screen
outmux
podem fazer isso e você pode configurá-los programaticamente de dentro de um script. Alguns emuladores de terminal com GUI podem permitir que você despeje o que eles recebem, você precisa configurá-los via GUI. O ponto é que você precisa de um terminal com o recurso. Execute um script em um terminal "errado" e você não poderá logar dessa maneira (você pode usarreptyr
para "movê-lo" para outro terminal). O script pode redirecionar sua saída como nosso script, mas isso não é "diretamente". Ou…Existem maneiras de bisbilhotar um tty ( exemplos ). Talvez você encontre algo que atenda às suas necessidades. Normalmente, tal espionagem requer acesso elevado, mesmo se você quiser espionar em um tty que você possa ler e escrever.