Verificando o status de saída do Bash de vários comandos com eficiência

261

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, saia 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 assim:

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

Porque os argumentos de cada comando que acredito (corrija-me se estiver errado) interferirão um no outro. Esses dois métodos me parecem terrivelmente longos e desagradáveis, então estou aqui pedindo um método mais eficiente.

jwbensley
fonte
2
Dar uma olhada para o unofficial modo estrito festa : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -eé uma ideia horrível . Veja os exercícios no BashFAQ # 105 discutindo apenas alguns dos casos inesperados apresentados, e / ou a comparação mostrando incompatibilidades entre diferentes implementações de shells (e versões de shell ') em in-ulm.de/~mascheck/various/set -e .
Charles Duffy

Respostas:

274

Você pode escrever uma função que inicia e testa o comando para você. Suponha command1e command2são variáveis ​​de ambiente que foram configuradas para um comando.

function mytest {
    "$@"
    local status=$?
    if (( status != 0 )); then
        echo "error with $1" >&2
    fi
    return $status
}

mytest "$command1"
mytest "$command2"
krtek
fonte
32
Não use $*, ele falhará se algum argumento tiver espaços; use em "$@"vez disso. Da mesma forma, coloque $1dentro das aspas no echocomando.
Gordon Davisson
82
Além disso, eu evitaria o nome, testpois esse é um comando interno.
John Kugelman
1
Este é o método que eu segui. Para ser sincero, acho que não fui claro o suficiente na minha postagem original, mas esse método permite que eu escreva minha própria função 'teste' para que eu possa executar ações de erro. o script. Obrigado :)
jwbensley 26/03
7
O código de saída retornado por test () não retornaria sempre 0 em caso de erro, pois o último comando executado foi 'eco'. Pode ser necessário salvar o valor de $? primeiro.
Magiconair 2/11
2
Esta não é uma boa ideia e incentiva práticas inadequadas. Considere o caso simples de ls. Se você chamar ls fooe receber uma mensagem de erro do formulário, ls: foo: No such file or directory\nentenderá o problema. Se, em vez disso, você ls: foo: No such file or directory\nerror with ls\nse distrair com informações supérfluas. Nesse caso, é fácil argumentar que a superfluidade é trivial, mas cresce rapidamente. Mensagens de erro concisas são importantes. Mais importante, porém, esse tipo de wrapper incentiva gravadores demais a omitir completamente boas mensagens de erro.
William Pursell
185

O que você quer dizer com "desistir e repetir o erro"? Se você quer dizer que deseja que o script seja finalizado assim que qualquer comando falhar, basta

set -e    # DON'T do this.  See commentary below.

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 lidar com isso. Em outras palavras, se você fizer:

#!/bin/sh

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

e command2 falhar, ao imprimir uma mensagem de erro no stderr, parece que você conseguiu o que deseja. (A menos que eu interprete mal o que você quer!)

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

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

 #!/bin/sh

 command1 || exit
 command2 || exit
 command3 || exit

ou

#!/bin/sh

command1 && command2 && command3
William Pursell
fonte
1
Esteja ciente de que, embora esta solução seja a mais simples, ela não permite a limpeza em caso de falha.
Josh J
6
A limpeza pode ser realizada com armadilhas. (por exemplo, trap some_func 0será executado some_funcna saída)
William Pursell
3
Observe também que a semântica do errexit (set -e) mudou em diferentes versões do bash e geralmente se comportará inesperadamente durante a invocação de funções e outras configurações. Não recomendo mais o seu uso. Na IMO, é melhor escrever || exitexplicitamente após cada comando.
William Pursell
87

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

Opcionalmente, você pode definir a $LOG_STEPSvariável como um nome de arquivo de log se desejar registrar quais comandos falharão.

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

Resultado

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
}
John Kugelman
fonte
isso é ouro puro. Embora eu compreenda como usar o script, não entendo completamente cada etapa, definitivamente fora do meu conhecimento de script do bash, mas acho que é uma obra de arte.
kingmilo
2
Essa ferramenta tem um nome formal? Eu adoraria ler uma página do manual sobre este estilo de step / try / próxima logging
ThorSummoner
Essas funções do shell parecem não estar disponíveis no Ubuntu? Eu estava esperando para usar isso, algo portátil-ish embora
ThorSummoner 21/15/15
@ThorSummoner, isso é provável porque o Ubuntu usa o Upstart em vez do SysV init, e em breve estará usando o systemd. O RedHat tende a manter a compatibilidade com versões anteriores por muito tempo, e é por isso que o material init.d ainda está lá.
precisa saber é o seguinte
Publiquei uma expansão na solução de John e permite que ela seja usada em sistemas não-RedHat como o Ubuntu. Veja stackoverflow.com/a/54190627/308145
Mark Thomson
51

