Gestione degli errori in Bash


240

Qual è il tuo metodo preferito per gestire gli errori in Bash? Il miglior esempio di gestione degli errori che ho trovato sul web è stato scritto da William Shotts, Jr su http://www.linuxcommand.org .

Suggerisce di usare la seguente funzione per la gestione degli errori in 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."

Hai una migliore routine di gestione degli errori che usi negli script Bash?


1
Vedi questa risposta dettagliata: solleva l'errore in uno script Bash .
codeforester,

1
Vedi l'implementazione della registrazione e della gestione degli errori qui: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Risposte:


154

Usa una trappola!

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

... quindi, ogni volta che crei un file temporaneo:

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

e $temp_fooverrà eliminato all'uscita e verrà stampato il numero di riga corrente. ( set -eallo stesso modo ti darà un comportamento all'uscita dall'errore, anche se viene fornito con gravi avvertenze e indebolisce la prevedibilità e la portabilità del codice).

Puoi consentire alla trap di chiamarti error(nel qual caso utilizza il codice di uscita predefinito 1 e nessun messaggio) oppure chiamarla tu stesso e fornire valori espliciti; per esempio:

error ${LINENO} "the foobar failed" 2

uscirà con lo stato 2 e darà un messaggio esplicito.


4
@draemon la capitalizzazione variabile è intenzionale. Tutto in maiuscolo è convenzionale solo per i builtin della shell e le variabili di ambiente - l'uso di lettere minuscole per tutto il resto previene i conflitti dello spazio dei nomi. Vedi anche stackoverflow.com/questions/673055/…
Charles Duffy,

1
prima di romperlo di nuovo, prova le tue modifiche. Le convenzioni sono una buona cosa, ma sono secondarie al codice funzionante.
Draemon,

3
@Draemon, in realtà non sono d'accordo. Il codice ovviamente rotto viene notato e corretto. Cattive pratiche ma il codice per lo più funzionante vive per sempre (e viene propagato).
Charles Duffy,

1
ma non te ne sei accorto. Il codice non funzionante viene notato perché il problema è il funzionamento del codice.
Draemon,

5
non è esattamente gratuito ( stackoverflow.com/a/10927223/26334 ) e se il codice è già incompatibile con la rimozione di POSIX la parola chiave funzione non lo rende più in grado di funzionare con POSIX sh, ma il mio punto principale era che tu ' ve (IMO) ha svalutato la risposta indebolendo la raccomandazione di usare set -e. Stackoverflow non riguarda il "tuo" codice, si tratta di avere le risposte migliori.
Draemon,

123

Questa è un'ottima soluzione. Volevo solo aggiungere

set -e

come meccanismo di errore rudimentale. Arresterà immediatamente lo script se un semplice comando non riesce. Penso che questo avrebbe dovuto essere il comportamento predefinito: poiché tali errori significano quasi sempre qualcosa di inaspettato, non è davvero "sano" continuare a eseguire i seguenti comandi.


29
set -enon è privo di gotchas : vedi mywiki.wooledge.org/BashFAQ/105 per diversi.
Charles Duffy,

3
@CharlesDuffy, alcuni dei gotchas possono essere superati conset -o pipefail
piani di cottura

7
@CharlesDuffy Grazie per aver indicato i gotchas; nel complesso, comunque, penso ancora che set -eabbia un elevato rapporto costi-benefici.
Bruno De Fraine,

3
@BrunoDeFraine Io uso set -eme stesso, ma un certo numero di altri clienti abituali in irc.freenode.org # bash sconsigliano (in termini abbastanza forti) di non farlo. Come minimo, i gotcha in questione dovrebbero essere ben compresi.
Charles Duffy,

3
set -e -o pipefail -u # e sai cosa stai facendo
Sam Watkins

78

Leggere tutte le risposte in questa pagina mi ha ispirato molto.

Quindi, ecco il mio suggerimento:

contenuto del file: 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



Esempio di utilizzo:
contenuto del file: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0


In esecuzione:

bash trap-test.sh

Produzione:

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!


Come puoi vedere dallo screenshot qui sotto, l'output è colorato e il messaggio di errore arriva nella lingua utilizzata.

inserisci qui la descrizione dell'immagine


3
questa cosa è fantastica .. dovresti creare un progetto github per questo, così le persone possono facilmente apportare miglioramenti e contribuire indietro. L'ho combinato con log4bash e insieme crea un potente ambiente per la creazione di buoni script bash.
Dominik Dorn,

