Le débogage est deux fois plus difficile que l'écriture de code en premier lieu. Donc, si vous écrivez du code aussi intelligemment que possible, vous êtes, par définition, pas assez intelligent pour le déboguer.
-- Brian Kernighan
Le shell Bash ne contient pas de débogueur intégré mais seulement des commandes et des constructions. Les erreurs de syntaxe ou de frappe dans les scripts génèrent des messages d'erreur incompréhensibles n'apportant souvent aucune aide pour déboguer un script non fonctionnel.
Exemple 29.1. Un script bogué
#!/bin/bash # ex74.sh # C'est un script bogué. # Où est donc l'erreur ? a=37 if [$a -gt 27 ] then echo $a fi exit 0
Sortie d'un script :
./ex74.sh: [37: command not found
Que se passe-t'il avec ce script ? (petite aide : après le if
Exemple 29.2. Mot clé manquant
#!/bin/bash # missing-keyword.sh : Quel message d'erreur sera généré ? for a in 1 2 3 do echo "$a" # done # Requiert le mot clé 'done' mis en commentaire ligne 7. exit 0
Sortie d'un script :
missing-keyword.sh: line 10: syntax error: unexpected end of file
Notez que le message d'erreur ne fait pas nécessairement référence à la ligne où l'erreur se trouve mais à la ligne où l'interpréteur Bash s'aperçoit de l'erreur.
Les messages d'erreur peuvent ne pas tenir compte des lignes de commentaires d'un script lors de l'affichage du numéro de ligne de l'instruction ayant provoqué une erreur de syntaxe.
Que faire si le script s'exécute mais ne fonctionne pas comme vous vous y attendiez ? C'est une erreur de logique trop commune.
Exemple 29.3. test24, un autre script bogué
#!/bin/bash # Ce script est supposé supprimer tous les fichiers du répertoire courant #+ contenant des espaces dans le nom. # Cela ne fonctionne pas. # Pourquoi ? mauvaisnom=`ls | grep ' '` # Essayez ceci : # echo "$mauvaisnom" rm "$mauvaisnom" exit 0
Essayez de trouver ce qui ne va pas avec l'Exemple 29.3, « test24, un autre script bogué » en supprimant les caractères de commentaires de la ligne echo "$mauvaisnom". Les instructions echo sont utiles pour voir si ce que vous attendiez est bien ce que vous obtenez.
Dans ce cas particulier, rm "$mauvaisnom" ne donnera pas les résultats attendus parce que $mauvaisnom ne devrait pas être entre guillemets. Le placer entre guillemets nous assure que rm n'a qu'un seul argument (il correspondra à un seul nom de fichier). Une correction partielle est de supprimer les guillemets de $mauvaisnom et de réinitialiser $IFS pour contenir seulement un retour à la ligne, IFS=$'\n'. Néanmoins, il existe des façons plus simples de faire cela.
# Bonnes méthodes de suppression des fichiers contenant des espaces dans leur nom. rm *\ * rm *" "* rm *' '* # Merci, S.C.
Résumer les symptômes d'un script bogué,
Il quitte brutalement avec un message d'erreur de syntaxe (« syntax error »)
Il se lance bien mais ne fonctionne pas de la façon attendue (erreur logique, logic error).
Il fonctionne comme vous vous y attendiez mais a des effets indésirables déplaisants (logic bomb).
Il existe des outils pour déboguer des scripts non fonctionnels
Des instructions echo aux points critiques du script pour tracer les variables, ou pour donner un état de ce qui se passe.
Encore mieux, une instruction echo qui affiche seulement lorsque le mode de débogage (debug) est activé.
### debecho (debug-echo) par Stefano Falsetto ### ### Affichera les paramètres seulement si DEBUG est configuré. ### debecho () { if [ ! -z "$DEBUG" ]; then echo "$1" >&2 # ^^^ vers stderr fi } DEBUG=on Whatever=whatnot debecho $Whatever # whatnot DEBUG= Whatever=notwhat debecho $Whatever # (N'affichera rien.)
utiliser le filtre tee pour surveiller les processus ou les données aux points critiques.
initialiser des paramètres optionnelles -n -v -x
sh -n nomscript vérifie les erreurs de syntaxe sans réellement exécuter le script. C'est l'équivalent de l'insertion de set -n ou set -o noexec dans le script. Notez que certains types d'erreurs de syntaxe peuvent passer à côté de cette vérification.
sh -v nomscript affiche chaque commande avant de l'exécuter. C'est l'équivalent de l'insertion de set -v ou set -o verbose dans le script.
Les options -n et -v fonctionnent bien ensemble. sh -nv nomscript permet une vérification verbeuse de la syntaxe.
sh -x nomscript affiche le résultat de chaque commande mais d'une façon abrégée. C'est l'équivalent de l'insertion de set -x ou set -o xtrace dans le script.
Insérer set -u ou set -o nounset dans le script le lance mais donne un message d'erreur « unbound variable » à chaque essai d'utilisation d'une variable non déclarée.
Utiliser une fonction « assert » pour tester une variable ou une condition aux points critiques d'un script (cette idée est empruntée du C).
Exemple 29.4. Tester une condition avec un assert
#!/bin/bash # assert.sh assert () # Si la condition est fausse, { #+ sort du script avec un message d'erreur. E_PARAM_ERR=98 E_ASSERT_FAILED=99 if [ -z "$2" ] # Pas assez de paramètres passés then # à la fonction assert(). return $E_PARAM_ERR # Pas de dommages. fi noligne=$2 if [ ! $1 ] then echo "Mauvaise assertion : \"$1\"" echo "Fichier \"$0\", ligne $noligne" # Donne le nom du fichier et le numéro de ligne exit $E_ASSERT_FAILED # else (sinon) # return (retour) # et continue l'exécution du script. fi } # Insèrer une fonction assert() similaire dans un script #+ que vous devez déboguer. ####################################################################### a=5 b=4 condition="$a -lt $b" # Message d'erreur et sortie du script. # Essayer de configurer la "condition" en autre chose #+ et voir ce qui se passe. assert "$condition" $LINENO # Le reste du script s'exécute si assert n'échoue pas. # Quelques commandes. # Quelques autres commandes... echo "Cette instruction s'exécute seulement si \"assert\" n'échoue pas." # ... # Quelques commandes de plus. exit $?
La commande exit d'un script déclenche un signal 0, terminant le processus, c'est-à-dire le script lui-même. [85] Il est souvent utilisé pour récupérer la main lors de exit en forçant un « affichage » des variables par exemple. Le trap doit être la première commande du script.
Récupérer les signaux
Spécifie une action à la réception d'un signal ; aussi utile pour le débogage.
Un signal est un simple message envoyé au processus, soit par le noyau soit par un autre processus lui disant de réaliser une action spécifiée (habituellement pour finir son exécution). Par exemple, appuyer sur Control-C envoie une interruption utilisateur, un signal INT, au programme en cours d'exécution.
trap '' 2 # Ignore l'interruption 2 (Control-C), sans action définie. trap 'echo "Control-C désactivé."' 2 # Message lorsque Control-C est utilisé.
Exemple 29.5. Récupérer la sortie
#!/bin/bash # Chasse aux variables avec un piège. trap 'echo Liste de Variables --- a = $a b = $b' EXIT # EXIT est le nom du signal généré en sortie d'un script. # # La commande spécifiée par le "trap" ne s'exécute pas #+ tant que le signal approprié n'est pas envoyé. echo "Ceci s'affiche avant le \"trap\" -- " echo "même si le script voit le \"trap\" avant" echo a=39 b=36 exit 0 # Notez que mettre en commentaire la commande 'exit' ne fait aucune différence # car le script sort dans tous les cas après avoir exécuté les commandes.
Exemple 29.6. Nettoyage après un Control-C
#!/bin/bash # logon.sh: Un script rapide mais sale pour vérifier si vous êtes déjà connecté. umask 177 # S'assurer que les fichiers temporaires ne sont pas lisibles #+ par tout le monde. VRAI=1 JOURNAL=/var/log/messages # Notez que $JOURNAL doit être lisible (en tant que root, chmod 644 /var/log/messages). FICHIER_TEMPORAIRE=temp.$$ # Crée un fichier temporaire "unique" en utilisant l'identifiant du processus. # Utiliser 'mktemp' est une alternative. # Par exemple : # FICTMP=`mktemp temp.XXXXXX` MOTCLE=adresse # À la connexion, la ligne "remote IP address xxx.xxx.xxx.xxx" # ajoutée à /var/log/messages. ENLIGNE=22 INTERRUPTION_UTILISATEUR=13 VERIFIE_LIGNES=100 # Nombre de lignes à vérifier dans le journal. trap 'rm -f $FICHIER_TEMPORAIRE; exit $INTERRUPTION_UTILISATEUR' TERM INT # Nettoie le fichier temporaire si le script est interrompu avec Control-C. echo while [ $VRAI ] # Boucle sans fin. do tail -n $VERIFIE_LIGNES $JOURNAL> $FICHIER_TEMPORAIRE # Sauve les 100 dernières lignes du journal dans un fichier temporaire. # Nécessaire car les nouveaux noyaux génèrent beaucoup de messages lors de la # connexion. search=`grep $MOTCLE $FICHIER_TEMPORAIRE` # Vérifie la présence de la phrase "IP address" # indiquant une connexion réussie. if [ ! -z "$search" ] # Guillemets nécessaires à cause des espaces possibles. then echo "En ligne" rm -f $FICHIER_TEMPORAIRE # Suppression du fichier temporaire. exit $ENLIGNE else echo -n "." # l'option -n supprime les retours à la ligne de echo, # de façon à obtenir des lignes de points continus. fi sleep 1 done # Note : Si vous modifiez la variable MOTCLE par "Exit", # ce script peut être utilisé lors de la connexion pour vérifier une déconnexion # inattendue. # Exercice : Modifiez le script, suivant la note ci-dessus, et embellissez-le. exit 0 # Nick Drage suggère une autre méthode. while true do ifconfig ppp0 | grep UP 1> /dev/null && echo "connecté" && exit 0 echo -n "." # Affiche des points (.....) jusqu'au moment de la connexion. sleep 2 done # Problème : Appuyer sur Control-C pour terminer ce processus peut être # insuffisant (des points pourraient toujours être affichés). # Exercice : Corrigez ceci. # Stéphane Chazelas a lui-aussi suggéré une autre méthode. CHECK_INTERVAL=1 while ! tail -n 1 "$JOURNAL" | grep -q "$MOTCLE" do echo -n . sleep $CHECK_INTERVAL done echo "On-line" # Exercice : Discutez les avantages et inconvénients de chacune des méthodes.
L'argument DEBUG pour trap exécute une action spécifique après chaque commande dans un script. Cela permet de tracer les variables, par exemple.
Exemple 29.7. Tracer une variable
#!/bin/bash trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG # Affiche la valeur de $variable après chaque commande. variable=29 echo "Initialisation de \"\$variable\" à $variable." let "variable *= 3" echo "Multiplication de \"\$variable\" par 3." exit $? # La construction "trap 'commande1 ... commande2 ...' DEBUG" est plus #+ appropriée dans le contexte d'un script complexe #+ où placer plusieurs instructions "echo $variable" pourrait être #+ difficile et consommer du temps. # Merci, Stéphane Chazelas, pour cette information. Affichage du script : VARIABLE-TRACE> $variable = "" VARIABLE-TRACE> $variable = "29" Initialisation de "$variable" à 29. VARIABLE-TRACE> $variable = "29" VARIABLE-TRACE> $variable = "87" Multiplication de "$variable" par 3. VARIABLE-TRACE> $variable = "87"
Bien sûr, la commande trap a d'autres utilités en dehors du débogage.
Exemple 29.8. Lancer plusieurs processus (sur une machine SMP)
#!/bin/bash # parent.sh # Exécuter plusieurs processus sur une machine SMP. # Auteur : Tedman Eng # Ceci est le premier de deux scripts, #+ les deux étant présent dans le même répertoire courant. LIMITE=$1 # Nombre total de processus à lancer NBPROC=4 # Nombre de threads simultanés (processus fils ?) IDPROC=1 # ID du premier processus echo "Mon PID est $$" function lance_thread() { if [ $IDPROC -le $LIMITE ] ; then ./child.sh $IDPROC& let "IDPROC++" else echo "Limite atteinte." wait exit fi } while [ "$NBPROC" -gt 0 ]; do lance_thread; let "NBPROC--" done while true do trap "lance_thread" SIGRTMIN done exit 0 # ======== Le deuxième script suit ======== #!/bin/bash # child.sh # Lancer plusieurs processus sur une machine SMP. # Ce script est appelé par parent.sh. # Auteur : Tedman Eng temp=$RANDOM index=$1 shift let "temp %= 5" let "temp += 4" echo "Début $index Temps :$temp" "$@" sleep ${temp} echo "Fin $index" kill -s SIGRTMIN $PPID exit 0 # ==================== NOTES DE L'AUTEUR DU SCRIPT ==================== # # Ce n'est pas complètement sans bogue. # Je l'exécute avec limit = 500 et, après les premières centaines d'itérations, #+ un des threads simultanés a disparu ! # Pas sûr que ce soit dû aux collisions des signaux trap. # Une fois que le signal est reçu, le gestionnaire de signal est exécuté #+ un bref moment mais le prochain signal est configuré. #+ Pendant ce laps de temps, un signal peut être perdu, #+ donc un processus fils peut manquer. # Aucun doute que quelqu'un va découvrir le bogue et nous l'indiquer #+ ... dans le futur. # ===================================================================== # # ----------------------------------------------------------------------# ################################################################# # Ce qui suit est le script original écrit par Vernia Damiano. # Malheureusement, il ne fonctionne pas correctement. ################################################################# # multiple-processes.sh : Lance plusieurs processus sur une machine # multi-processeurs. # Script écrit par Vernia Damiano. # Utilisé avec sa permission. # Doit appeler le script avec au moins un paramètre de type entier #+ (nombre de processus concurrents). # Tous les autres paramètres sont passés aux processus lancés. INDICE=8 # Nombre total de processus à lancer TEMPO=5 # Temps de sommeil maximum par processus E_MAUVAISARGUMENTS=65 # Pas d'arguments passés au script. if [ $# -eq 0 ] # Vérifie qu'au moins un argument a été passé au script. then echo "Usage: `basename $0` nombre_de_processus [paramètres passés aux processus]" exit $E_MAUVAISARGUMENTS fi NBPROCESSUS=$1 # Nombre de processus concurrents shift PARAMETRI=( "$@" ) # Paramètres de chaque processus function avvia() { local temp local index temp=$RANDOM index=$1 shift let "temp %= $TEMPO" let "temp += 1" echo "Lancement de $index Temps:$temp" "$@" sleep ${temp} echo "Fin $index" kill -s SIGRTMIN $$ } function parti() { if [ $INDICE -gt 0 ] ; then avvia $INDICE "${PARAMETRI[@]}" & let "INDICE--" else trap : SIGRTMIN fi } trap parti SIGRTMIN while [ "$NBPROCESSUS" -gt 0 ]; do parti; let "NBPROCESSUS--" done wait trap - SIGRTMIN exit $? : <<COMMENTAIRES_AUTEUR_SCRIPT J'avais besoin de lancer un programme avec des options spécifiées sur un certain nombre de fichiers différents en utilisant une machine SMP. Donc, j'ai pensé conserver un certain nombre de processus en cours et en lancer un à chaque fois qu'un autre avait terminé. L'instruction "wait" n'aide pas car il attend un processus donné ou "tous" les processus exécutés en arrière-plan. Donc, j'ai écrit ce script bash, réalisant ce travail en utilisant l'instruction "trap". --Vernia Damiano COMMENTAIRES_AUTEUR_SCRIPT
trap '' SIGNAL (deux apostrophes adjacentes) désactive SIGNAL pour le reste du script. trap SIGNAL restaure la fonctionnalité de SIGNAL. C'est utile pour protéger une portion critique d'un script d'une interruption indésirable.
trap '' 2 # Le signal 2 est Control-C, maintenant désactivé. command command command trap 2 # Réactive Control-C