Recherche de site Web

Corrigez les bugs dans les scripts Bash en imprimant une trace de pile


L'impression automatique d'une trace de pile sur les erreurs non gérées dans vos scripts peut faciliter la recherche et la correction des bogues dans votre code.

Personne ne veut écrire du mauvais code, mais des bugs seront inévitablement créés. La plupart des langages modernes comme Java, JavaScript, Python, etc., impriment automatiquement une trace de pile lorsqu'ils rencontrent une exception non gérée, mais pas les scripts shell. Il serait beaucoup plus facile de trouver et de corriger les bogues dans les scripts shell si vous pouviez imprimer une trace de pile, et avec un peu de travail, vous le pouvez.

Les scripts Shell peuvent s'étendre sur plusieurs fichiers et le code bien écrit est ensuite décomposé en fonctions. Traquer les problèmes en cas de problème dans un script shell peut être difficile lorsque ces scripts deviennent suffisamment volumineux. Une trace de pile qui parcourt le code depuis l'erreur jusqu'au début peut vous montrer où votre code a échoué et vous permettre de mieux comprendre pourquoi afin que vous puissiez le corriger correctement.

Pour implémenter la trace de pile, j'utilise le trap de la manière suivante au début de mon script :

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

Cet exemple accomplit plusieurs choses, mais je vais d'abord aborder le deuxième, trap 'ERRO_LINENO=$LINENO' ERR. Cette ligne garantit que le script intercepte toutes les commandes qui se terminent avec un code de sortie différent de zéro (c'est-à-dire une erreur) et enregistre le numéro de ligne de la commande dans le fichier où l'erreur a été signalée. Ceci n'est pas capturé à la sortie.

La première ligne ci-dessus (set -E) garantit que le piège d'erreur est hérité tout au long du script. Sans cela, chaque fois que vous passez dans un bloc if ou jusqu'à, par exemple, vous perdriez la trace du numéro de ligne correct.

Le deuxième piège capture le signal de sortie du script et l'envoie à la fonction _failure, que je définirai dans un instant. Mais pourquoi en sortie et pas d'erreur si vous essayez de déboguer le script ? Dans les scripts bash, les échecs de commandes sont souvent utilisés dans la logique de contrôle ou peuvent être carrément ignorés car sans importance de par leur conception. Par exemple, disons au début de votre script, vous cherchez à voir si un programme particulier est déjà installé avant de demander à l'utilisateur s'il souhaite que vous l'installiez pour lui :

if [[ ! -z $(command -v some_command) ]]
then
   # CAPTURE LOCATION OF some_command
   SOME_COMMAND_EXEC=$(which some_command)
else
   # echo $? would give us a non-zero value here; i.e. an error code
   # IGNORE ERR: ASK USER IF THEY WANT TO INSTALL some_command
fi

Si vous deviez arrêter le traitement à chaque erreur et que some_command n'est pas installé, cela mettrait fin prématurément au script, ce qui n'est évidemment pas ce que vous voulez faire ici, donc en général, vous souhaitez uniquement enregistrer une erreur et empiler la trace lorsque le script s'est arrêté involontairement à cause d'une erreur.

Pour forcer la fermeture de votre script chaque fois qu'une erreur inattendue se produit, utilisez l'option set -e :

set -e
# SCRIPT WILL EXIT IF ANY COMMAND RETURNS A NON-ZERO CODE
# WHILE set -e IS IN FORCE
set +e
# COMMANDS WITH ERRORS WILL NOT CAUSE THE SCRIPT TO EXIT HERE

La question suivante est la suivante : quels sont les exemples dans lesquels vous souhaiteriez probablement que votre script se termine et mette en évidence un échec ? Les exemples courants sont les suivants :

  1. Un système distant inaccessible
  2. L'authentification sur un système distant échoue
  3. Erreurs de syntaxe dans les fichiers de configuration ou de script provenant de la source
  4. Constructions d’images Docker
  5. Erreurs du compilateur

Passer au peigne fin de nombreuses pages de journaux après la fin d'un script à la recherche d'erreurs possibles qui peuvent être difficiles à repérer peut être extrêmement frustrant. C'est encore plus frustrant lorsque vous découvrez que quelque chose ne va pas bien après l'exécution du script et que vous devez maintenant parcourir plusieurs ensembles de journaux pour trouver ce qui a pu mal se passer et où. Le pire, c'est lorsque l'erreur existe depuis un certain temps et que vous ne la découvrez qu'au pire moment possible. Dans tous les cas, identifier le problème le plus rapidement possible et le résoudre reste toujours la priorité.

Regardez l'exemple de code de trace de pile (disponible en téléchargement ici) :

# Sample code for generating a stack trace on catastrophic failure

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

