Gestion des erreurs dans Bash


240

Quelle est votre méthode préférée pour gérer les erreurs dans Bash? Le meilleur exemple de gestion des erreurs que j'ai trouvé sur le Web a été écrit par William Shotts, Jr à http://www.linuxcommand.org .

Il suggère d'utiliser la fonction suivante pour la gestion des erreurs dans 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."

Avez-vous une meilleure routine de gestion des erreurs que vous utilisez dans les scripts Bash?


1
Voir cette réponse détaillée: déclencher une erreur dans un script Bash .
codeforester

1
Voir l'implémentation de la journalisation et de la gestion des erreurs ici: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester le

Réponses:


154

Utilisez un piège!

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

... puis, chaque fois que vous créez un fichier temporaire:

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

et $temp_foosera supprimé à la sortie, et le numéro de ligne actuel sera imprimé. ( set -evous donnera également un comportement de sortie sur erreur, bien qu'il s'accompagne de sérieuses mises en garde et affaiblit la prévisibilité et la portabilité du code).

Vous pouvez laisser le piège vous appeler error(auquel cas il utilise le code de sortie par défaut 1 et aucun message) ou l'appeler vous-même et fournir des valeurs explicites; par exemple:

error ${LINENO} "the foobar failed" 2

quittera avec le statut 2 et donnera un message explicite.


4
@draemon la capitalisation variable est intentionnelle. Les majuscules ne sont conventionnelles que pour les commandes internes du shell et les variables d'environnement - l'utilisation de minuscules pour tout le reste évite les conflits d'espace de noms. Voir aussi stackoverflow.com/questions/673055/…
Charles Duffy

1
avant de le casser à nouveau, testez votre changement. Les conventions sont une bonne chose, mais elles sont secondaires au code qui fonctionne.
Draemon

3
@Draemon, je ne suis pas d'accord. Le code manifestement cassé est remarqué et corrigé. Les mauvaises pratiques, mais le code qui fonctionne surtout, vivent pour toujours (et se propagent).
Charles Duffy

1
mais vous ne l'avez pas remarqué. Le code cassé est remarqué car le code fonctionnel est la principale préoccupation.
Draemon

5
ce n'est pas exactement gratuit ( stackoverflow.com/a/10927223/26334 ) et si le code est déjà incompatible avec POSIX, la suppression du mot-clé function ne le rend plus capable de fonctionner sous POSIX sh, mais mon point principal était que vous '' ve (IMO) a dévalué la réponse en affaiblissant la recommandation d'utiliser set -e. Stackoverflow ne concerne pas «votre» code, il s'agit d'avoir les meilleures réponses.
Draemon

123

Voilà une bonne solution. Je voulais juste ajouter

set -e

comme mécanisme d'erreur rudimentaire. Il arrêtera immédiatement votre script si une simple commande échoue. Je pense que cela aurait dû être le comportement par défaut: comme de telles erreurs signifient presque toujours quelque chose d'inattendu, il n'est pas vraiment «sensé» de continuer à exécuter les commandes suivantes.


29
set -en'est pas sans accrochages: Voir mywiki.wooledge.org/BashFAQ/105 pour plusieurs.
Charles Duffy

3
@CharlesDuffy, certains des pièges peuvent être surmontés avecset -o pipefail
plaques

7
@CharlesDuffy Merci d'avoir pointé le doigt sur les pièges; dans l'ensemble cependant, je pense toujours set -eavoir un rapport avantages-coûts élevé.
Bruno De Fraine

3
@BrunoDeFraine Je m'utilise moi- set -emême, mais un certain nombre d'autres habitués de irc.freenode.org # bash le déconseillent (en termes assez forts). Au minimum, les pièges en question doivent être bien compris.
Charles Duffy

3
définissez -e -o pipefail -u # et sachez ce que vous faites
Sam Watkins

78

La lecture de toutes les réponses sur cette page m'a beaucoup inspiré.

Voici donc mon indice:

contenu du fichier: 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



Exemple d'utilisation:
contenu du fichier: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

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

exit 0


Fonctionnement:

bash trap-test.sh

Production:

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!


Comme vous pouvez le voir sur la capture d'écran ci-dessous, la sortie est colorée et le message d'erreur vient dans la langue utilisée.

entrez la description de l'image ici


3
cette chose est géniale .. vous devez créer un projet github pour cela, afin que les gens puissent facilement apporter des améliorations et contribuer en retour. Je l'ai combiné avec log4bash et ensemble, il crée un env puissant pour créer de bons scripts bash.
Dominik Dorn

