Obter status de saída do processo que é canalizado para outra


Respostas:


256

Se você estiver usando bash, poderá usar a PIPESTATUSvariável da matriz para obter o status de saída de cada elemento do pipeline.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Se você estiver usando zsh, a matriz é chamada pipestatus(o caso é importante!) E os índices da matriz começam em um:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Para combiná-los em uma função de uma maneira que não perca os valores:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Execute o acima em bashou zshe você obterá os mesmos resultados; apenas um de retval_bashe retval_zshserá definido. O outro ficará em branco. Isso permitiria que uma função terminasse return $retval_bash $retval_zsh(observe a falta de aspas!).


9
E pipestatusno zsh. Infelizmente, outras conchas não possuem esse recurso.
Gilles

8
Nota: As matrizes no zsh começam contra-intuitivamente no índice 1, por isso é echo "$pipestatus[1]" "$pipestatus[2]".
Christoph Wurm

5
Você pode verificar o pipeline inteiro como este:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0 25/09/12

7
@JanHudec: Talvez você devesse ler as cinco primeiras palavras da minha resposta. Indique também onde a pergunta solicitou uma resposta apenas para POSIX.
Camh

12
@JanHudec: Nem foi marcado com POSIX. Por que você acha que a resposta deve ser POSIX? Não foi especificado, por isso forneci uma resposta qualificada. Não há nada incorreto na minha resposta, além de várias respostas para resolver outros casos.
Camh

238

Existem 3 maneiras comuns de fazer isso:

Pipefail

A primeira maneira é definir a pipefailopção ( ksh, zshou bash). Isso é o mais simples e o que ele faz é basicamente definir o status de saída $?para o código de saída do último programa para sair diferente de zero (ou zero, se todos foram encerrados com êxito).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

O Bash também possui uma variável de matriz chamada $PIPESTATUS( $pipestatusin zsh) que contém o status de saída de todos os programas no último pipeline.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

Você pode usar o terceiro exemplo de comando para obter o valor específico no pipeline necessário.

Execuções separadas

Esta é a mais pesada das soluções. Execute cada comando separadamente e capture o status

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

2
Droga! Eu ia postar sobre o PIPESTATUS.
Slm

1
Para referência, existem várias outras técnicas discutidas nesta questão do SO: stackoverflow.com/questions/1221833/…
slm

1
@ Patrick a solução pipestatus é funciona no bash, apenas mais quastion no caso de eu usar o script ksh, você acha que podemos encontrar algo semelhante ao pipestatus? , (Meenwhile eu ver o pipestatus não suportada por ksh)
yael

2
@yael Eu não uso ksh, mas, de uma breve olhada na página de manual, ele não suporta $PIPESTATUSou algo semelhante. Ele suporta a pipefailopção embora.
21413 Patrick

1
Eu decidi ir com pipefail, pois me permite obter o status do comando falhou aqui:LOG=$(failed_command | successful_command)
vmrob

51

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

o resultado dessa construção é stdout de filtercomo stdout da construção e o status de someprogsaída de como status de saída da construção.


essa construção também funciona com um simples agrupamento de comandos em {...}vez de subshells (...). subcascas têm algumas implicações, entre outras, um custo de desempenho, do qual não precisamos aqui. leia o manual do fine bash para obter mais detalhes: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Infelizmente, a gramática do bash requer espaços e ponto e vírgula para as chaves, de modo que a construção se torne muito mais espaçosa.

No restante deste texto, usarei a variante subshell.


Exemplo someproge filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

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

echo $?

Exemplo de saída:

filtered line1
filtered line2
filtered line3
42

Nota: o processo filho herda os descritores de arquivo aberto do pai. Isso significa someprogque herdará o descritor de arquivo aberto 3 e 4. Se someproggravar no descritor de arquivo 3, esse será o status de saída. O status de saída real será ignorado porque readapenas lê uma vez.

Se você se preocupa com a someprogpossibilidade de gravar no descritor de arquivo 3 ou 4, é melhor fechar os descritores de arquivo antes de chamar someprog.

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

O exec 3>&- 4>&-anterior someprogfecha o descritor de arquivo antes de executar, someprogportanto, para someprogesses descritores de arquivo simplesmente não existem.

