Captura de códigos de erro em um tubo de shell

97

Atualmente, tenho um script que faz algo como

./a | ./b | ./c

Quero modificá-lo para que, se algum de a, b ou c sair com um código de erro, eu imprima uma mensagem de erro e pare em vez de enviar uma saída incorreta para frente.

Qual seria a maneira mais simples / limpa de fazer isso?

hugomg
fonte
7
Realmente precisa haver algo como &&|"apenas continue o pipe se o comando anterior for bem-sucedido". Suponho que você também poderia ter, o |||que significaria "continuar o pipe se o comando anterior falhar" (e possivelmente canalizar a mensagem de erro como o do Bash 4 |&).
Pausado até novo aviso.
6
@DennisWilliamson, você não pode "parar o pipe", porque a, b, ccomandos não são executados sequencialmente, mas em paralelo. Em outras palavras, os fluxos de dados sequencialmente a partir ade c, mas o efectivo a, be ccomandos iniciar (aproximadamente) ao mesmo tempo.
Giacomo

Respostas:

20

Se você realmente não deseja que o segundo comando prossiga até que o primeiro seja bem-sucedido, provavelmente será necessário usar arquivos temporários. A versão simples disso é:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

O redirecionamento '1> & 2' também pode ser abreviado para '> & 2'; no entanto, uma versão antiga do shell MKS tratou incorretamente o redirecionamento de erro sem o '1' anterior, então usei essa notação inequívoca para confiabilidade por muito tempo.

Isso vaza arquivos se você interromper algo. A programação shell à prova de bomba (mais ou menos) usa:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

A primeira linha de trap diz 'execute os comandos' rm -f $tmp.[12]; exit 1'quando qualquer um dos sinais 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE ou 15 SIGTERM ocorrer, ou 0 (quando o shell sair por qualquer motivo). Se você estiver escrevendo um script de shell, o trap final só precisa remover o trap em 0, que é o trap de saída do shell (você pode deixar os outros sinais no lugar, pois o processo está prestes a terminar de qualquer maneira).

No pipeline original, é viável para 'c' ler dados de 'b' antes de 'a' terminar - isso geralmente é desejável (dá trabalho a vários núcleos, por exemplo). Se 'b' é uma fase de 'classificação', então isso não se aplica - 'b' tem que ver todas as suas entradas antes de poder gerar qualquer uma das suas saídas.

Se você deseja detectar quais comandos falham, você pode usar:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Isso é simples e simétrico - é trivial estender para um pipeline de 4 ou N partes.

A experimentação simples com 'set -e' não ajudou.

Jonathan Leffler
fonte
1
Eu recomendaria usar mktempou tempfile.
Pausado até novo aviso.
@Dennis: sim, acho que devo me acostumar com comandos como mktemp ou tmpfile; eles não existiam no nível da casca quando eu aprendi, oh, tantos anos atrás. Vamos fazer uma verificação rápida. Acho mktemp no MacOS X; Tenho mktemp no Solaris, mas apenas porque instalei ferramentas GNU; parece que mktemp está presente no antigo HP-UX. Não tenho certeza se há uma invocação comum de mktemp que funciona em várias plataformas. POSIX padroniza nem mktemp nem tmpfile. Não encontrei tmpfile nas plataformas às quais tenho acesso. Conseqüentemente, não poderei usar os comandos em scripts de shell portáteis.
Jonathan Leffler
1
Ao usar trap, lembre-se de que o usuário sempre pode enviar SIGKILLa um processo para encerrá-lo imediatamente; nesse caso, a armadilha não terá efeito. O mesmo vale para quando o sistema passa por uma queda de energia. Ao criar arquivos temporários, certifique-se de usar mktempporque isso colocará seus arquivos em algum lugar, onde serão limpos após uma reinicialização (geralmente em /tmp).
Josch de
156

