Eu tenho dois processos foo
e bar
, conectado com um pipe:
$ foo | bar
bar
sempre sai 0; Estou interessado no código de saída foo
. Existe alguma maneira de chegar lá?
Eu tenho dois processos foo
e bar
, conectado com um pipe:
$ foo | bar
bar
sempre 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 PIPESTATUS
variá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 bash
ou zsh
e você obterá os mesmos resultados; apenas um de retval_bash
e retval_zsh
será definido. O outro ficará em branco. Isso permitiria que uma função terminasse return $retval_bash $retval_zsh
(observe a falta de aspas!).
pipestatus
no 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 pipefail
opção ( ksh
, zsh
ou 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
( $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 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 $PIPESTATUS
ou algo semelhante. Ele suporta a pipefail
opçã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 someprog
saí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 filter
como stdout da construção e o status de someprog
saí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 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 arquivo aberto do pai. Isso significa someprog
que 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 com a someprog
possibilidade 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 someprog
fecha o descritor de arquivo antes de executar, someprog
portanto, para someprog
esses 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 $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.#part2
e, por sua vez, será o status de saída de toda a construção.#part5
e #part6
) e à direita ( filter >&4
) são executados. A saída de filter
é redirecionada para o descritor de arquivo 4. No #part1
descritor 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 #part3
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.someprog
É executado. O status de saída é recebido #part5
. O stdout é levado pelo tubo #part4
e 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 pipefail
dentro do script deve ser mais robusto, por exemplo, caso alguém execute o script via bash foo.sh
.
-o pipefail
nã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
- getopt
de 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-pf
fizesse isso exec /bin/bash -o pipefail "$@"
e colocasse em #!
O que faço quando possível é alimentar o código de saída de foo
para bar
. Por exemplo, se eu sei que foo
nunca 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 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 começar bar
a 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_codes
geralmente foo:X bar:Y
, mas pode ser bar:Y foo:X
se for bar
encerrado 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 bar
nunca 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_statuses
nos meus testes; sem eles grep
nã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>&1
fizemos 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>&1
Tem 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>&1
herdará 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 pipefail
ksh ou com qualquer número de wait
comandos 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 if
para 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:
$PIPESTATUS
nemset -o pipefail
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 | baz
no 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/tmp
wait
é necessário para ksh
, porque ksh
o 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.read
retorna false
, true
indica um erroIsso pode ser usado como uma substituição de plug-in para um único comando e precisa apenas do seguinte:
/proc/fd/N
Insetos:
Este script possui um bug no caso de /tmp
falta 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 0
em 000
depende 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:
ksh
e conchas semelhantes que só esperar para o último comando tubulação precisa do wait
descomentada
O último exemplo usa em printf "%1s" "$?"
vez de, echo -n "$?"
porque é mais portátil. Nem toda plataforma interpreta -n
corretamente.
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
, sash
e 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 -makerw
e armazenará seu stdout e stderr em "$ haconf_out". Se o valor retornado de haconf
for verdadeiro, o bloco 'if' será executado e grep
exibirá "$ 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 -e
um 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 foo
falhar 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 foo
imprime 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