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.

flybywire
fonte
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.

codar
fonte
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

Felipe Alvarez
fonte
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 $?
EFraim
fonte
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
Stefano Borini
fonte
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.

lesmana
fonte
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

par
fonte
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.

mtraceur
fonte
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.

Bryan Larsen
fonte
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.

jakob-r
fonte
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.

Anthony Scemama
fonte
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?

maxdev137
fonte
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.

clacke
fonte
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).

Coroos
fonte
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
Sam Liddicott
fonte
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.

clacke
fonte