Tratamento de erros no Bash

240

Qual é o seu método favorito para lidar com erros no Bash? O melhor exemplo de manipulação de erros que encontrei na Web foi escrito por William Shotts, Jr em http://www.linuxcommand.org .

Ele sugere usar a seguinte função para tratamento de erros no Bash:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

Você tem uma rotina de tratamento de erros melhor que você usa nos scripts Bash?

Noob
fonte
1
Veja esta resposta detalhada: Aumente o erro em um script Bash .
codeforester
1
Veja a implementação de registro e tratamento de erros aqui: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Respostas:

154

Use uma armadilha!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... então, sempre que você criar um arquivo temporário:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

e $temp_fooserá excluído na saída, e o número da linha atual será impresso. ( set -etambém fornecerá um comportamento de saída com erro, embora venha com sérias advertências e diminua a previsibilidade e portabilidade do código).

Você pode deixar a interceptação chamar errorpor você (nesse caso, ele usa o código de saída padrão 1 e nenhuma mensagem) ou chamá-lo você mesmo e fornecer valores explícitos; por exemplo:

error ${LINENO} "the foobar failed" 2

sairá com o status 2 e fornecerá uma mensagem explícita.

Charles Duffy
fonte
4
@draemon a capitalização variável é intencional. O All-caps é convencional apenas para componentes internos do shell e variáveis ​​de ambiente - o uso de letras minúsculas para todo o resto evita conflitos de namespace. Veja também stackoverflow.com/questions/673055/…
Charles Duffy
1
antes de quebrá-lo novamente, teste sua alteração. As convenções são boas, mas são secundárias ao código em funcionamento.
Draemon
3
@ Demônio, eu realmente discordo. O código obviamente quebrado é percebido e corrigido. Más práticas, mas o código que funciona principalmente, vive para sempre (e é propagado).
Charles Duffy
1
mas você não percebeu. Código quebrado é notado porque o código em funcionamento é a principal preocupação.
Draemon
5
não é exatamente gratuito ( stackoverflow.com/a/10927223/26334 ) e se o código já é incompatível com o POSIX, a remoção da palavra-chave function não torna mais possível a execução no POSIX sh, mas meu ponto principal é que você ' ve (IMO) desvalorizou a resposta enfraquecendo a recomendação de usar set -e. O Stackoverflow não é sobre o código "seu", é sobre ter as melhores respostas.
precisa saber é o seguinte
123

Essa é uma boa solução. Eu só queria adicionar

set -e

como um mecanismo de erro rudimentar. Parará imediatamente o seu script se um comando simples falhar. Eu acho que esse deveria ter sido o comportamento padrão: como esses erros quase sempre significam algo inesperado, não é realmente sensato continuar executando os seguintes comandos.

Bruno De Fraine
fonte
29
set -enão é sem truques : Veja mywiki.wooledge.org/BashFAQ/105 para vários.
26812 Charles Duffy
3
@CharlesDuffy, algumas das armadilhas podem ser superados comset -o pipefail
placa
7
@CharlesDuffy Obrigado por apontar para as dicas; No geral, ainda acho que set -etem uma alta relação custo-benefício.
Bruno De Fraine
3
@BrunoDeFraine Eu uso a set -emim mesmo, mas vários outros usuários regulares do irc.freenode.org # bash aconselham (em termos bastante fortes) contra isso. No mínimo, as dicas em questão devem ser bem compreendidas.
Charles Duffy
3
definir -e -o pipefail -u # e sabe o que está fazendo
Sam Watkins
78

Ler todas as respostas desta página me inspirou muito.

Então, aqui está a minha dica:

conteúdo do arquivo: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



Exemplo de uso:
conteúdo do arquivo: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


Corrida:

bash trap-test.sh

Resultado:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


Como você pode ver na captura de tela abaixo, a saída é colorida e a mensagem de erro é exibida no idioma usado.

insira a descrição da imagem aqui

