Como capturar STDOUT / STDERR ordenado e adicionar timestamp / prefixos?

25

Eu explorei quase todas as perguntas semelhantes disponíveis , sem sucesso.

Deixe-me descrever o problema em detalhes:

Eu executo alguns scripts autônomos e eles podem produzir saída padrão e linhas de erro padrão. Quero capturá-los em sua ordem precisa, conforme exibido por um emulador de terminal, e depois adicionar um prefixo como "STDERR:" e "STDOUT:" a eles.

Eu tentei usar pipes e até mesmo uma abordagem baseada em epolls, sem sucesso. Eu acho que a solução está em uso pty, embora eu não seja mestre nisso. Também examinei o código-fonte do VTE do Gnome , mas isso não foi muito produtivo.

Idealmente, eu usaria o Go em vez do Bash para fazer isso, mas não consegui. Parece que os tubos proíbem automaticamente manter uma ordem correta das linhas devido ao buffer.

Alguém foi capaz de fazer algo semelhante? Ou é simplesmente impossível? Eu acho que se um emulador de terminal pode fazer isso, então não é - talvez criando um pequeno programa C manipulando os PTY (s) de maneira diferente?

Idealmente, eu usaria entrada assíncrona para ler esses 2 fluxos (STDOUT e STDERR) e depois os reimprimiria segundo minhas necessidades, mas a ordem de entrada é crucial!

NOTA: Conheço stderred, mas ele não funciona para mim com scripts Bash e não pode ser facilmente editado para adicionar um prefixo (já que basicamente envolve muitos syscalls).

Atualização: adicionada abaixo de duas sugestões

(atrasos aleatórios de menos de um segundo podem ser adicionados no script de amostra que forneci para provar um resultado consistente)

Atualização: a solução para essa pergunta também resolveria essa outra questão , como apontou o @Gilles. No entanto, cheguei à conclusão de que não é possível fazer o que é solicitado aqui e ali. Quando o uso de 2>&1ambos os fluxos é mesclado corretamente no nível pty / pipe, mas para usá- los separadamente e na ordem correta, deve-se realmente usar a abordagem de stderred que invole o gancho do syscall e pode ser vista como suja de várias maneiras.

Ficarei ansioso para atualizar esta pergunta se alguém puder refutar o que foi dito acima.

Deim0s
fonte
1
Não é isso que você quer? stackoverflow.com/questions/21564/…
slm
@slm provavelmente não, visto que as necessidades OP para preceder diferentes strings para diferentes correntes.
Peterph 26/09
Você pode compartilhar por que o pedido é tão importante? Talvez poderia haver alguma outra maneira de contornar o problema ...
peterph
@peterph é um pré-requisito, se eu não puder ter uma saída consistente, prefiro enviá-la para / dev / null do que ler e ficar confusa com ela :) 2> & 1 preserva a ordem, por exemplo, mas não permite o tipo de personalização que eu peço nesta questão
Deim0s

Respostas:

12

Você pode usar coprocessos. Wrapper simples que alimenta as duas saídas de um determinado comando para duas sedinstâncias (uma para stderra outra stdout), que fazem a marcação.

#!/bin/bash
exec 3>&1
coproc SEDo ( sed "s/^/STDOUT: /" >&3 )
exec 4>&2-
coproc SEDe ( sed "s/^/STDERR: /" >&4 )
eval $@ 2>&${SEDe[1]} 1>&${SEDo[1]}
eval exec "${SEDo[1]}>&-"
eval exec "${SEDe[1]}>&-"

Observe várias coisas:

  1. É um encantamento mágico para muitas pessoas (inclusive eu) - por um motivo (veja a resposta vinculada abaixo).

  2. Não há garantia de que ocasionalmente não troque algumas linhas - tudo depende do agendamento dos coprocessos. Na verdade, é quase garantido que, em algum momento, isso acontecerá. Dito isto, se manter a ordem estritamente a mesma, você deverá processar os dados de ambos stderre stdinno mesmo processo, caso contrário, o agendador do kernel pode (e irá) estragar tudo.

    Se eu entendi o problema corretamente, significa que você precisaria instruir o shell para redirecionar os dois fluxos para um processo (o que pode ser feito no AFAIK). O problema começa quando esse processo começa a decidir o que agir primeiro - ele teria que pesquisar as duas fontes de dados e, em algum momento, entrar em um estado em que estaria processando um fluxo e os dados chegarem aos dois fluxos antes de terminar. E é exatamente aí que quebra. Isso também significa que agrupar os syscalls de saída stderredé provavelmente a única maneira de alcançar o resultado desejado (e mesmo assim você poderá ter um problema quando algo se tornar multithread em um sistema multiprocessador).