Pelo que vale, uma maneira mais curta de escrever código para verificar se cada comando é bem-sucedido é:

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

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

John Kugelman
fonte
Não penso nisso, não é o método que eu fui com, mas ele é rápido e fácil de ler, obrigado pela informação :)
jwbensley
3
Para executar os comandos em silêncio e conseguir a mesma coisa:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Sou fã desse método, existe uma maneira de executar vários comandos após a sala de cirurgia? Algo comocommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Uma alternativa é simplesmente unir os comandos, &&para que o primeiro a falhar impeça a execução do restante:

command1 &&
  command2 &&
  command3

Essa não é a sintaxe solicitada na pergunta, mas é um padrão comum para o caso de uso que você descreve. Em geral, os comandos devem ser responsáveis ​​pelas falhas de impressão, para que você não precise fazer isso manualmente (talvez com um -qsinalizador para silenciar os erros quando não os desejar). Se você tem a capacidade de modificar esses comandos, eu os edito 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

E quando você fazer necessidade de verificar os códigos de retorno usar um contexto aritmética em vez de [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414
fonte
34

Em vez de criar funções de corredor 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 acesso ao número da linha e à linha de comando do comando que a acionou. As variáveis ​​são $BASH_LINENOe $BASH_COMMAND.

Pausado até novo aviso.
fonte
4
Se você deseja imitar um bloco try ainda mais de perto, use trap - ERRpara desativar a armadilha no final do "bloco".
Gordon Davisson
14

Pessoalmente, prefiro usar uma abordagem leve, como pode ser 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"
sonolento
fonte
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
fonte
6
Não corra $*, ele falhará se algum argumento tiver espaços neles; use em "$@"vez disso. (Embora $ * esteja ok no echocomando).
Gordon Davisson
6

Desenvolvi uma implementação try & catch quase perfeita 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 faz parte do meu boilerplate / framework do bash . Além disso, amplia a idéia de tentar e capturar coisas como manipulação de erros com backtrace e exceções (além de outros recursos interessantes).

Aqui está o código responsável apenas pelo try & catch:

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 livre para usar, usar e contribuir - é no GitHub .

niieani
fonte
1
Eu olhei para o repositório e não vou usar isso sozinho, porque é muito mágico para o meu gosto (para a IMO é melhor usar o Python se precisar de mais poder de abstração), mas definitivamente um grande +1 de mim porque parece incrível.
Alexander Malakhov
Obrigado pelas amáveis ​​palavras @AlexanderMalakhov. Concordo com a quantidade de "mágica" - essa é uma das razões pelas quais estamos discutindo uma versão simplificada do framework 3.0, que será muito mais fácil de entender, depurar etc. Há um problema em aberto sobre o 3.0 no GH, se você gostaria de cravar seus pensamentos.
Niieani 8/07
3

Desculpe por não poder comentar a primeira resposta. Mas você deve usar uma nova instância 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
umount
fonte
2

Para usuários de casca de peixe que tropeçam nesse segmento.

Let fooSer uma função que não "retorna" (eco) um valor, mas define o código de saída como de costume.
Para evitar a verificação $statusapós chamar a função, você pode:

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
Dennis
fonte
1

Quando uso ssh, preciso diferenciar os problemas causados ​​por problemas de conexão e os códigos de erro do comando remoto no modo errexit( set -e). Eu uso a seguinte função:

# 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
Tomilov Anatoliy
fonte
1

Você pode usar a incrível solução de @ john-kugelman encontrada acima em sistemas não-RedHat, comentando esta linha em seu código:

. /etc/init.d/functions

Em seguida, cole o código abaixo no final. Divulgação completa: Esta é apenas uma cópia e colagem direta dos bits relevantes do arquivo acima mencionado, extraídos do Centos 7.

Testado no MacOS e Ubuntu 18.04.


BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"

echo_success() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
    echo -n $"  OK  "
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 0
}

echo_failure() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
    echo -n $"FAILED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_passed() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"PASSED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_warning() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"WARNING"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
} 
Mark Thomson
fonte
0

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

Resultado

Status is 127
eslavo
fonte