No bash você pode usar set -ee set -o pipefailno início do seu arquivo. Um comando subsequente ./a | ./b | ./cfalhará quando qualquer um dos três scripts falhar. O código de retorno será o código de retorno do primeiro script com falha.

Observe que pipefailnão está disponível no sh padrão .

Michel Samia
fonte
Eu não sabia sobre pipefail, realmente útil.
Phil Jackson
A intenção é colocá-lo em um script, não no shell interativo. Seu comportamento pode ser simplificado para definir -e; falso; ele também sai do processo shell, que é o comportamento esperado;)
Michel Samia
11
Observação: isso ainda executará todos os três scripts e não interromperá o pipe no primeiro erro.
hyde de
1
@ n2liquid-GuilhermeVieira Btw, por "variações diferentes", eu quis dizer especificamente, remover um ou ambos set(para 4 versões diferentes no total) e ver como isso afeta a saída do último echo.
hyde
1
@josch Eu encontrei esta página ao pesquisar como fazer isso no bash do google, e pensei, "Isso é exatamente o que estou procurando". Suspeito que muitas pessoas que votam positivamente nas respostas passam por um processo de pensamento semelhante e não verificam as tags e as definições das tags.
Troy Daniels
43

Você também pode verificar a ${PIPESTATUS[]}matriz após a execução completa, por exemplo, se você executar:

./a | ./b | ./c

Em seguida, ${PIPESTATUS}haverá uma matriz de códigos de erro de cada comando no tubo, portanto, se o comando do meio falhou, echo ${PIPESTATUS[@]}conteria algo como:

0 1 0

e algo como isto é executado após o comando:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

permitirá que você verifique se todos os comandos no pipe foram bem-sucedidos.

Imron
fonte
11
Isso é bashish --- é uma extensão bash e não faz parte do padrão Posix, então outros shells como dash e ash não o suportam. Isso significa que você pode ter problemas se tentar usá-lo em scripts que iniciam #!/bin/sh, porque se shnão for o bash, não funcionará. (Facilmente corrigível, lembrando-se de usar em #!/bin/bashvez disso.)
David dado
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- retorna 0 se $PIPESTATUS[@]contiver apenas 0 e espaços (se todos os comandos no tubo forem bem-sucedidos).
MattBianco
@MattBianco Esta é a melhor solução para mim. Também funciona com &&, por exemplo, command1 && command2 | command3se algum desses falhar, sua solução retorna um valor diferente de zero.
DavidC de
8

Infelizmente, a resposta de Johnathan requer arquivos temporários e as respostas de Michel e Imron requerem bash (mesmo que esta pergunta seja shell marcada). Conforme já apontado por outros, não é possível abortar o pipe antes de os processos posteriores serem iniciados. Todos os processos são iniciados de uma vez e, portanto, todos serão executados antes que qualquer erro possa ser comunicado. Mas o título da pergunta também perguntava sobre códigos de erro. Eles podem ser recuperados e investigados após a conclusão do tubo para descobrir se algum dos processos envolvidos falhou.

Aqui está uma solução que detecta todos os erros no tubo e não apenas os erros do último componente. Portanto, é como o pipefail do bash, apenas mais poderoso no sentido de que você pode recuperar todos os códigos de erro.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Para detectar se algo falhou, um echocomando é impresso no erro padrão caso algum comando falhe. Em seguida, a saída do erro padrão combinado é salva $rese investigada posteriormente. É também por isso que o erro padrão de todos os processos é redirecionado para a saída padrão. Você também pode enviar essa saída /dev/nullou deixá-la como mais um indicador de que algo deu errado. Você pode substituir o último redirecionamento /dev/nullpor um arquivo se não precisar armazenar a saída do último comando em qualquer lugar.

Para brincar mais com essa construção e se convencer de que ela realmente faz o que deveria, substituí ./a, ./be ./cpor subshells que executam echo, cate exit. Você pode usar isso para verificar se essa construção realmente encaminha todas as saídas de um processo para outro e se os códigos de erro são registrados corretamente.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
Josch
fonte