1
CRONACA - test ${#g_libs[@]} == 0non è conforme a POSIX (supporti di prova POSIX =per i confronti di stringhe o -eqper i confronti numerici, ma non ==, per non parlare della mancanza di array in POSIX), e se stai non cercando di essere conforme a POSIX, perché nel mondo stai usando testpiuttosto che un contesto matematico? (( ${#g_libs[@]} == 0 ))è, dopo tutto, più facile da leggere.
Charles Duffy,

2
@Luca - è davvero grandioso! La tua foto mi ha ispirato a creare la mia implementazione di questo, che lo fa ancora qualche passo in più. L'ho pubblicato nella mia risposta di seguito .
niieani,

3
Bravissimo !! Questo è un modo eccellente per eseguire il debug di uno script. Grazie mille L'unica cosa che ho aggiunto è stata una verifica per OS X in questo modo: 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
Un po 'un'auto-spina spudorata, ma abbiamo preso questo frammento, ripulito, aggiunto più funzionalità, migliorato la formattazione dell'output e reso più compatibile con POSIX (funziona sia su Linux che su OSX). È pubblicato come parte di Privex ShellCore su Github: github.com/Privex/shell-core
Someguy123

22

Un'alternativa equivalente a "set -e" è

set -o errexit

Rende il significato della bandiera un po 'più chiaro di un semplice "-e".

Aggiunta casuale: per disabilitare temporaneamente il flag e tornare al valore predefinito (di esecuzione continua indipendentemente dai codici di uscita), basta usare

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

Ciò preclude la corretta gestione degli errori menzionata in altre risposte, ma è rapida ed efficace (proprio come bash).


1
usare $(foo)su una linea nuda piuttosto che semplicemente fooè la cosa sbagliata. Perché promuoverlo dandogli un esempio?
Charles Duffy,

20

Ispirato dalle idee qui presentate, ho sviluppato un modo leggibile e conveniente per gestire gli errori negli script bash nel mio progetto bash boilerplate .

Semplicemente acquistando la libreria, ottieni quanto segue immediatamente (cioè interromperà l'esecuzione su qualsiasi errore, come se usasse set -egrazie a un trapon ERRe un po 'di bash-fu ):

gestione degli errori bash-oo-framework

Ci sono alcune funzionalità extra che aiutano a gestire gli errori, come try and catch , o la parola chiave throw , che ti permettono di interrompere l'esecuzione in un punto per vedere la backtrace. Inoltre, se il terminale lo supporta, emette emoji powerline, colora parti dell'output per un'ottima leggibilità e sottolinea il metodo che ha causato l'eccezione nel contesto della riga di codice.

Il rovescio della medaglia è - non è portatile - il codice funziona solo in bash, probabilmente> = 4 (ma immagino che potrebbe essere portato con qualche sforzo per bash 3).

Il codice è separato in più file per una migliore gestione, ma sono stato ispirato dall'idea di backtrace dalla risposta sopra di Luca Borrione .

Per saperne di più o dai un'occhiata alla fonte, vedi GitHub:

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


Questo è all'interno del progetto Bash Object Oriented Framework . ... Fortunatamente ha solo 7.4k LOC (secondo GLOC ). OOP - Dolore orientato agli oggetti?
ingyhere

@ingyhere è altamente modulare (e facile da eliminare), quindi puoi usare la parte delle eccezioni solo se è quello per cui sei venuto;)
niieani

11

Preferisco qualcosa di veramente facile da chiamare. Quindi uso qualcosa che sembra un po 'complicato, ma è facile da usare. Di solito, copia e incolla il codice qui sotto nei miei script. Una spiegazione segue il codice.

#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 ))`):"'

Di solito metto una chiamata alla funzione cleanup a fianco della funzione error_exit, ma questa varia da script a script, quindi l'ho lasciata fuori. Le trappole catturano i segnali di terminazione comuni e assicurano che tutto venga ripulito. L'alias è ciò che fa la vera magia. Mi piace controllare tutto per errore. Quindi, in generale, chiamo i programmi in un "if!" istruzione di tipo. Sottraendo 1 dal numero di riga l'alias mi dirà dove si è verificato l'errore. È anche semplicissimo da chiamare e praticamente una prova idiota. Di seguito è riportato un esempio (basta sostituire / bin / false con quello che si intende chiamare).

#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

2
Puoi espandere l'affermazione "Dobbiamo consentire esplicitamente gli alias" ? Sarei preoccupato che potrebbe derivarne un comportamento inaspettato. C'è un modo per ottenere la stessa cosa con un impatto minore?
blong,

Non ho bisogno $LINENO - 1. Mostra correttamente senza di essa.
Kyb,

Esempio di utilizzo più breve in bash e zshfalse || die "hello death"
kyb

6

Un'altra considerazione è il codice di uscita da restituire. Solo " 1" è piuttosto standard, anche se ci sono una manciata di codici di uscita riservati che bash stesso usa , e quella stessa pagina sostiene che i codici definiti dall'utente dovrebbero essere nell'intervallo 64-113 per essere conformi agli standard C / C ++.

Potresti anche considerare l'approccio bit vector che mountusa per i suoi codici di uscita:

 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-unire insieme i codici consente allo script di segnalare più errori simultanei.


4

Uso il seguente codice trap, inoltre consente di rintracciare gli errori tramite pipe e comandi "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