_failure() {
  ERR_CODE=$? # capture last command exit code
  set +xv # turns off debug logging, just in case
  if [[  $- =~ e && ${ERR_CODE} != 0 ]]
  then
      # only log stack trace if requested (set -e)
      # and last command failed
      echo
      echo "========= CATASTROPHIC COMMAND FAIL ========="
      echo
      echo "SCRIPT EXITED ON ERROR CODE: ${ERR_CODE}"
      echo
      LEN=${#BASH_LINENO[@]}
      for (( INDEX=0; INDEX<$LEN-1; INDEX++ ))
      do
          echo '---'
          echo "FILE: $(basename ${BASH_SOURCE[${INDEX}+1]})"
          echo "  FUNCTION: ${FUNCNAME[${INDEX}+1]}"
          if [[ ${INDEX} > 0 ]]
          then
           # commands in stack trace
              echo "  COMMAND: ${FUNCNAME[${INDEX}]}"
              echo "  LINE: ${BASH_LINENO[${INDEX}]}"
          else
              # command that failed
              echo "  COMMAND: ${BASH_COMMAND}"
              echo "  LINE: ${ERRO_LINENO}"
          fi
      done
      echo
      echo "======= END CATASTROPHIC COMMAND FAIL ======="
      echo
  fi
}

# set working directory to this directory for duration of this test
cd "$(dirname ${0})"

echo 'Beginning stacktrace test'

set -e
source ./testfile1.sh
source ./testfile2.sh
set +e

_file1_function1

Dans le stacktrace.sh ci-dessus, la première chose que fait la fonction _failure est de capturer le code de sortie de la dernière commande à l'aide de la valeur shell intégrée $?. Il vérifie ensuite si la sortie était inattendue en vérifiant la sortie de $-, une valeur de shell intégrée qui contient les paramètres actuels du shell bash, pour voir si set -e est en vigueur. Si le script s'est terminé sur une erreur et que l'erreur était inattendue, la trace de la pile est affichée sur la console.

Les valeurs de shell intégrées suivantes sont utilisées pour créer la trace de pile :

  1. BASH_SOURCE : tableau de noms de fichiers où chaque commande a été rappelée au script principal.
  2. FUNCNAME : tableau de numéros de ligne correspondant à chaque fichier dans BASH_SOURCE.
  3. BASH_LINENO : tableau de numéros de ligne par fichier correspondant à BASH_SOURCE.
  4. BASH_COMMAND : dernière commande exécutée avec des indicateurs et des arguments.

Si le script se termine avec une erreur de manière inattendue, il boucle sur les variables ci-dessus et génère chacune d'elles afin qu'une trace de pile puisse être créée. Le numéro de ligne de la commande ayant échoué n'est pas conservé dans les tableaux ci-dessus, mais c'est pourquoi vous avez capturé le numéro de ligne chaque fois qu'une commande a échoué avec la première instruction trap ci-dessus.

Mettre tous ensemble

Créez les deux fichiers suivants pour prendre en charge le test, afin que vous puissiez voir comment les informations sont collectées dans plusieurs fichiers. Tout d'abord, testfile1.sh :

_file1_function1() {
   echo
   echo "executing in _file1_function1"
   echo

   _file2_function1
}

# adsfadfaf

_file1_function2() {
   echo
   echo "executing in _file1_function2"
   echo
  
   set -e
   curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

   # function never called
   _file2_does_not_exist
}

Et ensuite, testfile2.sh :

_file2_function1() {
   echo
   echo "executing in _file2_function1"
   echo

   curl this_curl_will_simply_fail

   _file1_function2
}

REMARQUE : Si vous créez ces fichiers vous-même, assurez-vous de rendre le fichier stacktrace.sh exécutable.

L'exécution de stacktrace.sh produira ce qui suit :

~/shell-stack-trace-example$./stracktrace.sh
Beginning stacktrace test

executing in _file1_function1

executing in _file2_function1
curl: (6) Could not resolve host: this_curl_will_simply_fail

executing in _file1_function2
curl: (6) Could not resolve host: this_curl_will_fail_and_CAUSE_A_STACK_TRACE

========= CATASTROPHIC COMMAND FAIL =========

SCRIPT EXITED ON ERROR CODE: 6

---
FILE: testfile1.sh
  FUNCTION: _file1_function2
  COMMAND: curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE
  LINE: 15
---
FILE: testfile2.sh
  FUNCTION: _file2_function1
  COMMAND: _file1_function2
  LINE: 7
---
FILE: testfile1.sh
  FUNCTION: _file1_function1
  COMMAND: _file2_function1
  LINE: 5
---
FILE: stracktrace.sh
  FUNCTION: main
  COMMAND: _file1_function1
  LINE: 53

======= END CATASTROPHIC COMMAND FAIL =======

Pour un crédit supplémentaire, essayez de décommenter la ligne dans testfile1.sh et d'exécuter à nouveau stacktrace.sh :

# adsfadfaf

Re-commentez ensuite la ligne, puis commentez la ligne suivante dans testfile1.sh qui a provoqué une trace de pile et exécutez stacktrace.sh une dernière fois :

curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

Cet exercice devrait vous donner une idée du résultat et du moment où il se produit si vous avez des fautes de frappe dans vos scripts.

Articles connexes: