Eu tenho dois processos fooe bar, conectado com um pipe:
$ foo | bar
barsempre sai 0; Estou interessado no código de saída foo. Existe alguma maneira de chegar lá?
Eu tenho dois processos fooe bar, conectado com um pipe:
$ foo | bar
barsempre sai 0; Estou interessado no código de saída foo. Existe alguma maneira de chegar lá?
Respostas:
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!).
pipestatusno zsh. Infelizmente, outras conchas não possuem esse recurso.
echo "$pipestatus[1]" "$pipestatus[2]".
if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
Existem 3 maneiras comuns de fazer isso:
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
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.
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
ksh, mas, de uma breve olhada na página de manual, ele não suporta $PIPESTATUSou algo semelhante. Ele suporta a pipefailopção embora.
LOG=$(failed_command | successful_command)
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:
#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.#part2e, por sua vez, será o status de saída de toda a construção.#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.#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.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(read; exit $REPLY)
(exec 3>&- 4>&-; someprog)simplifica para someprog 3>&- 4>&-.
{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
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
set -o pipefaildentro do script deve ser mais robusto, por exemplo, caso alguém execute o script via bash foo.sh.
-o pipefailnão está no POSIX.
#!/bin/bash -o pipefail. O erro é:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
#!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 #!
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:
/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")
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.
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.
-bash: 3: Bad file descriptor.
Se você tiver o pacote moreutils instalado, poderá usar o utilitário mispipe , que faz exatamente o que você pediu.
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.
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á .
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))
(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.
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:
$PIPESTATUSnemset -o pipefailSuposiçõ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:
mktemp. Isso geralmente cria imediatamente um arquivo no/tmpwaité 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.readretorna false, trueindica um erroIsso pode ser usado como uma substituição de plug-in para um único comando e precisa apenas do seguinte:
/proc/fd/NInsetos:
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
Com um pouco de precaução, isso deve funcionar:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
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.
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
$
(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)
EDIT : Esta resposta está errada, mas interessante, por isso vou deixar para referência futura.
!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.
# =========================================================== #
ls, não inverter o código de saída debogus_command