5
La functionparola chiave è gratuitamente incompatibile con POSIX. Valuta di fare la tua dichiarazione solo error() {, senza functionprima.
Charles Duffy,

5
${$?}dovrebbe essere $?, o ${?}se insisti nell'utilizzare parentesi graffe inutili; l'interno $è sbagliato.
Charles Duffy,

3
@CharlesDuffy ormai, POSIX è incompatibilmente GNU / Linux incompatibile (comunque, prendo il tuo punto)
Croad Langshan,

3

Ho usato

die() {
        echo $1
        kill $$
}

prima; penso perché "exit" non funzionava per me per qualche motivo. Le impostazioni di cui sopra sembrano comunque una buona idea.


Meglio inviare un messaggio di errore a STDERR, no?
ankostis,

3

Questo mi ha servito bene per un po 'di tempo. Stampa messaggi di errore o di avviso in rosso, una riga per parametro e consente un codice di uscita opzionale.

# 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}
}

3

Non sono sicuro se questo ti sarà utile, ma ho modificato alcune delle funzioni suggerite qui per includere il controllo dell'errore (codice di uscita dal comando precedente) al suo interno. Su ogni "controllo" passo anche come parametro il "messaggio" di quale sia l'errore ai fini della registrazione.

#!/bin/bash

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

Ora per chiamarlo all'interno dello stesso script (o in un altro se lo uso export -f error_exit) scrivo semplicemente il nome della funzione e invio un messaggio come parametro, in questo modo:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

Usando questo sono stato in grado di creare un file bash davvero robusto per alcuni processi automatizzati e si fermerà in caso di errori e mi avviserà ( log.shlo farà)


2
Prendi in considerazione l'utilizzo della sintassi POSIX per la definizione delle funzioni: nessuna functionparola chiave, solo error_exit() {.
Charles Duffy,

2
c'è un motivo per cui non lo fai cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares,

@ Pierre-OlivierVares Nessun motivo particolare per non utilizzare ||. Questo era solo un estratto di un codice esistente e ho appena aggiunto le righe di "gestione degli errori" dopo ogni riga relativa. Alcuni sono molto lunghi ed è stato più pulito averlo su una linea separata (immediata)
Nelson Rodriguez,

Sembra una soluzione pulita, tuttavia, il controllo della shell si lamenta: github.com/koalaman/shellcheck/wiki/SC2181
mhulse il

1

Questo trucco è utile per comandi o funzioni mancanti. Il nome della funzione mancante (o eseguibile) verrà passato in $ _

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

Non $_sarebbe disponibile nella funzione come $?? Non sono sicuro che ci sia alcun motivo per usarne uno nella funzione ma non l'altro.
ingyhere

1

Questa funzione mi ha servito piuttosto bene di recente:

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}
}

Lo chiami aggiungendo 0 o l'ultimo valore di ritorno al nome del comando da eseguire, quindi puoi concatenare i comandi senza dover controllare i valori di errore. Con questo, questo blocco di istruzioni:

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

Diventa questo:

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 uno qualsiasi dei comandi fallisce, il codice di errore viene semplicemente passato alla fine del blocco. Lo trovo utile quando non vuoi che i successivi comandi vengano eseguiti se uno precedente non è riuscito, ma non vuoi che lo script esca immediatamente (ad esempio, all'interno di un ciclo).


0

L'uso di trap non è sempre un'opzione. Ad esempio, se stai scrivendo una sorta di funzione riutilizzabile che richiede la gestione degli errori e che può essere richiamata da qualsiasi script (dopo aver acquisito il file con le funzioni di supporto), tale funzione non può assumere nulla sul tempo di uscita dello script esterno, che rende molto difficile l'uso delle trappole. Un altro svantaggio dell'uso delle trap è la scarsa componibilità, poiché si rischia di sovrascrivere le trap precedenti che potrebbero essere impostate in precedenza nella catena del chiamante.

C'è un piccolo trucco che può essere usato per gestire correttamente gli errori senza trappole. Come forse già saprai da altre risposte, set -enon funziona all'interno dei comandi se usi l' ||operatore dopo di loro, anche se li esegui in una subshell; ad esempio, questo non funzionerebbe:

#!/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

Ma l' ||operatore è necessario per impedire il ritorno dalla funzione esterna prima della pulizia. Il trucco è eseguire il comando interno in background, quindi attendere immediatamente. Il waitbuiltin restituirà il codice di uscita del comando interno, e ora stai usando ||dopo wait, non la funzione interna, quindi set -efunziona correttamente all'interno di quest'ultimo:

#!/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

Ecco la funzione generica che si basa su questa idea. Dovrebbe funzionare in tutte le shell compatibili con POSIX se rimuovete le localparole chiave, ovvero sostituite tutte local x=ycon solo 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
}

Esempio di utilizzo:

#!/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 "$@"

Eseguendo l'esempio:

$ ./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

L'unica cosa di cui è necessario essere consapevoli quando si utilizza questo metodo è che tutte le modifiche delle variabili Shell effettuate dal comando a cui si passa runnon si propagheranno alla funzione chiamante, poiché il comando viene eseguito in una subshell.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.