Luca Borrione
fonte
3
isso é incrível .. você deve criar um projeto no github para que as pessoas possam facilmente fazer melhorias e contribuir de volta. Combinei com o log4bash e, juntos, cria um ambiente poderoso para a criação de bons scripts bash.
Dominik Dorn
1
FYI - test ${#g_libs[@]} == 0não é compatível com POSIX (o teste POSIX suporta =comparações de cadeias ou comparações -eqnuméricas, mas não ==, sem mencionar a falta de matrizes no POSIX) e, se você não está tentando ser compatível com POSIX, por que, no mundo você está usando testem vez de um contexto de matemática? (( ${#g_libs[@]} == 0 ))é, afinal, mais fácil de ler.
Charles Duffy
2
@Luca - isso é realmente ótimo! Sua foto me inspirou a criar minha própria implementação disso, o que leva alguns passos adiante. Eu postei na minha resposta abaixo .
Niieani 3/15
3
Bravissimo !! Essa é uma excelente maneira de depurar um script. Grazie mille A única coisa que eu adicionei foi um cheque de OS X assim: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy
1
Um pouco desconectado, mas pegamos esse trecho, o limpamos, adicionamos mais recursos, melhoramos a formatação da saída e o tornamos mais compatível com POSIX (funciona no Linux e no OSX). Ele foi publicado como parte do Privex ShellCore no Github: github.com/Privex/shell-core
Someguy123
22

Uma alternativa equivalente a "set -e" é

set -o errexit

Isso torna o significado da bandeira um pouco mais claro do que apenas "-e".

Adição aleatória: para desativar temporariamente o sinalizador e retornar ao padrão (de execução contínua, independentemente dos códigos de saída), basta usar

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

Isso impede o tratamento adequado de erros mencionado em outras respostas, mas é rápido e eficaz (assim como o bash).

Ben Scholbrock
fonte
1
usando $(foo)em uma linha simples, em vez de apenas fooé geralmente a coisa errada. Por que promovê-lo, dando-o como um exemplo?
Charles Duffy
20

Inspirado pelas idéias apresentadas aqui, desenvolvi uma maneira legível e conveniente de lidar com erros nos scripts do bash no meu projeto do bash clichê .

Simplesmente fornecendo a biblioteca, você obtém o seguinte da caixa (isto é, interromperá a execução de qualquer erro, como se estivesse usando set -egraças a um trapon ERRe alguns bash-fu ):

tratamento de erros do bash-oo-framework

Existem alguns recursos extras que ajudam a lidar com erros, como try and catch ou a palavra-chave throw , que permitem interromper a execução em um ponto para ver o backtrace. Além disso, se o terminal o suportar, ele cospe emojis de linha de força, colore partes da saída para melhor legibilidade e enfatize o método que causou a exceção no contexto da linha de código.

A desvantagem é - não é portátil - o código funciona no bash, provavelmente> = 4 (mas eu imagino que poderia ser portado com algum esforço para o bash 3).

O código é separado em vários arquivos para melhor manuseio, mas eu fui inspirado pela idéia de backtrace da resposta acima de Luca Borrione .

Para ler mais ou dar uma olhada na fonte, consulte GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw

niieani
fonte
Isso está dentro do projeto Bash Object Oriented Framework . ... Felizmente, ele possui apenas 7.4k LOC (de acordo com o GLOC ). POO - dor orientada a objetos?
ingyhere 2/12/19
@ingyhere é altamente modular (e excluir-friendly), para que você só pode usar a parte exceções se é isso que você veio para;)
niieani
11

Prefiro algo realmente fácil de ligar. Então, eu uso algo que parece um pouco complicado, mas é fácil de usar. Normalmente, copio e colo o código abaixo nos meus scripts. Uma explicação segue o código.

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

Eu costumo fazer uma chamada para a função de limpeza ao lado da função error_exit, mas isso varia de script para script, então eu a deixei de fora. As armadilhas captam os sinais de terminação comuns e garantem que tudo seja limpo. O apelido é o que faz a mágica real. Eu gosto de verificar tudo quanto a falhas. Então, em geral, eu chamo de programas em um "se!" declaração de tipo. Subtraindo 1 do número da linha, o alias me dirá onde ocorreu a falha. Também é muito simples de ligar e praticamente à prova de idiotas. Abaixo está um exemplo (basta substituir / bin / false pelo que você quiser chamar).

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi
Michael Nooner
fonte
2
Você pode expandir a declaração "Temos que permitir explicitamente aliases" ? Eu ficaria preocupado que algum comportamento inesperado possa resultar. Existe uma maneira de conseguir a mesma coisa com um impacto menor?
BLONG
Eu não preciso $LINENO - 1. Mostrar corretamente sem ele.
Kyb #
Mais curto exemplo de uso em festa e zshfalse || die "hello death"
KYB
6

Outra consideração é o código de saída a ser retornado. Apenas " 1" é bastante padrão, embora exista um punhado de códigos de saída reservados que o bash usa , e a mesma página argumenta que os códigos definidos pelo usuário devem estar no intervalo 64-113 para estar em conformidade com os padrões C / C ++.

Você também pode considerar a abordagem de vetor de bits mountusada para seus códigos de saída:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

OR- Ao juntar os códigos, seu script sinaliza vários erros simultâneos.

yukondude
fonte
4

