Relever une erreur dans un script Bash

104

Je souhaite signaler une erreur dans un script Bash avec le message "Les cas de test ont échoué !!!". Comment faire cela dans Bash?

Par exemple:

if [ condition ]; then
    raise error "Test cases failed !!!"
fi
Naveen Kumar
la source
1
Que voulez-vous qu'il se passe sur cette erreur? Comment s'appelle votre script? S'agit-il d'un seul script ou de plusieurs scripts? À quoi ressemblera votre script?
Etan Reisner
juste un script. Je l'ai appelé en utilisant un terminal ubuntu comme ./script/test.sh
Naveen Kumar
unix.stackexchange.com
Seth McClaine
Pas d'amour pour echo you screwed up at ... | mail -s BUG $bugtrackeremailaddress?
infixé

Réponses:

121

Cela dépend de l'emplacement de stockage du message d'erreur.

Vous pouvez faire ce qui suit:

echo "Error!" > logfile.log
exit 125

Ou ce qui suit:

echo "Error!" 1>&2
exit 64

Lorsque vous déclenchez une exception, vous arrêtez l'exécution du programme.

Vous pouvez également utiliser quelque chose comme exit xxxxxxest le code d'erreur que vous voudrez peut-être retourner au système d'exploitation (de 0 à 255). Voici 125et ne 64sont que des codes aléatoires avec lesquels vous pouvez sortir. Lorsque vous devez indiquer au système d'exploitation que le programme s'est arrêté anormalement (par exemple, une erreur s'est produite), vous devez transmettre un code de sortie différent de zéro àexit .

Comme @chepner l'a souligné , vous pouvez le faire exit 1, ce qui signifie une erreur non spécifiée .

ForceBru
la source
12
ou vous pouvez l'envoyer à stderr, où les erreurs sont censées se produire.
comment l'envoyer à stderr?
Naveen Kumar
2
@ user3078630, je viens de modifier ma réponse. 1>&2fera l'affaire
ForceBru
Si c'est une erreur, vous devez sortir d'un état de sortie non nul aussi bien. exitutilise par lui-même le statut de sortie de la dernière commande terminée, qui peut être 0.
chepner
3
À moins que vous n'ayez une signification spécifique à l'esprit, vous devez simplement utiliser exit 1, qui par convention signifie une erreur non spécifiée.
chepner
37

Gestion des erreurs de base

Si votre exécuteur de cas de test renvoie un code différent de zéro pour les tests ayant échoué, vous pouvez simplement écrire:

test_handler test_case_x; test_result=$?
if ((test_result != 0)); then
  printf '%s\n' "Test case x failed" >&2  # write error message to stderr
  exit 1                                  # or exit $test_result
fi

Ou encore plus court:

if ! test_handler test_case_x; then
  printf '%s\n' "Test case x failed" >&2
  exit 1
fi

Ou le plus court:

test_handler test_case_x || { printf '%s\n' "Test case x failed" >&2; exit 1; }

Pour quitter avec le code de sortie de test_handler:

test_handler test_case_x || { ec=$?; printf '%s\n' "Test case x failed" >&2; exit $ec; }

Gestion avancée des erreurs

Si vous souhaitez adopter une approche plus complète, vous pouvez avoir un gestionnaire d'erreurs:

exit_if_error() {
  local exit_code=$1
  shift
  [[ $exit_code ]] &&               # do nothing if no error code passed
    ((exit_code != 0)) && {         # do nothing if error code is 0
      printf 'ERROR: %s\n' "$@" >&2 # we can use better logging here
      exit "$exit_code"             # we could also check to make sure
                                    # error code is numeric when passed
    }
}

puis invoquez-le après avoir exécuté votre cas de test:

run_test_case test_case_x
exit_if_error $? "Test case x failed"

ou

run_test_case test_case_x || exit_if_error $? "Test case x failed"

Les avantages d'avoir un gestionnaire d'erreurs comme exit_if_errorsont:

  • nous pouvons standardiser toute la logique de gestion des erreurs telle que la journalisation , l'impression d'une trace de pile , la notification, le nettoyage, etc., en un seul endroit
  • en obligeant le gestionnaire d'erreurs à obtenir le code d'erreur comme argument, nous pouvons épargner à l'appelant l'encombrement des ifblocs qui testent les codes de sortie pour les erreurs
  • si nous avons un gestionnaire de signal (utilisant trap ), nous pouvons appeler le gestionnaire d'erreur à partir de là

Gestion des erreurs et bibliothèque de journalisation

Voici une implémentation complète de la gestion des erreurs et de la journalisation:

https://github.com/codeforester/base/blob/master/lib/stdlib.sh


Articles Similaires

codeforester
la source
9

Il existe plusieurs façons d'aborder ce problème. En supposant que l'une de vos exigences est d'exécuter un script / une fonction shell contenant quelques commandes shell et de vérifier si le script s'est exécuté avec succès et de générer des erreurs en cas d'échec.

Les commandes shell reposent généralement sur des codes de sortie renvoyés pour indiquer au shell si l'opération a réussi ou échoué en raison d'événements inattendus.

Donc, ce que vous voulez faire relève de ces deux catégories

  • sortie en cas d'erreur
  • quitter et nettoyer en cas d'erreur

Selon celui que vous souhaitez faire, il existe des options de shell disponibles à utiliser. Pour le premier cas, le shell fournit une option avec set -eet pour le second, vous pouvez faire un trapsurEXIT

Dois-je utiliser exitdans mon script / ma fonction?

L'utilisation exitaméliore généralement la lisibilité Dans certaines routines, une fois que vous connaissez la réponse, vous souhaitez quitter immédiatement la routine d'appel. Si la routine est définie de manière à ne pas nécessiter de nettoyage supplémentaire une fois qu'elle détecte une erreur, ne pas quitter immédiatement signifie que vous devez écrire plus de code.

Donc, dans les cas où vous devez effectuer des actions de nettoyage sur le script pour rendre la fin du script propre, il est préférable de ne pas utiliser exit.

Dois-je utiliser set -epour une erreur à la sortie?

Non!

set -eétait une tentative pour ajouter une "détection automatique d'erreur" au shell. Son objectif était de provoquer l'abandon du shell à chaque fois qu'une erreur se produisait, mais cela comporte de nombreux pièges potentiels, par exemple,

  • Les commandes qui font partie d'un test if sont immunisées. Dans l'exemple, si vous vous attendez à ce qu'il se casse lors de la testvérification du répertoire non existant, ce ne serait pas le cas, il passe à la condition else

    set -e
    f() { test -d nosuchdir && echo no dir; }
    f
    echo survived
  • Les commandes dans un pipeline autre que le dernier sont immunisées. Dans l'exemple ci-dessous, parce que le code de sortie de la commande la plus récemment exécutée (le plus à droite) est considéré ( cat) et qu'il a réussi. Cela pourrait être évité en définissant l' set -o pipefailoption, mais c'est toujours une mise en garde.

    set -e
    somecommand that fails | cat -
    echo survived 

Utilisation recommandée - trapen sortie

Le verdict est que si vous voulez pouvoir gérer une erreur au lieu de quitter aveuglément, au lieu d'utiliser set -e, utilisez un trapsur le ERRpseudo-signal.

Le ERRpiège n'est pas d'exécuter du code lorsque le shell lui-même se termine avec un code d'erreur différent de zéro, mais lorsqu'une commande exécutée par ce shell qui ne fait pas partie d'une condition (comme si cmd, ou cmd ||) se termine avec un statut de sortie différent de zéro .

La pratique générale est de définir un gestionnaire d'interruptions pour fournir des informations de débogage supplémentaires sur quelle ligne et ce qui cause la sortie. Souvenez-vous que le code de sortie de la dernière commande à l'origine du ERRsignal serait toujours disponible à ce stade.

cleanup() {
    exitcode=$?
    printf 'error condition hit\n' 1>&2
    printf 'exit code returned: %s\n' "$exitcode"
    printf 'the command executing at the time of the error was: %s\n' "$BASH_COMMAND"
    printf 'command present on line: %d' "${BASH_LINENO[0]}"
    # Some more clean up code can be added here before exiting
    exit $exitcode
}

et nous utilisons simplement ce gestionnaire comme ci-dessous en plus du script qui échoue

trap cleanup ERR

En rassemblant cela sur un simple script qui contenait falseà la ligne 15, les informations que vous obtiendriez comme

error condition hit
exit code returned: 1
the command executing at the time of the error was: false
command present on line: 15

Le trapfournit également des options indépendamment de l'erreur pour simplement exécuter le nettoyage à la fin du shell (par exemple, votre script shell se termine), au signal EXIT. Vous pouvez également intercepter plusieurs signaux en même temps. La liste des signaux pris en charge à intercepter se trouve sur le trap.1p - page de manuel Linux

Une autre chose à noter serait de comprendre qu'aucune des méthodes fournies ne fonctionne si vous traitez avec des sous-shells, auquel cas vous devrez peut-être ajouter votre propre gestion des erreurs.

  • Sur un sous-shell avec set -ene fonctionnerait pas. Le falseest limité au sous-shell et n'est jamais propagé au shell parent. Pour faire la gestion des erreurs ici, ajoutez votre propre logique à faire(false) || false

    set -e
    (false)
    echo survived
  • La même chose se produit trapégalement. La logique ci-dessous ne fonctionnerait pas pour les raisons mentionnées ci-dessus.

    trap 'echo error' ERR
    (false)
Inian
la source
5

Voici un piège simple qui affiche le dernier argument de tout ce qui a échoué à STDERR, signale la ligne sur laquelle il a échoué et quitte le script avec le numéro de ligne comme code de sortie. Notez que ce ne sont pas toujours de bonnes idées, mais cela démontre une application créative sur laquelle vous pourriez vous appuyer.

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR

Je l'ai mis dans un script avec une boucle pour le tester. Je vérifie juste un succès sur certains nombres aléatoires; vous pouvez utiliser des tests réels. Si j'ai besoin de caution, j'appelle false (qui déclenche le piège) avec le message que je veux lancer.

Pour une fonctionnalité élaborée, demandez au trap d'appeler une fonction de traitement. Vous pouvez toujours utiliser une instruction case sur votre argument ($ _) si vous avez besoin de faire plus de nettoyage, etc. Assignez à un var pour un peu de sucre syntaxique -

trap 'echo >&2 "$_ at $LINENO"; exit $LINENO;' ERR
throw=false
raise=false

while :
do x=$(( $RANDOM % 10 ))
   case "$x" in
   0) $throw "DIVISION BY ZERO" ;;
   3) $raise "MAGIC NUMBER"     ;;
   *) echo got $x               ;;
   esac