1
FYI - test ${#g_libs[@]} == 0n'est pas compatible POSIX (le test POSIX prend en charge =les comparaisons de chaînes ou -eqles comparaisons numériques, mais pas ==, sans parler du manque de tableaux dans POSIX), et si vous n'essayez pas d'être conforme POSIX, pourquoi dans le monde utilisez-vous testplutôt que d'un contexte mathématique? (( ${#g_libs[@]} == 0 ))est, après tout, plus facile à lire.
Charles Duffy

2
@Luca - c'est vraiment génial! Votre photo m'a inspiré pour créer ma propre mise en œuvre de cela, ce qui va même plus loin. Je l'ai posté dans ma réponse ci-dessous .
niieani

3
Bravissimo !! C'est un excellent moyen de déboguer un script. Grazie mille La seule chose que j'ai ajoutée était un chèque pour OS X comme ceci: 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 peu d'une auto-prise sans vergogne, mais nous avons pris cet extrait, l'avons nettoyé, ajouté plus de fonctionnalités, amélioré le formatage de sortie et l'avons rendu plus compatible POSIX (fonctionne sur Linux et OSX). Il est publié dans le cadre de Privex ShellCore sur Github: github.com/Privex/shell-core
Someguy123

22

Une alternative équivalente à "set -e" est

set -o errexit

Cela rend la signification du drapeau un peu plus claire que le simple "-e".

Ajout aléatoire: pour désactiver temporairement l'indicateur et revenir à la valeur par défaut (de continuer l'exécution indépendamment des codes de sortie), utilisez simplement

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

Cela empêche la gestion correcte des erreurs mentionnée dans d'autres réponses, mais est rapide et efficace (tout comme bash).


1
utiliser $(foo)sur une ligne nue plutôt que juste fooest généralement la mauvaise chose. Pourquoi le promouvoir en le donnant comme exemple?
Charles Duffy

20

Inspiré par les idées présentées ici, j'ai développé un moyen lisible et pratique de gérer les erreurs dans les scripts bash dans mon projet bash passe-partout .

En achetant simplement la bibliothèque, vous obtenez ce qui suit de la boîte (c'est-à-dire qu'il arrêtera l'exécution sur toute erreur, comme si vous l'utilisiez set -egrâce à un trapon ERRet à certains bash-fu ):

gestion des erreurs de bash-oo-framework

Il existe des fonctionnalités supplémentaires qui aident à gérer les erreurs, telles que try and catch ou le mot clé throw , qui vous permettent d'interrompre l'exécution à un moment pour voir la trace. De plus, si le terminal le prend en charge, il crache des emojis Powerline, colore des parties de la sortie pour une grande lisibilité et souligne la méthode qui a provoqué l'exception dans le contexte de la ligne de code.

L'inconvénient est - ce n'est pas portable - le code fonctionne en bash, probablement> = 4 seulement (mais j'imagine qu'il pourrait être porté avec un certain effort pour bash 3).

Le code est séparé en plusieurs fichiers pour une meilleure manipulation, mais j'ai été inspiré par l'idée de retour en arrière de la réponse ci-dessus de Luca Borrione .

Pour en savoir plus ou consulter la source, consultez GitHub:

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


C'est à l'intérieur du projet Bash Object Oriented Framework . ... Heureusement, il n'a que 7,4k LOC (selon GLOC ). POO - Douleur orientée objet?
ingyhere

@ingyhere est très modulaire (et facile à supprimer), vous ne pouvez donc utiliser la partie exceptions que si c'est pour cela que vous êtes venu;)
niieani

11

Je préfère quelque chose de très simple à appeler. J'utilise donc quelque chose qui a l'air un peu compliqué, mais qui est facile à utiliser. Je copie et collez généralement le code ci-dessous dans mes scripts. Une explication suit le code.

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

Je mets généralement un appel à la fonction de nettoyage à côté de la fonction error_exit, mais cela varie d'un script à l'autre, donc je l'ai laissé de côté. Les pièges captent les signaux de terminaison communs et s'assurent que tout est nettoyé. L'alias est ce qui fait la vraie magie. J'aime tout vérifier l'échec. Donc, en général, j'appelle les programmes dans un "si!" déclaration de type. En soustrayant 1 du numéro de ligne, l'alias me dira où la panne s'est produite. Il est également très simple à appeler et à peu près à l'épreuve des idiots. Voici un exemple (remplacez simplement / bin / false par ce que vous allez appeler).

#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
Pouvez-vous développer la déclaration "Nous devons autoriser explicitement les alias" ? Je craindrais qu'un comportement inattendu puisse en résulter. Existe-t-il un moyen de réaliser la même chose avec un impact moindre?
blong

Je n'en ai pas besoin $LINENO - 1. Montrez correctement sans elle.
kyb

Exemple d'utilisation plus courte dans bash et zshfalse || die "hello death"
kyb

6

Une autre considération est le code de sortie à retourner. Juste " 1" est assez standard, bien qu'il existe une poignée de codes de sortie réservés que bash utilise lui-même , et cette même page soutient que les codes définis par l'utilisateur devraient être compris entre 64 et 113 pour se conformer aux normes C / C ++.

Vous pouvez également considérer l'approche de vecteur de bits qui mountutilise pour ses codes de sortie:

 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-la combinaison des codes permet à votre script de signaler plusieurs erreurs simultanées.


4

