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


421

Desejo 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 teee não do comando. Como posso resolver isso?

Observe que o comando está demorando muito tempo e redirecionar a saída para um arquivo para visualizá-lo mais tarde não é uma boa solução para mim.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Correspondência - erro encontrado" || echo -e "Sem correspondência - tudo bom" Isso testará todos os valores da matriz de uma só vez e fornecerá uma mensagem de erro se algum dos valores de canal retornados não for zero. Essa é uma solução generalizada bastante robusta para detectar erros em uma situação de canalização.
— Brian S. Wilson

Respostas:


519

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

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

Ou outra alternativa que também funciona com outros shells (como zsh) seria ativar o pipefail:

set -o pipefail
...

A primeira opção não funciona zshdevido a uma sintaxe um pouco diferente.


21
Há uma boa explicação com exemplos de PIPESTATUS AND Pipefail aqui: unix.stackexchange.com/a/73180/7453 .
— SLM

18
Nota: $ PIPESTATUS [0] mantém o status de saída do primeiro comando no canal, $ PIPESTATUS [1] o status de saída do segundo comando e assim por diante.
— simpleuser

18
Obviamente, devemos lembrar que isso é específico do Bash: se eu (por exemplo) escrever um script para executar na implementação "sh" do BusyBox no meu dispositivo Android ou em alguma outra plataforma incorporada usando outro "sh" variante, isso não funcionaria.
— Asfand Qazi

4
Para aqueles preocupados com a expansão de variáveis ​​não citadas: O status de saída é sempre um número inteiro de 8 bits não assinado no Bash , portanto, não há necessidade de citá-lo. Isso também ocorre no Unix geralmente, onde o status de saída é definido explicitamente para 8 bits e é assumido como não assinado mesmo pelo próprio POSIX, por exemplo, ao definir sua negação lógica .
— Palec

3
Você também pode usar exit ${PIPESTATUS[0]}.
— Chaoran 9/09/16

142

usando 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


23
Caso não deseje modificar a configuração de pipefail de todo o script, você pode definir a opção apenas localmente:( set -o pipefail; command | tee out.txt ); ST=$?
— Jaan

7
@ Jaan Isso executaria um subshell. Se você quiser evitar isso, poderá set -o pipefailexecutar o comando e, imediatamente depois, set +o pipefaildesmarcar a opção.
— Linus Arver 27/09/16

2
Nota: o pôster da pergunta não deseja um "código de saída geral" do canal, ele deseja o código de retorno de 'comando'. Com -o pipefailele saberia se o canal falha, mas se 'command' e 'tee' falharem, ele receberia o código de saída de 'tee'.
— precisa saber é

@LinusArver não limparia o código de saída, já que é um comando bem-sucedido?
— Carlin.scott

127

Solução idiota: conectando-os através de um pipe nomeado (mkfifo). Em seguida, o comando pode ser executado em segundo.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

20
Esta é a única resposta nesta pergunta que também funciona para o shell sh Unix simples . Obrigado!
— JamesThomasMoon1979

3
@DaveKennedy: mudo como em "óbvia, não necessitando de conhecimento complexo de sintaxe bash"
— Efraim

10
Embora as respostas do bash sejam mais elegantes quando você tiver a vantagem dos recursos extras do bash, esta é a solução mais multiplataforma. Também é algo que vale a pena pensar em geral, pois sempre que você executa um comando de longa execução, um pipe de nome geralmente é a maneira mais flexível. Vale a pena notar que alguns sistemas não têm mkfifoe podem exigir, mknod -pse bem me lembro.
— Haravikk

3
Às vezes, no estouro de pilha, há respostas que você votaria centenas de vezes, para que as pessoas parassem de fazer outras coisas que não fazem sentido; essa é uma delas. Obrigado senhor.
— Dan perseguição

1
No caso de alguém ter um problema com mkfifoou mknod -p: no meu caso, o comando adequado para criar o arquivo de pipe era mknod FILE_NAME p.
— Karol Gil