done

Exemple de sortie:

# bash tst
got 2
got 8
DIVISION BY ZERO at 6
# echo $?
6

Évidemment, vous pourriez

runTest1 "Test1 fails" # message not used if it succeeds

Beaucoup de place pour l'amélioration de la conception.

Les inconvénients incluent le fait que ce falsen'est pas joli (donc le sucre), et d'autres choses qui déclenchent le piège peuvent sembler un peu stupides. Pourtant, j'aime cette méthode.

Paul Hodges
la source
4

Vous avez 2 options: rediriger la sortie du script vers un fichier, introduire un fichier journal dans le script et

  1. Redirection de la sortie vers un fichier :

Ici, vous supposez que le script génère toutes les informations nécessaires, y compris les messages d'avertissement et d'erreur. Vous pouvez ensuite rediriger la sortie vers un fichier de votre choix.

./runTests &> output.log

La commande ci-dessus redirige à la fois la sortie standard et la sortie d'erreur vers votre fichier journal.

En utilisant cette approche, vous n'avez pas besoin d'introduire un fichier journal dans le script, et donc la logique est un peu plus simple.

  1. Introduisez un fichier journal dans le script :

Dans votre script, ajoutez un fichier journal soit en le codant en dur:

logFile='./path/to/log/file.log'