J'utilise le code d'interruption suivant, il permet également de tracer les erreurs via des tuyaux et des commandes «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
Le functionmot-clé est incompatible avec POSIX gratuitement. Pensez à faire votre déclaration juste error() {, sans functionavant.
Charles Duffy

5
${$?}devrait être $?, ou ${?}si vous insistez pour utiliser des accolades inutiles; l'intérieur $est faux.
Charles Duffy

3
@CharlesDuffy maintenant, POSIX est incompatible avec GNU / Linux gratuitement (toujours, je
Croad Langshan

3

J'ai utilisé

die() {
        echo $1
        kill $$
}

avant; je pense parce que la «sortie» échouait pour moi pour une raison quelconque. Les valeurs par défaut ci-dessus semblent cependant être une bonne idée.


Mieux vaut envoyer un message d'erreur à STDERR, non?
ankostis

3

Cela me sert bien depuis un moment maintenant. Il imprime des messages d'erreur ou d'avertissement en rouge, une ligne par paramètre, et autorise un code de sortie facultatif.

# 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

Je ne sais pas si cela vous sera utile, mais j'ai modifié certaines des fonctions suggérées ici afin d'y inclure la vérification de l'erreur (code de sortie de la commande précédente). A chaque "vérification", je passe également en paramètre le "message" de l'erreur à des fins de journalisation.

#!/bin/bash

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

Maintenant, pour l'appeler dans le même script (ou dans un autre si j'utilise export -f error_exit), j'écris simplement le nom de la fonction et je passe un message en paramètre, comme ceci:

#!/bin/bash

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

rm *
error_exit "Unable to delete all files"

En utilisant cela, j'ai pu créer un fichier bash vraiment robuste pour un processus automatisé et il s'arrêtera en cas d'erreur et m'informera (le log.shfera)


2
Pensez à utiliser la syntaxe POSIX pour définir les fonctions - pas de functionmot-clé, juste error_exit() {.
Charles Duffy

2
y a-t-il une raison pour laquelle vous ne faites pas simplement cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares

@ Pierre-OlivierVares Aucune raison particulière de ne pas utiliser ||. Ce n'était qu'un extrait d'un code existant et j'ai juste ajouté les lignes de "gestion des erreurs" après chaque ligne concernant. Certains sont très longs et c'était juste plus propre de l'avoir sur une ligne séparée (immédiate)
Nelson Rodriguez

Cela ressemble à une solution propre, cependant, la vérification de la coque se plaint: github.com/koalaman/shellcheck/wiki/SC2181
mhulse

1

Cette astuce est utile pour les commandes ou fonctions manquantes. Le nom de la fonction manquante (ou exécutable) sera passé dans $ _

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

Ne serait pas $_disponible dans la fonction de la même manière que $?? Je ne suis pas sûr qu'il y ait une raison d'utiliser l'un dans la fonction mais pas l'autre.
ingyhere

1

Cette fonction m'a plutôt bien servi récemment:

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

Vous l'appelez en ajoutant 0 ou la dernière valeur de retour au nom de la commande à exécuter, vous pouvez donc chaîner des commandes sans avoir à vérifier les valeurs d'erreur. Avec cela, ce bloc de déclaration:

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

Devient ceci:

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

Si l'une des commandes échoue, le code d'erreur est simplement transmis à la fin du bloc. Je trouve cela utile lorsque vous ne voulez pas que les commandes suivantes s'exécutent si une précédente a échoué, mais vous ne voulez pas non plus que le script se termine immédiatement (par exemple, dans une boucle).


0

L'utilisation de trap n'est pas toujours une option. Par exemple, si vous écrivez une sorte de fonction réutilisable qui nécessite une gestion des erreurs et qui peut être appelée à partir de n'importe quel script (après avoir obtenu le fichier avec des fonctions d'assistance), cette fonction ne peut rien supposer de l'heure de sortie du script externe, ce qui rend l'utilisation des pièges très difficile. Un autre inconvénient de l'utilisation des interruptions est une mauvaise composabilité, car vous risquez d'écraser l'interruption précédente qui pourrait être définie plus tôt dans la chaîne d'appel.

Il y a une petite astuce qui peut être utilisée pour faire une gestion correcte des erreurs sans pièges. Comme vous le savez peut-être déjà à partir d'autres réponses, set -ene fonctionne pas à l'intérieur des commandes si vous utilisez l' ||opérateur après elles, même si vous les exécutez dans un sous-shell; par exemple, cela ne fonctionnerait pas:

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

Mais l' ||opérateur est nécessaire pour empêcher le retour de la fonction externe avant le nettoyage. L'astuce consiste à exécuter la commande interne en arrière-plan, puis à l'attendre immédiatement. La fonction waitintégrée renverra le code de sortie de la commande interne, et maintenant vous utilisez ||après wait, pas la fonction interne, donc set -efonctionne correctement à l'intérieur de cette dernière:

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

Voici la fonction générique qui s'appuie sur cette idée. Cela devrait fonctionner dans tous les shells compatibles POSIX si vous supprimez des localmots clés, c'est-à-dire remplacez tout local x=ypar juste 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
}

Exemple d'utilisation:

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

Exécuter l'exemple:

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

La seule chose dont vous devez être conscient lorsque vous utilisez cette méthode est que toutes les modifications des variables Shell effectuées à partir de la commande à laquelle vous passez runne se propageront pas à la fonction appelante, car la commande s'exécute dans un sous-shell.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.