36

Há uma matriz que fornece 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

26

Esta solução funciona sem usar recursos específicos do bash ou arquivos temporários. Bônus: no final, o status de saída é realmente um status de saída e não uma sequência de caracteres em um arquivo.

Situação:

someprog | filter

você deseja o status de someprogsaída e a saída de filter.

Aqui está a minha solução:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Veja minha resposta para a mesma pergunta em unix.stackexchange.com para obter uma explicação detalhada e uma alternativa sem sub-conchas e algumas ressalvas.


20

Ao combinar PIPESTATUS[0]e o resultado da execução do exitcomando em uma 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


4
Obrigado, isso me permitiu usar a construção: VALUE=$(might_fail | piping)que não definirá o PIPESTATUS no shell principal, mas definirá seu nível de erro. Usando: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})Eu quero o que queria.
— vaab

@vaab, essa sintaxe parece muito legal, mas estou confuso sobre o que significa 'canalização' no seu contexto? É aí que alguém faria 'tee' ou qualquer outro processamento na saída de poder_falha? ty!
— AnneTheAgile

1
@AnneTheAgile 'piping' no meu exemplo significa comandos dos quais você não deseja ver o errlvl. Por exemplo: uma ou qualquer combinação de 'tee', 'grep', 'sed', ... Não é tão incomum que esses comandos de tubulação sejam criados para formatar ou extrair informações de uma saída maior ou saída de log do principal comando: você está mais interessado no nível de erro do comando principal (o que eu chamei de 'falha_pode' no meu exemplo), mas sem a minha construção, a atribuição inteira retorna o errlvl do último comando canalizado, que não tem sentido aqui. Isso é mais claro?
— vaab

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}no caso não tee, mas grep filtragem
— user1742529 6/04

12

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

# 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 - command1 executará e imprimirá sua saída regular em stdout (descritor de arquivo 1); depois que estiver pronto, printf executará e imprimirá o código de saída do icommand1 em seu stdout, mas esse stdout será 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 chega ao comando2 porque enviamos para o descritor de arquivo 3 em vez de 1, que é o que o canal lê). Em seguida, redirecionamos a saída do command2 para o descritor de arquivo 4, para que ele também fique fora do descritor de arquivo 1 - porque queremos que o descritor de arquivo 1 fique livre por um pouco mais tarde, porque traremos a saída printf do descritor de arquivo 3 de volta para o descritor de arquivo 1 - porque é isso que a substituição de comando (os backticks) captura e é isso que será colocado na variável.

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

( exec 4>&1Tem que ser um comando separado, porque muitos shells comuns não gostam quando você tenta gravar em um descritor de arquivo dentro de uma substituição de comando, que é aberto no comando "externo" que está usando a substituição. Portanto, este é o maneira portátil mais simples de fazer isso.)

Você pode vê-lo de uma maneira menos técnica e mais divertida, como se as saídas dos comandos estivessem saltando uma contra a outra: command1 canaliza para command2, então a saída do printf salta sobre o comando 2 para que o comando2 não o pegue e depois a saída do comando 2 salta para cima e para fora da substituição de comando, assim como printf chega bem a tempo de ser capturada pela substituição, para que acabe na variável, e a saída do comando2 segue seu caminho alegre sendo gravada 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 canal, porque atribuições de variáveis, substituições de comandos e comandos compostos são efetivamente transparentes ao código de retorno do comando dentro deles, portanto, o status de retorno de command2 deve ser propagado - isso, e não sendo necessário definir uma função adicional, é por isso que acho que essa pode ser uma solução um pouco melhor do que a proposta por lesmana.

De acordo com as advertências mencionadas por lesmana, é possível que o command1 acabe, em algum momento, usando os descritores de arquivo 3 ou 4; portanto, para ser mais robusto, faça o seguinte:

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 (usar em ( )vez de { }também funcionará, embora talvez seja menos eficiente.)

