Obtenha o status de saída do processo que é canalizado para outro

235

Eu tenho dois processos foo e bar , conectados a um canal:

$ foo | bar

bar sempre sai 0; Estou interessado no código de saída de foo . Existe alguma maneira de chegar a ele?

    
por Michael Mrozek 02.06.2011 / 22:19

15 respostas

206

Se você estiver usando bash , poderá usar a variável PIPESTATUS array 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 dentro de uma função de 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 bash ou zsh e você obterá os mesmos resultados; somente um dos retval_bash e retval_zsh será definido. O outro ficará em branco. Isso permitiria que uma função terminasse com return $retval_bash $retval_zsh (observe a falta de citações!).

    
por 02.06.2011 / 22:49
204

Existem 3 formas comuns de o fazer:

Pipefail

A primeira maneira é definir a opção pipefail ( ksh , zsh ou bash ). Isto é o mais simples eo que faz é basicamente definir o status de saída $? para o código de saída do último programa para sair de um valor diferente de zero (ou zero se todos tiverem saído com sucesso).

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

$ PIPESTATUS

O Bash também tem uma variável de matriz chamada $PIPESTATUS ( $pipestatus in 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 de que precisa.

Execuções separadas

Esta é a solução mais difícil de gerenciar. 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
    
por 21.04.2013 / 15:56
40

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

Situação:

someprog | filter

você quer o status de saída de someprog 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 $?

Exemplo someprog e 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 arquivos abertos do pai. Isso significa que someprog herdará o descritor de arquivo aberto 3 e 4. Se someprog gravar no descritor de arquivo 3, esse será o status de saída. O status de saída real será ignorado porque read apenas lê uma vez.

Se você se preocupa que o someprog possa gravar no descritor de arquivo 3 ou 4, é melhor fechar os descritores de arquivos antes de chamar someprog .

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

O exec 3>&- 4>&- antes de someprog fecha o descritor de arquivo antes de executar someprog , então, para someprog , esses descritores de arquivo simplesmente não existem.

Explicação passo a passo do constructo:

( ( ( ( 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 arquivos 4 redirecionado para stdout. Isso significa que tudo o que for impresso no descritor de arquivo 4 na subshell terminará como o stdout de toda a construção.
  2. Um canal é criado e os comandos à esquerda ( #part3 ) e à direita ( #part2 ) são executados. exit $xs també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 stdout. Isso significa que o que for impresso no descritor de arquivo 3 dessa sub-linha terminará em #part2 e, por sua vez, será o status de saída de toda a construção.
  4. Um canal é criado e os comandos à esquerda ( #part5 e #part6 ) e à direita ( filter >&4 ) são executados. A saída de filter é redirecionada para o descritor de arquivo 4. Em #part1 , o descritor de arquivo 4 foi redirecionado para stdout. Isso significa que a saída de filter é a stdout de toda a construção.
  5. O status de saída de #part6 é impresso no descritor de arquivo 3. Em #part3 , o descritor de arquivo 3 foi redirecionado para #part2 . Isso significa que o status de saída de #part6 será o status de saída final para toda a construção.
  6. someprog é executado. O status de saída é obtido em #part5 . O stdout é obtido pelo pipe em #part4 e encaminhado para filter . A saída de filter , por sua vez, atingirá stdout conforme explicado em #part4
por 31.03.2013 / 12:08
34

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

#!/bin/bash -o pipefail

para que seus pipes retornem o último retorno não zero.

pode ser um pouco menos de codificação

Editar: exemplo

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
    
por 02.06.2011 / 23:59
22

O que eu faço quando possível é alimentar o código de saída de foo em bar . Por exemplo, se eu souber que foo nunca produz uma linha com apenas dígitos, posso inserir apenas o código de saída:

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

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

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

Isso sempre pode ser feito se houver alguma maneira de fazer com que bar trabalhe em todos, menos na última linha, e passe a última linha como seu código de saída.

Se bar for 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_codes geralmente é foo:X bar:Y , mas pode ser bar:Y foo:X se bar sair antes de ler todas as suas entradas ou se você tiver azar. Acho que as gravações em pipes de até 512 bytes são atômicas em todos os unices, portanto, as partes foo:$? e bar:$? não serão mescladas, contanto que as strings da tag tenham menos de 507 bytes.

Se você precisar capturar a saída de bar , ficará difícil. Você pode combinar as técnicas acima, organizando a saída de bar para nunca conter uma linha que se pareça com uma indicação de código de saída, mas 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 usando um arquivo temporário para armazenar o status. Simples, mas não isso 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 lugares, você precisará 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. Geralmente, /tmp é o único lugar em que um script pode gravar arquivos. Use mktemp , que não é POSIX, mas disponível em todos os graves unices hoje em dia.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
    
por 03.06.2011 / 02:33
17

A partir do pipeline:

foo | bar | baz

Aqui está uma solução geral usando apenas o 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 aspas em torno de $error_statuses em meus testes; sem eles grep não pode diferenciar porque as novas linhas são coagidas para espaços.

    
por 15.07.2011 / 09:02
11

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 - command1 executará e imprimirá sua saída regular em stdout (descritor de arquivo 1), então uma vez feito, printf executará e imprimirá o código de saída do comando1 em seu stdout, mas stdout é redirecionado para o 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 o faz para o comando2 porque o enviamos para o descritor de arquivo 3 em vez de 1, que é o que o pipe lê). Então, redirecionamos a saída do comando2 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 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 bit final de mágica é 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 irá capturar 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. uma vez que ele "saia" da substituição do comando, ele ainda estará efetivamente indo para o descritor de arquivo geral do script 1.

(O exec 4>&1 precisa 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 é aberta no comando "external" que está usando o comando 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 nas outras: command1 pipes para command2, então a saída do printf salta sobre o comando 2 para que o comando2 não o capture e, em seguida, 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 é 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 pipe, porque as atribuições de variáveis, as substituições de comandos e os comandos compostos são efetivamente transparentes para o código de retorno do comando dentro deles , então o status de retorno do comando2 deve ser propagado - isso, e não ter que definir uma função adicional, é por isso que eu acho que essa pode ser uma solução um pouco melhor do que a proposta pela lesmana.

De acordo com as ressalvas que o lesmana menciona, é possível que o comando1 acabe usando os descritores de arquivo 3 ou 4, então, para ser mais robusto, você faria:

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

Note que eu uso comandos compostos no meu exemplo, mas subshells (usando ( ) ao invés de { } também funcionarão, 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 4>&- interno próximo ao 3>&- , mas eu entendo por que não limitar seu escopo o máximo possível.

Não sei com que frequê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, eu acho (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). Então, o último é provavelmente o melhor para se manter em mente e usar para casos gerais.

    
por 05.06.2015 / 06:43
10

Se você tem o pacote moreutils instalado, pode usar o utilitário mispipe , que faz exatamente o que você perguntou.

    
por 08.06.2013 / 13:22
6

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

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

Eu verifiquei este constructo com a versão 0.5.5 do traço e as versões 3.2.25 e 4.2.42 do bash, portanto, mesmo que alguns shells não suportem { .. } grouping, ele ainda é compatível com POSIX.

    
por 22.11.2013 / 13:50
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/_$$))

Editar: aqui está uma versão mais strong seguindo os comentários de Gilles:

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

Editar2: e aqui está uma variante um pouco mais leve seguindo o comentário dubiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
    
por 03.06.2011 / 01:17
4

O seguinte é um complemento da resposta do @Patrik, caso você não consiga usar uma das soluções comuns.

Esta resposta assume o seguinte:

  • Você tem um shell que não conhece $PIPESTATUS nor set -o pipefail
  • Você deseja usar um canal para execução paralela, sem arquivos temporários.
  • Você não deseja mais confusão se interromper o script, possivelmente por uma queda repentina de energia.
  • Esta solução deve ser relativamente fácil de seguir e limpar para ler.
  • Você não deseja introduzir subpastos 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 um novo temporariamente)

Additional assumptions. You can get rid of all, but this clobbers the recipe too much, so it is not covered here:

  • All you want to know is that all commands in the PIPE have exit code 0.
  • You do not need additional side band information.
  • Your shell does wait for all pipe commands to return.

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

Desejado: $? não deve ser 0 (true), se algum dos comandos no pipe falhar

Depois:

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 arquivo temporário é criado com mktemp . Isso geralmente cria imediatamente um arquivo em /tmp
  • Este tempfile é então redirecionado para o FD 9 para gravação e o FD 8 para leitura
  • Em seguida, o arquivo temporário é imediatamente excluído. Ele permanece aberto, até que ambos os FDs deixem de existir.
  • Agora o pipe é iniciado. Cada etapa é adicionada apenas ao FD 9, se houver um erro.
  • O wait é necessário para ksh , porque ksh else não espera que todos os comandos de canal terminem. No entanto, por favor, note que existem efeitos secundários indesejados, se algumas tarefas em segundo plano estão presentes, então eu comentei por padrão. Se a espera não doer, você pode comentá-la.
  • Depois, o conteúdo do arquivo é lido. Se estiver vazio (porque tudo funcionou) read retorna false , então true indica um erro

Isso pode ser usado como um substituto de plug-in para um único comando e só precisa ser seguido:

  • FDs 9 e 8 não utilizados
  • Uma única variável de ambiente para manter o nome do arquivo temporário
  • E esta receita pode ser adaptada para qualquer shell que permita o redirecionamento de IO
  • Também é relativamente independente de plataforma e não precisa de coisas como /proc/fd/N

BUGs:

Este script tem um bug no caso de /tmp ficar sem espaço. Se você precisar de proteção contra este caso artificial, também, você pode fazê-lo da seguinte forma, no entanto, isso tem a desvantagem de que o número de 0 em 000 depende do número de comandos no canal, 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:

  • ksh e shells similares que aguardam apenas o comando last pipe precisam do wait uncommented

  • O último exemplo usa printf "%1s" "$?" em vez de echo -n "$?" porque isso é mais portátil. Nem todas as plataformas interpretam -n corretamente.

  • printf "$?" também, mas printf "%1s" captura alguns casos caso você execute o script em alguma plataforma realmente quebrada. (Leia: se acontecer de você programar em paranoia_mode=extreme .)

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

  • Foi testado com o Debian 8,2 sh , bash , ksh , ash , sash e até csh

por 17.11.2015 / 13:26
3

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

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
    
por 02.06.2011 / 22:40
2

O seguinte bloco 'if' só será executado se 'comando' for bem-sucedido:

if command; then
   # ...
fi

Especificamente falando, você pode executar algo assim:

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

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

Que executará haconf -makerw e armazenará seu stdout e stderr em "$ haconf_out". Se o valor retornado de haconf for true, o bloco 'if' será executado e grep lerá "$ haconf_out", tentando corresponder a "Cluster já gravável".

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

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

    
por 22.04.2013 / 02:37
0
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
$
    
por 27.11.2015 / 01:18
-1

EDITAR : Esta resposta está errada, mas interessante, por isso deixarei para referência futura.

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

link

# =========================================================== #
# 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.
# =========================================================== #
    
por 03.06.2011 / 00:09

Tags