Também pode ser escrito assim: someprog 3>&- 4>&-


Explicação passo a passo da construção:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

De baixo para cima:

  1. Um subshell é criado com o descritor de arquivo 4 redirecionado para o stdout. Isso significa que tudo o que for impresso no descritor de arquivo 4 no subshell será o stdout de toda a construção.
  2. Um canal é criado e os comandos à esquerda ( #part3) e à direita ( #part2) são executados. exit $xstambém é o último comando do pipe e isso significa que a string de stdin será o status de saída de toda a construção.
  3. Um subshell é criado com o descritor de arquivo 3 redirecionado para o stdout. Isso significa que tudo o que for impresso no descritor de arquivo 3 neste subshell terminará #part2e, por sua vez, será o status de saída de toda a construção.
  4. Um canal é criado e os comandos à esquerda ( #part5e #part6) e à direita ( filter >&4) são executados. A saída de filteré redirecionada para o descritor de arquivo 4. No #part1descritor de arquivo 4 foi redirecionado para stdout. Isso significa que a saída de filteré o stdout de toda a construção.
  5. O status de saída de #part6é impresso no descritor de arquivo 3. No #part3descritor de arquivo 3, foi redirecionado para #part2. Isso significa que o status de saída de #part6será o status de saída final para toda a construção.
  6. someprogÉ executado. O status de saída é recebido #part5. O stdout é levado pelo tubo #part4e encaminhado para filter. A saída de filter, por sua vez, alcançará stdout, conforme explicado em#part4

1
Agradável. Para a função que eu poderia fazer(read; exit $REPLY)
jthill

5
(exec 3>&- 4>&-; someprog)simplifica para someprog 3>&- 4>&-.
Roger Pate

Este método também funciona sem { { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
sub

36

Embora não seja exatamente o que você pediu, você pode usar

#!/bin/bash -o pipefail

para que seus tubos retornem o último retorno diferente de zero.

pode ser um pouco menos codificado

Editar: Exemplo

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1

9
set -o pipefaildentro do script deve ser mais robusto, por exemplo, caso alguém execute o script via bash foo.sh.
maxschlepzig

Como isso funciona? você tem um exemplo?
9688 Johan

2
Observe que -o pipefailnão está no POSIX.
Scy

2
Isso não funciona no meu lançamento do BASH 3.2.25 (1). No topo de / tmp / ff eu tenho #!/bin/bash -o pipefail. O erro é:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez

2
@FelipeAlvarez: Alguns ambientes (incluindo Linux) não analisar espaços em #!linhas além do primeiro, e assim por isso torna-se /bin/bash -o pipefail /tmp/ff, em vez do necessário /bin/bash -o pipefail /tmp/ff- getoptde análise (ou similar) usando o optarg, que é o próximo item ARGV, como argumento para -o, então falha. Se você fizesse um invólucro (digamos, bash-pffizesse isso exec /bin/bash -o pipefail "$@"e colocasse em #!
risco

22

O que faço quando possível é alimentar o código de saída de foopara bar. Por exemplo, se eu sei que foonunca produz uma linha com apenas dígitos, posso simplesmente aderir ao código de saída:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Ou se eu souber que a saída de foonunca contém uma linha com apenas .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Isso sempre pode ser feito se houver alguma maneira de começar bara trabalhar, exceto a última linha, e passá-la como seu código de saída.

Se baré um pipeline complexo cuja saída você não precisa, você pode ignorar parte dele imprimindo o código de saída em um descritor de arquivo diferente.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

Depois disso , $exit_codesgeralmente foo:X bar:Y, mas pode ser bar:Y foo:Xse for barencerrado antes de ler todas as entradas ou se você tiver azar. Eu acho que gravações em pipes de até 512 bytes são atômicas em todas as unidades, portanto as partes foo:$?e bar:$?não serão misturadas enquanto as strings de tag estiverem abaixo de 507 bytes.

Se você precisar capturar a saída bar, fica difícil. Você pode combinar as técnicas acima, organizando para que a saída barnunca contenha uma linha que se pareça com uma indicação de código de saída, mas ela fica complicada.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

E, claro, há a opção simples de usar um arquivo temporário para armazenar o status. Simples, mas não tão simples na produção:

  • Se houver vários scripts em execução simultaneamente ou se o mesmo script usar esse método em vários locais, será necessário garantir que eles usem nomes de arquivos temporários diferentes.
  • Criar um arquivo temporário com segurança em um diretório compartilhado é difícil. Muitas vezes, /tmpé o único lugar em que um script certamente poderá gravar arquivos. Use mktemp, que não é POSIX, mas está disponível em todas as unidades sérias atualmente.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

1
Ao usar a abordagem de arquivo temporário I preferem adicionar uma armadilha para SAIR que remove todos os arquivos temporários de modo que nenhum lixo será deixado mesmo se o script morre
miracle173

17

A partir do pipeline:

foo | bar | baz

Aqui está uma solução geral usando apenas shell POSIX e nenhum arquivo temporário:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses contém os códigos de status de qualquer processo com falha, em ordem aleatória, com índices para informar qual comando emitiu cada status.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Observe as citações $error_statusesnos meus testes; sem eles grepnão podem se diferenciar porque as novas linhas são coagidas a espaços.


12

Então, eu queria contribuir com uma resposta como a de Lesmana, mas acho que a minha talvez seja uma solução um pouco mais simples e um pouco mais vantajosa e pura 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 - 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 comando1 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 o enviamos ao 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, depois de "sair" da substituição de comando, ele efetivamente ainda vai 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 entre si: 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, em seguida, 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 ressalvas que lesmana menciona, é possível que o command1 acabe, em algum momento, usando os descritores de arquivo 3 ou 4, 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 possa ser 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 herdará o descritor de arquivo quatro e o 3>&-herdará o descritor de 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 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.


Parece interessante, mas não consigo entender o que você espera que esse comando faça, e meu computador também não; Eu entendo -bash: 3: Bad file descriptor.
G-Man

@ G-Man Certo, continuo esquecendo que o bash não faz ideia do que está fazendo quando se trata de descritores de arquivos, ao contrário das conchas que normalmente uso (as cinzas que acompanham o busybox). Avisarei quando pensar em uma solução alternativa que faça o bash feliz. Enquanto isso, se você tem uma caixa debian à mão, pode experimentá-la no painel, ou se você tem a busybox à mão, pode experimentá-la com o busybox ash / sh.
Mtraceur

@ G-Man Quanto ao que eu espero que o comando faça, e o que ele faz em outros shells, é redirecionar stdout do command1 para que não seja pego pela substituição do comando, mas uma vez fora da substituição do comando, ele cai. O fd3 volta ao stdout, por isso é canalizado conforme o esperado command2. Quando o comando1 sai, o printf dispara e imprime seu status de saída, que é capturado na variável pela substituição do comando. Detalhes muito detalhados aqui: stackoverflow.com/questions/985876/tee-and-exit-status/… Além disso, esse seu comentário é lido como se fosse um insulto?
precisa saber é o seguinte

Por onde devo começar? (1) Sinto muito se você se sentiu insultado. "Parece interessante" foi feito com sinceridade; seria ótimo se algo tão compacto quanto isso funcionasse tão bem quanto você esperava. Além disso, eu estava dizendo, simplesmente, que não entendia o que sua solução deveria estar fazendo. Eu trabalho / brinco com o Unix há muito tempo (desde antes da existência do Linux) e, se eu não entendo alguma coisa, é uma bandeira vermelha que, talvez, outras pessoas também não entendam, e que precisa de mais explicações (IMNSHO). ... (continua)
G-Man

(Continua)… Como você “… gosta de pensar… que [entende] quase tudo além da pessoa comum”, talvez você deva se lembrar que o objetivo do Stack Exchange não é ser um serviço de escrita de comando, agitando milhares de soluções pontuais para perguntas trivialmente distintas; mas antes ensinar as pessoas a resolver seus próprios problemas. E, para esse fim, talvez você precise explicar as coisas suficientemente bem para que uma “pessoa comum” possa entendê-las. Veja a resposta de lesmana para um exemplo de uma excelente explicação. ... (continua)
G-Man

11

Se você tiver o pacote moreutils instalado, poderá usar o utilitário mispipe , que faz exatamente o que você pediu.


7

A solução de lesmana acima também pode ser feita sem a sobrecarga de iniciar subprocessos aninhados usando-o { .. }(lembrando que essa forma de comandos agrupados sempre tem que terminar com ponto e vírgula). Algo assim:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

Eu verifiquei essa construção com a versão de traço 0.5.5 e as versões 3.2.2 e 4.2.42 do bash; portanto, mesmo que alguns shells não suportem { .. }agrupamento, ele ainda é compatível com POSIX.


Isso funciona muito bem com a maioria dos shells que eu tentei, incluindo o NetBSD sh, pdksh, mksh, dash, bash. No entanto, não consigo fazê-lo funcionar com o AT&T Ksh (93s +, 93u +) ou zsh (4.3.9, 5.2), mesmo com o set -o pipefailksh ou com qualquer número de waitcomandos espalhados . Eu acho que pode, pelo menos em parte, ser um problema de análise para o ksh, como se eu continuasse usando subshells, então funcionaria bem, mas mesmo com um ifpara escolher a variante do subshell para o ksh, mas deixar os comandos compostos para os outros, ele falhará .
Greg A. Woods,

4

Isso é portátil, ou seja, funciona com qualquer shell compatível com POSIX, não requer que o diretório atual seja gravável e permite que vários scripts usando o mesmo truque sejam executados simultaneamente.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Edit: aqui está uma versão mais forte após os comentários de Gilles:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: e aqui está uma variante um pouco mais leve após o comentário dubiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))

3
Isso não funciona por vários motivos. 1. O arquivo temporário pode ser lido antes de ser gravado. 2. Criar um arquivo temporário em um diretório compartilhado com um nome previsível é inseguro (DoS trivial, corrida de links simbólicos). 3. Se o mesmo script usar esse truque várias vezes, ele sempre usará o mesmo nome de arquivo. Para resolver 1, leia o arquivo após a conclusão do pipeline. Para resolver 2 e 3, use um arquivo temporário com um nome gerado aleatoriamente ou em um diretório privado.
Gilles

+1 Bem, o $ {PIPESTATUS [0]} é mais fácil, mas a idéia básica aqui funciona se alguém souber dos problemas mencionados por Gilles.
9119 Johan

Você pode salvar alguns subshells: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Eu concordo que é mais fácil com o Bash, mas em alguns contextos, saber como evitar o Bash vale a pena.
precisa saber é o seguinte

4

A seguir, um complemento para a resposta do @Patrik, caso você não consiga usar uma das soluções comuns.

Esta resposta assume o seguinte:

  • Você tem uma concha que não conhece $PIPESTATUSnemset -o pipefail
  • Você deseja usar um canal para execução paralela, portanto, não há arquivos temporários.
  • Você não deseja ter mais confusão se interromper o script, possivelmente por uma queda de energia repentina.
  • Esta solução deve ser relativamente fácil de seguir e limpa para ler.
  • Você não deseja introduzir sub-shell adicionais.
  • Você não pode mexer com os descritores de arquivos existentes, portanto, stdin / out / err não deve ser tocado (no entanto, você pode introduzir temporariamente um novo)

Suposições adicionais. Você pode se livrar de tudo, mas isso prejudica muito a receita, por isso não é abordada aqui:

  • Tudo o que você deseja saber é que todos os comandos no PIPE possuem o código de saída 0.
  • Você não precisa de informações adicionais sobre a banda lateral.
  • Seu shell espera que todos os comandos de pipe retornem.

Antes:, foo | bar | bazno entanto, isso retorna apenas o código de saída do último comando ( baz)

Procura-se: $?não deve ser 0(verdadeiro), se algum dos comandos no canal falhar

Depois de:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Explicado:

  • Um tempfile é criado com mktemp. Isso geralmente cria imediatamente um arquivo no/tmp
  • Esse arquivo temporário é então redirecionado para o FD 9 para gravação e o FD 8 para leitura
  • O tempfile é excluído imediatamente. Ele permanece aberto, no entanto, até que os dois DFs deixem de existir.
  • Agora o tubo está iniciado. Cada etapa é adicionada apenas ao FD 9, se houver um erro.
  • O waité necessário para ksh, porque ksho resto não esperar por todos os comandos tubo para terminar. No entanto, observe que existem efeitos colaterais indesejados se algumas tarefas em segundo plano estiverem presentes, então eu as comentei por padrão. Se a espera não doer, você pode comentar.
  • Depois, o conteúdo do arquivo é lido. Se estiver vazio (porque tudo funcionou) readretorna false, trueindica um erro

Isso pode ser usado como uma substituição de plug-in para um único comando e precisa apenas do seguinte:

  • FDs não utilizados 9 e 8
  • Uma única variável de ambiente para armazenar o nome do arquivo temporário
  • E essa receita pode ser adaptada a praticamente qualquer shell existente, o que permite o redirecionamento de IO
  • Também é bastante independente de plataforma e não precisa de coisas como /proc/fd/N

Insetos:

Este script possui um bug no caso de /tmpfalta de espaço. Se você precisa de proteção contra este caso artificial, também, você pode fazê-lo da seguinte forma, no entanto, isso tem a desvantagem, que o número de 0em 000depende do número de comandos no tubo, por isso é um pouco mais complicado:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Notas de portabilidade:

  • kshe conchas semelhantes que só esperar para o último comando tubulação precisa do waitdescomentada

  • O último exemplo usa em printf "%1s" "$?"vez de, echo -n "$?"porque é mais portátil. Nem toda plataforma interpreta -ncorretamente.

  • printf "$?"faria isso também, no entanto, printf "%1s"pega alguns casos extremos caso você execute o script em uma plataforma realmente quebrada. (Leia: se você programar paranoia_mode=extreme.)

  • O FD 8 e o FD 9 podem ser maiores em plataformas que suportam vários dígitos. AFAIR, um shell compatível com POSIX, precisa apenas suportar dígitos únicos.

  • Foi testado com Debian 8.2 sh, bash, ksh, ash, sashe até mesmocsh


3

Com um pouco de precaução, isso deve funcionar:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

Que tal limpar como o jlliagre? Você não deixa um arquivo para trás chamado foo-status?
9688 Johan

@ Johan: Se você preferir minha sugestão, não hesite em votar;) Além de não deixar um arquivo, ele tem a vantagem de permitir que múltiplos processos executem isso simultaneamente e o diretório atual não precisa ser gravável.
Jlliagre

2

O seguinte bloco 'if' será executado apenas se 'command' for bem-sucedido:

if command; then
   # ...
fi

Especificamente, você pode executar algo como isto:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

O qual executará haconf -makerwe armazenará seu stdout e stderr em "$ haconf_out". Se o valor retornado de haconffor verdadeiro, o bloco 'if' será executado e grepexibirá "$ haconf_out", tentando compará-lo com "Cluster já gravável".

Observe que os tubos se limpam automaticamente; com o redirecionamento, você terá que ter cuidado para remover "$ haconf_out" quando terminar.

Não é tão elegante quanto pipefail, mas uma alternativa legítima se essa funcionalidade não estiver ao seu alcance.


1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$

0

(Com bash pelo menos) combinado com set -eum pode-se usar subshell para emular explicitamente pipefail e sair no erro de pipe

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Portanto, se foofalhar por algum motivo - o restante do programa não será executado e o script será encerrado com o código de erro correspondente. (Isso pressupõe que fooimprime seu próprio erro, o que é suficiente para entender o motivo da falha)


-1

EDIT : Esta resposta está errada, mas interessante, por isso vou deixar para referência futura.


Adicionar !a ao comando inverte o código de retorno.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #

1
Eu acho que isso não tem relação. No seu exemplo, eu quero saber o código de saída de ls, não inverter o código de saída debogus_command
Michael Mrozek

2
Sugiro recuar essa resposta.
maxschlepzig

3
Bem, parece que sou um idiota. Na verdade, eu usei isso em um script antes de pensar que ele fazia o que o OP queria. Boa coisa que eu não usá-lo para qualquer coisa importante
Falmarri
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.