bash - last - shell script exit code




Saída de tubulação e status de saída de captura no Bash (10)

Às vezes, pode ser mais simples e mais claro usar um comando externo, em vez de investigar os detalhes do bash. pipeline , do execline linguagem de script de processo mínimo, sai com o código de retorno do segundo comando *, assim como um sh pipeline faz, mas ao contrário sh , permite reverter a direção do pipe, para que possamos capturar o código de retorno o processo produtor (o abaixo está todo na linha de comando sh , mas com o execline instalado):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

O uso do pipeline tem as mesmas diferenças para os pipelines bash nativos como a substituição do processo bash usada na resposta #43972501 .

* Na verdade, o pipeline não sai, a menos que haja um erro. Ele é executado no segundo comando, então é o segundo comando que faz o retorno.

Eu quero executar um comando de longa execução no Bash, e ambos capturam seu status de saída e tee sua saída.

Então eu faço isso:

command | tee out.txt
ST=$?

O problema é que a variável ST captura o status de saída do tee e não do comando. Como posso resolver isso?

Observe que o comando é longo e redirecionar a saída para um arquivo para exibi-lo posteriormente não é uma boa solução para mim.


A maneira mais simples de fazer isso no bash simples é usar a substituição de processos em vez de um pipeline. Existem várias diferenças, mas elas provavelmente não importam muito para o seu caso de uso:

  • Ao executar um pipeline, o bash aguarda até que todos os processos sejam concluídos.
  • Enviar Ctrl-C para o bash faz com que ele mate todos os processos de um pipeline, não apenas o principal.
  • A opção pipefail e a variável PIPESTATUS são irrelevantes para processar a substituição.
  • Possivelmente mais

Com a substituição do processo, o bash apenas inicia o processo e esquece, nem é visível nos jobs .

Diferenças mencionadas à parte, consumer < <(producer) e producer | consumer producer | consumer são essencialmente equivalentes.

Se você quiser inverter qual é o processo "principal", basta inverter os comandos e a direção da substituição para o producer > >(consumer) . No seu caso:

command > >(tee out.txt)

Exemplo:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

Como eu disse, existem diferenças na expressão do pipe. O processo pode nunca parar de funcionar, a menos que seja sensível ao fechamento do tubo. Em particular, ele pode continuar escrevendo coisas para o seu stdout, o que pode ser confuso.


Combinando PIPESTATUS[0] e o resultado da execução do comando exit em um subshell, você pode acessar diretamente o valor de retorno do seu comando inicial:

command | tee ; ( exit ${PIPESTATUS[0]} )

Aqui está um exemplo:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

Darei à você:

return value: 1


Então, eu queria contribuir com uma resposta como a do lesmana, mas acho que a minha talvez seja uma solução um pouco mais simples e um pouco mais vantajosa de Bourne-shell:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Eu acho que isso é melhor explicado de dentro para fora - o comando1 irá executar e imprimir sua saída regular no stdout (descritor de arquivo 1), então uma vez feito, o printf irá executar e imprimir o código de saída do icommand1 em seu stdout, mas esse stdout é redirecionado para descritor de arquivo 3.

Enquanto o comando1 está em execução, seu stdout está sendo canalizado para o comando2 (a saída do printf nunca faz isso para o comando2 porque o enviamos para o descritor de arquivo 3 em vez de 1, que é o que o canal lê). Em seguida, redirecionamos a saída do comando2 para o descritor de arquivo 4, para que ele fique fora do descritor de arquivo 1 - porque queremos que o descritor de arquivo 1 seja livre um pouco mais tarde, porque traremos a saída printf do descritor de arquivo 3 para descritor de arquivo 1 - porque é isso que a substituição do comando (os backticks) irá capturar e é isso que será colocado na variável.

O último bit de mágica é que o primeiro exec 4>&1 que fizemos como um comando separado - ele abre o descritor de arquivos 4 como uma cópia do stdout do shell externo. A substituição de comandos capturará tudo o que estiver escrito no padrão fora da perspectiva dos comandos dentro dele - mas como a saída do comando2 vai arquivar o descritor 4 no que diz respeito à substituição do comando, a substituição de comando não o captura - no entanto, uma vez fica "fora" da substituição do comando, ele ainda está efetivamente indo para o descritor de arquivo geral do script 1.