ou en le passant par un paramètre:

logFile="${1}"  # This assumes the first parameter to the script is the log file

C'est une bonne idée d'ajouter l'horodatage au moment de l'exécution au fichier journal en haut du script:

date '+%Y%-m%d-%H%M%S' >> "${logFile}"

Vous pouvez ensuite rediriger vos messages d'erreur vers le fichier journal

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
fi

Cela ajoutera l'erreur au fichier journal et poursuivra l'exécution. Si vous souhaitez arrêter l'exécution lorsque des erreurs critiques se produisent, vous pouvez exitle script:

if [ condition ]; then
    echo "Test cases failed!!" >> "${logFile}"; 
    # Clean up if needed
    exit 1;
fi

Notez que cela exit 1indique que le programme arrête l'exécution en raison d'une erreur non spécifiée. Vous pouvez le personnaliser si vous le souhaitez.

En utilisant cette approche, vous pouvez personnaliser vos journaux et avoir un fichier journal différent pour chaque composant de votre script.


Si vous avez un script relativement petit ou si vous voulez exécuter le script de quelqu'un d'autre sans le modifier, la première approche est plus appropriée.

Si vous voulez toujours que le fichier journal soit au même emplacement, c'est la meilleure option du 2. De plus, si vous avez créé un gros script avec plusieurs composants, vous voudrez peut-être enregistrer chaque partie différemment et la deuxième approche est votre seule option.

