Verificar o status de saída do Bash de vários comandos de maneira eficiente

Existe algo semelhante ao pipefail para vários comandos, como uma instrução ‘try’, mas dentro do bash. Eu gostaria de fazer algo assim:

echo "trying stuff" try { command1 command2 command3 } 

E a qualquer momento, se algum comando falhar, retire-se e repita o erro desse comando. Eu não quero ter que fazer algo como:

 command1 if [ $? -ne 0 ]; then echo "command1 borked it" fi command2 if [ $? -ne 0 ]; then echo "command2 borked it" fi 

E assim por diante … ou algo do tipo:

 pipefail -o command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3 

Porque os argumentos de cada comando que eu acredito (me corrijam se eu estiver errado) irão interferir um no outro. Esses dois methods parecem horrivelmente longos e desagradáveis ​​para mim, então estou aqui apelando para um método mais eficiente.

Você pode escrever uma function que lança e testa o comando para você. Suponha que command2 e command2 sejam variables ​​de ambiente que foram configuradas para um comando.

 function mytest { "$@" local status=$? if [ $status -ne 0 ]; then echo "error with $1" >&2 fi return $status } mytest $command1 mytest $command2 

O que você quer dizer com “desistir e ecoar o erro”? Se você quer dizer que quer que o script termine assim que algum comando falhar, então faça

 set -e 

no início do script (mas observe o aviso abaixo). Não se preocupe em repetir a mensagem de erro: deixe o comando com falha manipular isso. Em outras palavras, se você fizer:

 #!/bin/sh set -e # Use caution. eg, don't do this command1 command2 command3 

e command2 falhar, enquanto imprime uma mensagem de erro para stderr, então parece que você conseguiu o que deseja. (A menos que eu interprete mal o que você quer!)

Como um corolário, qualquer comando que você escrever deve se comportar bem: ele deve relatar erros para stderr em vez de stdout (o código de amostra na pergunta imprime erros para stdout) e deve sair com um status diferente de zero quando falhar.

No entanto, não considero mais que isso seja uma boa prática. set -e mudou sua semântica com diferentes versões do bash, e embora funcione bem para um script simples, existem muitos casos extremos que são essencialmente inutilizáveis. (Considere coisas como: set -e; foo() { false; echo should not print; } ; foo && echo ok A semântica aqui é um pouco razoável, mas se você refatorar o código em uma function que dependia da configuração de opção para terminar , você pode facilmente ser mordido.) IMO é melhor escrever:

  #!/bin/sh command1 || exit command2 || exit command3 || exit 

ou

 #!/bin/sh command1 && command2 && command3 

Eu tenho um conjunto de funções de script que uso extensivamente no meu sistema Red Hat. Eles usam as funções do sistema de /etc/init.d/functions para imprimir indicadores de status verdes [ OK ] e vermelhos [FAILED] .

Opcionalmente, você pode definir a variável $LOG_STEPS como um nome de arquivo de log se quiser registrar quais comandos falham.

Uso

 step "Installing XFS filesystem tools:" try rpm -i xfsprogs-*.rpm next step "Configuring udev:" try cp *.rules /etc/udev/rules.d try udevtrigger next step "Adding rc.postsysinit hook:" try cp rc.postsysinit /etc/rc.d/ try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit try echo $'\nexec /etc/rc.postsysinit' >> /etc/rc.sysinit next 

Saída

 Installing XFS filesystem tools: [ OK ] Configuring udev: [FAILED] Adding rc.postsysinit hook: [ OK ] 

Código

 #!/bin/bash . /etc/init.d/functions # Use step(), try(), and next() to perform a series of commands and print # [ OK ] or [FAILED] at the end. The step as a whole fails if any individual # command fails. # # Example: # step "Remounting / and /boot as read-write:" # try mount -o remount,rw / # try mount -o remount,rw /boot # next step() { echo -n "$@" STEP_OK=0 [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ } try() { # Check for `-b' argument to run command in the background. local BG= [[ $1 == -b ]] && { BG=1; shift; } [[ $1 == -- ]] && { shift; } # Run the command. if [[ -z $BG ]]; then "$@" else "$@" & fi # Check if command failed and update $STEP_OK if so. local EXIT_CODE=$? if [[ $EXIT_CODE -ne 0 ]]; then STEP_OK=$EXIT_CODE [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$ if [[ -n $LOG_STEPS ]]; then local FILE=$(readlink -m "${BASH_SOURCE[1]}") local LINE=${BASH_LINENO[0]} echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS" fi fi return $EXIT_CODE } next() { [[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; } [[ $STEP_OK -eq 0 ]] && echo_success || echo_failure echo return $STEP_OK } 

Por que vale a pena, uma forma mais curta de escrever código para verificar cada comando em busca de sucesso é:

 command1 || echo "command1 borked it" command2 || echo "command2 borked it" 

Ainda é tedioso, mas pelo menos é legível.

Uma alternativa é simplesmente unir os comandos junto com && para que o primeiro a falhar evite que o resto seja executado:

 command1 && command2 && command3 

Esta não é a syntax que você pediu na pergunta, mas é um padrão comum para o caso de uso que você descreve. Em geral, os comandos devem ser responsáveis ​​por falhas de impressão, para que você não precise fazer isso manualmente (talvez com um sinalizador -q para silenciar os erros quando você não os quiser). Se você tiver a capacidade de modificar esses comandos, eu os editaria para gritar com falha, em vez de envolvê-los em outra coisa que o faça.


Observe também que você não precisa fazer:

 command1 if [ $? -ne 0 ]; then 