Os comandos herdam os descritores de arquivo do processo que os inicia; portanto, a segunda linha inteira herdará o descritor de arquivo quatro e o comando composto seguido por 3>&1herdará o descritor de arquivo três. Portanto, ele 4>&-garante que o comando composto interno não herda o descritor de arquivo quatro e o 3>&-herdeiro não descreva o arquivo três; portanto, o comando1 obtém um ambiente mais limpo e padrão. Você também pode mover o interior 4>&-para o próximo 3>&-, mas acho que não basta limitar o escopo o máximo possível.

Não sei com que frequência as coisas usam o descritor de arquivos três e quatro diretamente - acho que na maioria das vezes os programas usam syscalls que retornam descritores de arquivo não usados ​​no momento, mas às vezes o código grava diretamente no descritor de arquivo 3, palpite (eu poderia imaginar um programa verificando um descritor de arquivo para ver se ele está aberto e usando-o se estiver, ou se comportando de maneira diferente se não estiver). Portanto, é provável que o último seja lembrado e usado em casos de uso geral.


Boa explicação!
— Selurvedu

6

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


5
(command | tee out.txt; exit ${PIPESTATUS[0]})

Diferentemente da resposta do @ cODAR, isso retorna o código de saída original do primeiro comando e não apenas 0 para o sucesso e 127 para a falha. Mas como @Chaoran apontou, você pode simplesmente ligar ${PIPESTATUS[0]}. É importante, no entanto, que tudo seja colocado entre colchetes.


4

Fora do bash, você pode fazer:

bash -o pipefail  -c "command1 | tee output"

Isso é útil, por exemplo, em scripts ninja em que o shell deve estar /bin/sh.


3

PIPESTATUS [@] deve ser copiado para uma matriz imediatamente após o retorno do comando pipe. Qualquer leitura do PIPESTATUS [@] apaga o conteúdo. Copie-o para outra matriz se você planeja verificar o status de todos os comandos de canal. "$?" é o mesmo valor que o último elemento de "$ {PIPESTATUS [@]}" e a leitura parece destruir "$ {PIPESTATUS [@]}", mas ainda não o verifiquei totalmente.

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

Isso não funcionará se o tubo estiver em uma subcasca. Para uma solução para esse problema,
consulte bash pipestatus no comando backticked?


3

A maneira mais simples de fazer isso no bash simples é usar a substituição do processo 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 elimine todos os processos de um pipeline, não apenas o principal.
  • A pipefailopção e a PIPESTATUSvariável 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 sequer é visível no jobs.

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

Se você deseja alterar qual é o processo "principal", basta alternar os comandos e a direção da substituição para 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 de 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 no seu stdout, o que pode ser confuso.


1

Solução de casca pura:

% 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 catsubstituí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

Observe que o primeiro gato também falha, porque o stdout fica fechado nele. A ordem dos comandos com falha no log está correta neste exemplo, mas não conte com isso.

Esse método permite capturar stdout e stderr para os comandos individuais, para que você possa despejar isso também em um arquivo de log se ocorrer um erro ou apenas excluí-lo se não houver erro (como a saída do dd).


1

Baseie-se na resposta de @ brian-s-wilson; esta função auxiliar bash:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

usado assim:

1: get_bad_things deve ter êxito, mas não deve produzir saída; mas queremos ver a saída que ela produz

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: todo o pipeline deve ter sucesso

thing | something -q | thingy
pipeinfo || return

1

Às vezes, pode ser mais simples e claro usar um comando externo, em vez de se aprofundar nos detalhes do bash. O pipeline , da linguagem de script de processo mínima execline , sai com o código de retorno do segundo comando *, assim como um shpipeline, mas sh, ao contrário , permite reverter a direção do pipe, para que possamos capturar o código de retorno do produtor processo (o abaixo é tudo na shlinha de comando, mas com execlineinstalado):

$ # 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 pipelinetem as mesmas diferenças para os pipelines de bash nativos que a substituição do processo de bash usada na resposta # 43972501 .

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.