Quanto aos coprocessos, leia a excelente resposta de Stéphane em Como você usa o comando coproc no Bash? para uma visão aprofundada.

peterph
fonte
Obrigado @ Peter pela sua resposta, no entanto, estou procurando especificamente maneiras de preservar o pedido. Nota: Eu acho que o intérprete deve ser festa por causa da substituição de processo que você usa (eu recebo ./test1.sh: 3: ./test1.sh: Syntax error: "(" unexpectedpor copiar / colar o script)
Deim0s
Muito provavelmente por isso, eu corri-lo em bashcom /bin/sh(não sei por que eu tinha lá).
Peterph 26/09/2014
Atualizei a questão um pouco, sobre onde a mistura de fluxo poderia acontecer.
Peterph 26/09
1
eval $@é bastante buggy. Use "$@"se você deseja executar seus argumentos como uma linha de comando exata - adicionando uma camada de evalinterpretações em várias previsões difíceis de prever (e potencialmente maliciosas, se você estiver passando nomes de arquivos ou outro conteúdo que você não controla como argumentos), e falhando em citar até mais (divide nomes com espaços em várias palavras, expande os globs, mesmo que tenham sido citados anteriormente como literais, etc.).
Charles Duffy
1
Além disso, no bash moderno de coprocessos modernos, você não precisa eval fechar os descritores de arquivo nomeados em uma variável. exec {SEDo[1]}>&-funcionará como está (sim, a falta de um $antes do {deliberado).
Charles Duffy
5

Método 1. Usando descritores de arquivo e awk

Que tal algo assim usando as soluções desta seção de perguntas e respostas com o título: Existe um utilitário Unix para acrescentar carimbos de data e hora às linhas de texto? e este SO Q&A intitulado: canalize STDOUT e STDERR para dois processos diferentes no shell script? .

A abordagem

Etapa 1, criamos 2 funções no Bash que executam a mensagem de carimbo de data / hora quando chamadas:

$ msgOut () {  awk '{ print strftime("STDOUT: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }
$ msgErr () {  awk '{ print strftime("STDERR: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }

Etapa 2, você usaria as funções acima para obter as mensagens desejadas:

$ { { { ...command/script... } 2>&3; } 2>&3 | msgErr; } 3>&1 1>&2 | msgOut

Exemplo

Aqui inventei um exemplo que grava aem STDOUT, dorme por 10 segundos e grava a saída em STDERR. Quando colocamos essa sequência de comandos em nossa construção acima, recebemos mensagens conforme especificado.

$ { { echo a; sleep 10; echo >&2 b; } 2>&3 | \
    msgErr; } 3>&1 1>&2 | msgOut
STDERR: 2014-09-26 09:22:12 a
STDOUT: 2014-09-26 09:22:22 b

Método # 2. Usando anotação-saída

Existe uma ferramenta chamada annotate-outputque faz parte do devscriptspacote que fará o que você deseja. Sua única restrição é que ele deve executar os scripts para você.

Exemplo

Se colocarmos nossa sequência de comandos de exemplo acima em um script chamado mycmds.bashassim:

$ cat mycmds.bash 
#!/bin/bash

echo a
sleep 10
echo >&2 b

Podemos então executá-lo assim:

$ annotate-output ./mycmds.bash 
09:48:00 I: Started ./mycmds.bash
09:48:00 O: a
09:48:10 E: b
09:48:10 I: Finished with exitcode 0

O formato da saída pode ser controlado para a parte do registro de data e hora, mas não além disso. Mas é um resultado semelhante ao que você está procurando, por isso pode ser adequado.

slm
fonte
1
infelizmente isso também não resolve o problema de trocar algumas linhas.
Peterph
exatamente. Penso que a resposta a esta minha pergunta "não é possível". O evento com stderredvocê não pode determinar facilmente os limites das linhas (tentar isso seria um hack). Eu queria ver se alguém poderia me ajudar com este problema, mas, aparentemente, todo mundo quer desistir da restrição único ( ordem ), que é a base para a pergunta
Deim0s
O passo 2 do método 1 requer outro {na frente para funcionar corretamente.
Austin Hanson