Você pode simplesmente dizer:

 if ! command1; then 

Em vez de criar funções de runner ou usar set -e , use um trap :

 trap 'echo "error"; do_cleanup failed; exit' ERR trap 'echo "received signal to stop"; do_cleanup interrupted; exit' SIGQUIT SIGTERM SIGINT do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; } command1 command2 command3 

A armadilha ainda tem access ao número da linha e à linha de comando do comando que a acionou. As variables ​​são $BASH_LINENO e $BASH_COMMAND .

Pessoalmente prefiro muito mais usar uma abordagem leve, como visto aqui ;

 yell() { echo "$0: $*" >&2; } die() { yell "$*"; exit 111; } try() { "$@" || die "cannot $*"; } asuser() { sudo su - "$1" -c "${*:2}"; } 

Exemplo de uso:

 try apt-fast upgrade -y try asuser vagrant "echo 'uname -a' >> ~/.profile" 
 run() { $* if [ $? -ne 0 ] then echo "$* failed with exit code $?" return 1 else return 0 fi } run command1 && run command2 && run command3 

Eu desenvolvi uma implementação quase sem falhas try & catch no bash, que permite escrever código como:

 try echo 'Hello' false echo 'This will not be displayed' catch echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!" 

Você pode até aninhar os blocos try-catch dentro de si!

 try { echo 'Hello' try { echo 'Nested Hello' false echo 'This will not execute' } catch { echo "Nested Caught (@ $__EXCEPTION_LINE__)" } false echo 'This will not execute too' } catch { echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!" } 

O código é uma parte do meu boilerplate / framework bash . Ele amplia ainda mais a idéia de tentar e pegar com coisas como manipulação de erros com backtrace e exceções (além de alguns outros resources interessantes).

Aqui está o código que é responsável apenas por tentar e pegar:

 set -o pipefail shopt -s expand_aliases declare -ig __oo__insideTryCatch=0 # if try-catch is nested, then set +e before so the parent handler doesn't catch us alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e; __oo__insideTryCatch+=1; ( set -e; trap \"Exception.Capture \${LINENO}; \" ERR;" alias catch=" ); Exception.Extract \$? || " Exception.Capture() { local script="${BASH_SOURCE[1]#./}" if [[ ! -f /tmp/stored_exception_source ]]; then echo "$script" > /tmp/stored_exception_source fi if [[ ! -f /tmp/stored_exception_line ]]; then echo "$1" > /tmp/stored_exception_line fi return 0 } Exception.Extract() { if [[ $__oo__insideTryCatch -gt 1 ]] then set -e fi __oo__insideTryCatch+=-1 __EXCEPTION_CATCH__=( $(Exception.GetLastException) ) local retVal=$1 if [[ $retVal -gt 0 ]] then # BACKWARDS COMPATIBILE WAY: # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}" # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}" export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}" export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}" export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}" return 1 # so that we may continue with a "catch" fi } Exception.GetLastException() { if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]] then cat /tmp/stored_exception cat /tmp/stored_exception_line cat /tmp/stored_exception_source else echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}" fi rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source return 0 } 

Sinta-se à vontade para usar, bifurcar e contribuir – está no GitHub .

Desculpe por não poder fazer um comentário para a primeira resposta Mas você deve usar new instance para executar o comando: cmd_output = $ ($ @)

 #!/bin/bash function check_exit { cmd_output=$($@) local status=$? echo $status if [ $status -ne 0 ]; then echo "error with $1" >&2 fi return $status } function run_command() { exit 1 } check_exit run_command 

Para usuários de shell de peixe que tropeçam neste tópico.

Seja foo uma function que não “retorna” (echo) um valor, mas define o código de saída como de costume.
Para evitar verificar o $status depois de chamar a function, você pode fazer:

 foo; and echo success; or echo failure 

E se for muito longo para caber em uma linha:

 foo; and begin echo success end; or begin echo failure end 

Quando eu uso o ssh eu preciso distinguir entre problemas causados ​​por problemas de conexão e códigos de erro do comando remoto no modo errexit ( set -e ). Eu uso a seguinte function:

 # prepare environment on calling site: rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip" function exit255 { local flags=$- set +e "$@" local status=$? set -$flags if [[ $status == 255 ]] then exit 255 else return $status fi } export -f exit255 # callee: set -e set -o pipefail [[ $rssh ]] [[ $remote_ip ]] [[ $( type -t exit255 ) == "function" ]] rjournaldir="/var/log/journal" if exit255 $rssh "[[ ! -d '$rjournaldir/' ]]" then $rssh "mkdir '$rjournaldir/'" fi rconf="/etc/systemd/journald.conf" if [[ $( $rssh "grep '#Storage=auto' '$rconf'" ) ]] then $rssh "sed -i 's/#Storage=auto/Storage=persistent/' '$rconf'" fi $rssh systemctl reenable systemd-journald.service $rssh systemctl is-enabled systemd-journald.service $rssh systemctl restart systemd-journald.service sleep 1 $rssh systemctl status systemd-journald.service $rssh systemctl is-active systemd-journald.service 

Verificando o status de maneira funcional

 assert_exit_status() { lambda() { local val_fd=$(echo $@ | tr -d ' ' | cut -d':' -f2) local arg=$1 shift shift local cmd=$(echo $@ | xargs -E ':') local val=$(cat $val_fd) eval $arg=$val eval $cmd } local lambda=$1 shift eval $@ local ret=$? $lambda : <(echo $ret) } 

Uso:

 assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls 

Saída

 Status is 127