alamoot
la source
3

Je trouve souvent utile d'écrire une fonction pour gérer les messages d'erreur afin que le code soit globalement plus propre.

# Usage: die [exit_code] [error message]
die() {
  local code=$? now=$(date +%T.%N)
  if [ "$1" -ge 0 ] 2>/dev/null; then  # assume $1 is an error code if numeric
    code="$1"
    shift
  fi
  echo "$0: ERROR at ${now%???}${1:+: $*}" >&2
  exit $code
}

Cela prend le code d'erreur de la commande précédente et l'utilise comme code d'erreur par défaut lors de la sortie du script entier. Il note également l'heure, avec les microsecondes là où elles sont prises en charge (la date GNU %Nest en nanosecondes, que nous tronquons en microsecondes plus tard).

Si la première option est zéro ou un entier positif, elle devient le code de sortie et nous la supprimons de la liste d'options. Nous rapportons ensuite le message à l'erreur standard, avec le nom du script, le mot "ERREUR" et l'heure (nous utilisons l'expansion des paramètres pour tronquer les nanosecondes en microsecondes, ou pour les temps non GNU, pour tronquer par exemple 12:34:56.%Nà 12:34:56). Un deux-points et un espace sont ajoutés après le mot ERREUR, mais uniquement lorsqu'un message d'erreur est fourni. Enfin, nous quittons le script en utilisant le code de sortie précédemment déterminé, déclenchant les interruptions normalement.

Quelques exemples (supposons que le code existe script.sh):

if [ condition ]; then die 123 "condition not met"; fi
# exit code 123, message "script.sh: ERROR at 14:58:01.234564: condition not met"

$command |grep -q condition || die 1 "'$command' lacked 'condition'"
# exit code 1, "script.sh: ERROR at 14:58:55.825626: 'foo' lacked 'condition'"

$command || die
# exit code comes from command's, message "script.sh: ERROR at 14:59:15.575089"
Adam Katz
la source