No script bash, como capturar stdout linha por linha

26

Em um script bash, eu gostaria de capturar a saída padrão de uma longa linha de comando por linha, para que eles possam ser analisados ​​e relatados enquanto o comando inicial ainda está em execução. Esta é a maneira complicada que posso imaginar de fazer isso:

# Start long command in a separated process and redirect stdout to temp file
longcommand > /tmp/tmp$$.out &

#loop until process completes
ps cax | grep longcommand > /dev/null
while [ $? -eq 0 ]
do
    #capture the last lines in temp file and determine if there is new content to analyse
    tail /tmp/tmp$$.out

    # ...

    sleep 1 s  # sleep in order not to clog cpu

    ps cax | grep longcommand > /dev/null
done

Eu gostaria de saber se existe uma maneira mais simples de fazer isso.

EDITAR:

Para esclarecer minha pergunta, adicionarei isso. Ele longcommandexibe seu status linha por linha uma vez por segundo. Eu gostaria de pegar a saída antes da longcommandconclusão.

Dessa forma, eu posso matar o potencial longcommandse ele não fornecer os resultados esperados.

Eu tentei:

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Mas whatever(por exemplo echo) só é executado após a longcommandconclusão.

gfrigon
fonte

Respostas:

32

Basta canalizar o comando em um whileloop. Existem várias nuances para isso, mas basicamente (em bashou em qualquer shell POSIX):

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

A outra pegadinha principal com isso (além das IFScoisas abaixo) é quando você tenta usar variáveis ​​de dentro do loop quando ele terminar. Isso ocorre porque o loop é realmente executado em um sub-shell (apenas outro processo do shell) do qual você não pode acessar variáveis ​​(também termina quando o loop o faz, e nesse ponto as variáveis ​​desaparecem completamente. você pode fazer:

longcommand | {
  while IFS= read -r line
  do
    whatever "$line"
    lastline="$line"
  done

  # This won't work without the braces.
  echo "The last line was: $lastline"
}

Exemplo de configuração de Hauke lastpipeem bashoutra solução.

Atualizar

Para se certificar de que você está processando a saída do comando 'como acontece', você pode usar stdbufpara definir o processo ' stdoutpara que seja buffer de linha.

stdbuf -oL longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Isso configurará o processo para gravar uma linha de cada vez no canal, em vez de armazenar internamente sua saída em blocos. Cuidado que o programa pode alterar essa configuração internamente. Um efeito semelhante pode ser alcançado com unbuffer(parte de expect) ou script.

stdbufestá disponível nos sistemas GNU e FreeBSD, afeta apenas o stdiobuffer e funciona apenas para aplicativos não setuid e não setgid que são dinamicamente vinculados (como ele usa um truque LD_PRELOAD).

Graeme
fonte
@ Stephanie O IFS=não é necessário bash, eu verifiquei isso após a última vez.
Graeme
2
sim. Não é necessário se você omitir line(nesse caso, o resultado é inserido $REPLYsem os espaços à esquerda e à direita aparados). Tente: echo ' x ' | bash -c 'read line; echo "[$line]"'e compare com echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'ouecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@ Stephanie, ok, nunca percebi que havia uma diferença entre isso e uma variável nomeada. Obrigado.
Graeme
@Graeme Talvez eu não tenha ficado claro na minha pergunta, mas gostaria de processar a linha de saída por linha antes que o longo comando seja concluído (para reagir rapidamente se o comando longo exibir uma mensagem de erro). Vou editar a minha pergunta para tornar mais clara
gfrigon
@gfrigon, atualizado.
Graeme
2
#! /bin/bash
set +m # disable job control in order to allow lastpipe
shopt -s lastpipe
longcommand |
  while IFS= read -r line; do lines[i]="$line"; ((i++)); done
echo "${lines[1]}" # second line
Hauke ​​Laging
fonte