Eu uso o seguinte código de interceptação, ele também permite que os erros sejam rastreados através de pipes e comandos 'time'

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR
Olivier Delrieu
fonte
5
A functionpalavra-chave é incompatível com POSIX gratuitamente. Considere fazer sua declaração apenas error() {, sem functionantes.
Charles Duffy
5
${$?}deveria ser apenas $?, ou ${?}se você insistir em usar aparelhos desnecessários; o interior $está errado.
22813 Charles Duffy
3
@CharlesDuffy até agora, POSIX é gratuitamente GNU / Linux-incompatíveis (ainda, eu levo o seu ponto)
Croad Langshan
3

Eu usei

die() {
        echo $1
        kill $$
}

antes; Eu acho que porque 'exit' estava falhando para mim por algum motivo. Os padrões acima parecem uma boa idéia, no entanto.

pjz
fonte
Melhor enviar mensagem de erro para STDERR, não?
Ankostis
3

Isso me serviu bem por um tempo agora. Ele imprime mensagens de erro ou aviso em vermelho, uma linha por parâmetro e permite um código de saída opcional.

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}
l0b0
fonte
3

Não tenho certeza se isso será útil para você, mas modifiquei algumas das funções sugeridas aqui para incluir a verificação do erro (código de saída do comando anterior) dentro dele. Em cada "verificação", também passo como parâmetro a "mensagem" sobre qual é o erro para fins de registro.

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

Agora, para chamá-lo no mesmo script (ou em outro, se eu usar export -f error_exit), basta escrever o nome da função e passar uma mensagem como parâmetro, assim:

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

Usando isso, eu consegui criar um arquivo bash realmente robusto para algum processo automatizado e ele será interrompido em caso de erros e me notificará ( log.shfará isso)

Nelson Rodriguez
fonte
2
Considere usar a sintaxe POSIX para definir funções - nenhuma functionpalavra-chave, apenas error_exit() {.
Charles Duffy
2
existe uma razão pela qual você não faz apenas cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares
@ Pierre-OlivierVares Não há razão específica para não usar ||. Este foi apenas um trecho de um código existente e eu apenas adicionei as linhas de "tratamento de erros" após cada linha relativa. Alguns são muito longo e que era apenas mais limpo para tê-lo em uma linha separada (imediata)
Nelson Rodriguez
Parece uma solução limpa, no entanto, a verificação de shell reclama: github.com/koalaman/shellcheck/wiki/SC2181
mhulse 18/03
1

Esse truque é útil para a falta de comandos ou funções. O nome da função ausente (ou executável) será passado em $ _

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR
Orwellophile
fonte
Não $_estaria disponível na função o mesmo que $?? Não sei se há algum motivo para usar um na função, mas não o outro.
ingyhere 2/12/19
1

Essa função tem me servido muito bem recentemente:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

Você o chama anexando 0 ou o último valor de retorno ao nome do comando a ser executado, para poder encadear comandos sem precisar verificar valores de erro. Com isso, esse bloco de instruções:

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

Torna-se isso:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

Se algum dos comandos falhar, o código de erro é simplesmente passado para o final do bloco. Acho útil quando você não deseja que comandos subsequentes sejam executados se um anterior falhar, mas também não deseja que o script saia imediatamente (por exemplo, dentro de um loop).

xarxziux
fonte
0

Usar armadilha nem sempre é uma opção. Por exemplo, se você estiver escrevendo algum tipo de função reutilizável que precise de tratamento de erros e possa ser chamada de qualquer script (depois de buscar o arquivo com funções auxiliares), essa função não poderá assumir nada sobre o horário de saída do script externo, o que dificulta o uso de armadilhas. Outra desvantagem do uso de traps é a composição ruim, pois você corre o risco de substituir o traps anterior que pode ser configurado anteriormente na cadeia de chamadas.

Há um pequeno truque que pode ser usado para lidar adequadamente com erros sem traps. Como você já deve saber de outras respostas, set -enão funciona dentro de comandos se você usar o ||operador após eles, mesmo que você os execute em um subshell; por exemplo, isso não funcionaria:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Mas o ||operador é necessário para impedir o retorno da função externa antes da limpeza. O truque é executar o comando interno em segundo plano e esperar imediatamente por ele. O waitbuiltin retornará o código de saída do comando interno, e agora você está usando ||depois wait, não a função interna, portanto set -efunciona corretamente dentro do último:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aqui está a função genérica que se baseia nessa idéia. Ele deve funcionar em todos os shells compatíveis com POSIX se você remover localpalavras-chave, ou seja, substitua todos local x=ypor apenas x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Exemplo de uso:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Executando o exemplo:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

A única coisa que você precisa estar ciente ao usar esse método é que todas as modificações das variáveis ​​do Shell feitas a partir do comando para o qual você passa runnão serão propagadas para a função de chamada, porque o comando é executado em um subshell.

sam.kozin
fonte