(O exec 4>&1 tem que ser um comando separado porque muitos shells comuns não gostam quando você tenta escrever em um descritor de arquivo dentro de uma substituição de comando, que é aberta no comando "external" que está usando a substituição. Portanto, esta é a maneira mais simples de fazer isso.

Você pode olhar para isso de uma maneira menos técnica e mais divertida, como se as saídas dos comandos estivessem saltando umas às outras: command1 canaliza para command2, então a saída do printf passa sobre o comando 2 para que o comando2 não o capture, e então a saída do comando 2 salta para cima e para fora da substituição do comando, assim como o printf chega na hora de ser capturado pela substituição, de modo que ele fique na variável, e a saída do comando2 continua sendo escrita na saída padrão, assim como em um tubo normal.

Além disso, pelo que entendi, $? ainda conterá o código de retorno do segundo comando no pipe, porque atribuições de variáveis, substituições de comandos e comandos compostos são todos efetivamente transparentes para o código de retorno do comando dentro deles, portanto o status de retorno do comando2 deve ser propagado para fora - isso , e não ter que definir uma função adicional, é por isso que acho que essa pode ser uma solução um pouco melhor do que a proposta pela lesmana.

Pelas advertências que o lesmana menciona, é possível que o comando1 acabe usando os descritores de arquivo 3 ou 4, para ser mais robusto, você faria:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Observe que eu uso comandos compostos no meu exemplo, mas subshells (using ( ) vez de { } também funcionará, embora talvez seja menos eficiente).

Os comandos herdam os descritores de arquivos do processo que os inicia, portanto, a segunda linha inteira herdará o descritor de arquivos quatro, e o comando composto seguido por 3>&1 herdará o descritor de arquivo três. Portanto, o 4>&- certifica-se de que o comando composto interno não herdará o descritor de arquivo quatro, e o 3>&- não herdará o descritor de arquivo três, portanto o comando1 obtém um ambiente mais limpo e mais padrão. Você também pode mover o interior 4>&- próximo ao 3>&- , mas eu entendo por que não limitar seu escopo tanto quanto possível.

Não sei com que freqüência as coisas usam o descritor de arquivo três e quatro diretamente - acho que na maioria das vezes os programas usam syscalls que retornam descritores de arquivos não usados ​​no momento, mas às vezes escrevem códigos no descritor de arquivos 3 diretamente, acho que (eu poderia imaginar um programa verificando um descritor de arquivo para ver se ele está aberto e usá-lo se estiver, ou se comportando de maneira diferente se não estiver). Portanto, o último é provavelmente o melhor para ter em mente e usar para casos gerais.


Existe um array que lhe dá o status de saída de cada comando em um pipe.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

Existe uma variável interna de Bash chamada $PIPESTATUS ; é uma matriz que contém o status de saída de cada comando em seu último pipeline de primeiro plano de comandos.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Ou outra alternativa que também funciona com outras shells (como zsh) seria habilitar pipefail:

set -o pipefail
...

A primeira opção não funciona com zsh devido a uma sintaxe um pouco diferente.


No Ubuntu e Debian, você pode apt-get install moreutils . Contém um utilitário chamado mispipe que retorna o status de saída do primeiro comando no pipe.


PIPESTATUS [@] deve ser copiado para um array imediatamente após o retorno do comando pipe. Qualquer leitura de PIPESTATUS [@] irá apagar o conteúdo. Copie-o para outro array se você planeja verificar o status de todos os comandos do pipe. "$?" é o mesmo valor do último elemento de "$ {PIPESTATUS [@]}", e a leitura parece destruir "$ {PIPESTATUS [@]}", mas eu não o confirmei absolutamente.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Isso não funcionará se o pipe estiver em um sub-shell. Para uma solução para esse problema,
ver bash pipestatus no comando backticked?


Solução shell puro:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

E agora com o segundo cat substituído por false :

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

Por favor, note que o primeiro gato também falha, porque o stdout está fechado nele. A ordem dos comandos com falha no log está correta neste exemplo, mas não confie nela.

Este método permite a captura de stdout e stderr para os comandos individuais, para que você possa então descarregar isso também em um arquivo de log se ocorrer um erro, ou apenas apagá-lo se não houver erro (como a saída de dd).


usando o set -o pipefail de bash set -o pipefail é útil

pipefail: o valor de retorno de um pipeline é o status do último comando para sair com um status diferente de zero, ou zero se nenhum comando sair com um status diferente de zero







pipe