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


  • FrançaisFrançais


  • Personne ne veut écrire du mauvais code, mais inévitablement des bogues seront créés. La plupart des langages modernes tels que 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 un code bien écrit est ensuite décomposé en fonctions. Traquer les problèmes lorsque quelque chose ne va pas dans un script shell peut être difficile lorsque ces scripts deviennent suffisamment volumineux. Une trace de pile qui fait remonter le code de l’erreur au début peut vous montrer où votre code a échoué et vous donner une meilleure compréhension de la raison pour que vous puissiez le réparer correctement.

    Pour implémenter la trace de la pile, j’utilise le piège 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 aborder la seconde, piège ‘ERRO_LINENO=$LINENO’ ERR, première. Cette ligne garantit que le script intercepte toutes les commandes qui sortent 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 (ensemble -E) garantit que l’interruption d’erreur est héritée dans tout le script. Sans cela, chaque fois que vous tombez dans un si ou jusqu’à bloquer, 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 au _échec fonction, que je vais définir dans un instant. Mais pourquoi à la sortie et non à l’erreur si vous essayez de déboguer le script ? Dans les scripts bash, les échecs de commande sont souvent utilisés dans la logique de contrôle ou peuvent être purement et simplement ignorés car sans importance par conception. Par exemple, disons qu’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 ne voulez enregistrer une erreur et une trace de pile que lorsque le script a quitté involontairement à cause d’une erreur.

    Pour forcer votre script à se fermer en cas d’erreur inattendue, utilisez la commande ensemble -e option:

    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, quels sont les exemples où vous voudriez probablement que votre script se termine et mette en évidence un échec ? Les exemples courants incluent les suivants :

    1. Un système distant inaccessible
    2. Échec de l’authentification sur un système distant
    3. Erreurs de syntaxe dans les fichiers de configuration ou de script en cours de sourçage
    4. Créations 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’éventuelles erreurs 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 tourner et où. Le pire 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 est toujours la priorité.

    Examinez l’exemple de code de suivi de la pile (disponible pour télécharger 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 le _échec la fonction capture 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 ensemble -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 envoyée à la console.

    Les valeurs shell intégrées suivantes sont utilisées pour créer la trace de la 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 drapeaux et des arguments.

    Si le script se termine avec une erreur de manière inattendue, il boucle sur les variables ci-dessus et affiche chacune dans l’ordre 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. Première, 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 stacktrace.sh fichier exécutable.

    Exécution stacktrace.sh affichera 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 plus de crédit, essayez de décommenter la ligne dans testfile1.sh et l’exécution stacktrace.sh encore:

    # adsfadfaf

    Ensuite, re-commentez la ligne et commentez à la place la ligne suivante dans testfile1.sh qui a provoqué une trace de pile et exécuté stacktrace.sh une dernière fois:

    curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

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

    Source

    La Rédaction

    L'équipe rédactionnnelle du site

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code