
Le dĂ©bogage des scripts bash est comme chercher une aiguille dans une botte de foin, d'autant plus que de nouvelles extensions apparaissent dans une base de code existante sans un examen opportun des questions de structure, de journalisation et de fiabilitĂ©. Dans de telles situations, on peut se retrouver confrontĂ© Ă ses propres erreurs tout en gĂ©rant des enchevĂȘtrements complexes de scripts.
Commande J'ai traduit un article avec des recommandations qui vous aideront Ă mieux Ă©crire, dĂ©boguer et maintenir vos scripts. Que vous le croyiez ou non, rien ne peut Ă©galer la satisfaction d'Ă©crire un code bash propre, prĂȘt Ă l'emploi, qui fonctionne Ă chaque fois.
Dans l'article, l'auteur partage ce qu'il a appris au cours des derniÚres années, ainsi que certaines erreurs courantes qui l'ont surpris. C'est important car chaque développeur de logiciels travaille, à un moment donné de sa carriÚre, avec des scripts pour automatiser des tùches professionnelles répétitives.
Gestionnaires de piĂšges
La plupart des scripts bash que j'ai rencontrés n'ont jamais utilisé de mécanisme de nettoyage efficace lorsqu'il se passe quelque chose d'inattendu pendant l'exécution du script.
Des surprises peuvent survenir de l'extĂ©rieur, comme la rĂ©ception d'un signal du noyau. GĂ©rer de tels cas est extrĂȘmement important pour garantir que les scripts soient suffisamment fiables pour ĂȘtre exĂ©cutĂ©s dans des systĂšmes de production. J'utilise souvent des gestionnaires de sortie pour rĂ©agir Ă de tels scĂ©narios :
function handle_exit() {
// Ajoutez du code de nettoyage ici
// par exemple. rm -f "/tmp/${lock_file}.lock"
// quittez avec un code de statut approprié
}
// trap
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap est une commande intégrée de l'interpréteur de commandes qui vous aide à enregistrer une fonction de nettoyage appelée en cas de signaux. Cependant, il faut faire particuliÚrement attention avec des gestionnaires comme SIGINT, qui provoque l'interruption du script.
De plus, dans la plupart des cas, il convient de ne capturer que EXIT, mais l'idée est que vous pouvez vraiment personnaliser le comportement du script pour chaque signal individuel.
Fonctions intĂ©grĂ©es set - arrĂȘt rapide en cas d'erreur
Il est trĂšs important de rĂ©agir aux erreurs dĂšs qu'elles surviennent et d'arrĂȘter rapidement l'exĂ©cution. Rien n'est pire que de continuer Ă exĂ©cuter une commande comme celle-ci :
rm -rf ${directory_name}
Notez que la variable directory_name n'est pas définie.
Pour traiter de tels scĂ©narios, il est important d'utiliser les fonctions intĂ©grĂ©es set, telles que set -o errexit, set -o pipefail ou set -o nounset au dĂ©but du script. Ces fonctions garantissent que votre script s'arrĂȘte dĂšs qu'il rencontre un code de sortie non nul, l'utilisation de variables non dĂ©finies, de commandes incorrectes passĂ©es par le pipeline, et ainsi de suite :
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
Remarque : fonctions intégrées, telles que set -o errexit, sortiront du script dÚs qu'un code de retour « non géré » (autre que zéro) apparaßtra. Il est donc préférable d'introduire un traitement d'erreurs personnalisé, par exemple :
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
Ăcrire des scripts de cette maniĂšre vous oblige Ă prĂȘter une attention particuliĂšre au comportement de toutes les commandes dans le script et Ă prĂ©voir la possibilitĂ© d'erreurs avant qu'elles ne vous surprennent.
ShellCheck pour détecter les erreurs lors du développement
Il vaut la peine d'intégrer quelque chose comme dans vos pipelines de développement et de test pour vérifier votre code bash selon les meilleures pratiques.
Je l'utilise dans mes environnements de développement locaux pour obtenir des rapports sur la syntaxe, la sémantique et certaines erreurs de code que j'aurais pu manquer lors du développement. C'est un outil d'analyse statique pour vos scripts bash, et je le recommande vivement.
Utilisation de vos codes de sortie
Les codes de retour en POSIX ne se limitent pas Ă zĂ©ro ou un, mais peuvent aussi ĂȘtre zĂ©ro ou une valeur non nulle. Profitez de cette fonctionnalitĂ© pour renvoyer des codes d'erreur personnalisĂ©s (entre 201-254) pour diffĂ©rents cas d'erreur.
Ces informations peuvent ensuite ĂȘtre utilisĂ©es par d'autres scripts qui enveloppent le vĂŽtre, pour comprendre exactement quel type d'erreur s'est produit et rĂ©agir en consĂ©quence :
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
Remarque : veuillez faire particuliÚrement attention aux noms de variables que vous définissez afin d'éviter de redéfinir accidentellement des variables d'environnement.
Fonctions de logging
Une gestion des logs belle et structurée est essentielle pour comprendre facilement les résultats de l'exécution de votre script. Comme dans d'autres langages de programmation de haut niveau, j'utilise toujours dans mes scripts bash des fonctions de journalisation personnalisées, telles que __msg_info, __msg_error et ainsi de suite.
Cela permet d'assurer une structure de journalisation standardisée, en apportant des modifications à un seul endroit :
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
J'essaie gĂ©nĂ©ralement d'avoir dans mes scripts un mĂ©canisme __init, oĂč de telles variables de journalisation et d'autres variables systĂšme sont initialisĂ©es ou dĂ©finies avec des valeurs par dĂ©faut. Ces variables peuvent Ă©galement ĂȘtre dĂ©finies Ă partir des paramĂštres de ligne de commande lors de l'appel du script.
Par exemple, quelque chose comme :
$ ./run-script.sh --debug
Lorsque ce script est exécuté, il garantit que les paramÚtres systÚme sont définis sur des valeurs par défaut, si cela est obligatoire, ou, au minimum, initialisés par quelque chose de pertinent si nécessaire.
Je base gĂ©nĂ©ralement mon choix sur ce qui doit ĂȘtre initialisĂ© ou non, sur un compromis entre l'interface utilisateur et les dĂ©tails de configuration que l'utilisateur peut/doit comprendre.
Architecture pour la réutilisation et l'état propre du systÚme
Code modulaire / réutilisable
âââ framework
â âââ common
â â âââ loggers.sh
â â âââ mail_reports.sh
â â âââ slack_reports.sh
â âââ daily_database_operation.sh
Je garde un dĂ©pĂŽt sĂ©parĂ© qui peut ĂȘtre utilisĂ© pour initialiser un nouveau projet/script bash que je souhaite dĂ©velopper. Tout ce qui peut ĂȘtre rĂ©utilisĂ© peut ĂȘtre conservĂ© dans le dĂ©pĂŽt et rĂ©cupĂ©rĂ© dans d'autres projets qui souhaitent utiliser telles fonctionnalitĂ©s. Cette organisation des projets rĂ©duit considĂ©rablement la taille des autres scripts et garantit Ă©galement que la base de code est petite et facilement testable.
Comme dans l'exemple ci-dessus, toutes les fonctions de journalisation, telles que __msg_info, __msg_error et d'autres, comme des rapports sur Slack, sont contenues séparément dans common/* et sont dynamiquement incluses dans d'autres scénarios, comme daily_database_operation.sh.
Laissez un systĂšme propre
Si vous chargez des ressources pendant l'exécution d'un script, il est recommandé de stocker toutes ces données dans un répertoire commun avec un nom aléatoire, par exemple /tmp/AlRhYbD97/*. Vous pouvez utiliser des générateurs de texte aléatoire pour choisir le nom du répertoire :
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
AprĂšs l'exĂ©cution, le nettoyage de ces rĂ©pertoires peut ĂȘtre assurĂ© par les gestionnaires de piĂšges, comme mentionnĂ© ci-dessus. Si l'on ne prend pas soin de supprimer les rĂ©pertoires temporaires, ils s'accumulent et peuvent, Ă un certain moment, causer des problĂšmes inattendus sur l'hĂŽte, comme un disque plein.
Utilisation des fichiers de verrouillage
Il est souvent nĂ©cessaire d'assurer l'exĂ©cution d'une seule instance d'un script sur l'hĂŽte Ă tout moment. Cela peut ĂȘtre fait en utilisant des fichiers de verrouillage.
Je crĂ©e gĂ©nĂ©ralement des fichiers de verrouillage dans /tmp/project_name/*.lock et vĂ©rifie leur existence au dĂ©but du script. Cela aide Ă bien terminer le script et Ă Ă©viter des modifications inattendues de l'Ă©tat du systĂšme par un autre script s'exĂ©cutant en parallĂšle. Les fichiers de verrouillage ne sont pas nĂ©cessaires si vous devez exĂ©cuter le mĂȘme script en parallĂšle sur cet hĂŽte.
Mesurer et améliorer
Nous avons souvent à travailler avec des scripts qui s'exécutent pendant de longues périodes, comme des opérations quotidiennes sur des bases de données. Ces opérations comprennent généralement une séquence d'étapes : chargement des données, vérification des anomalies, importation des données, envoi de rapports d'état, etc.
Dans de tels cas, j'essaie toujours de décomposer le script en petits scripts distincts et de signaler leur état et leur temps d'exécution avec :
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Plus tard, je peux consulter le temps d'exécution avec :
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Cela m'aide à identifier les zones problématiques/lentes dans les scripts nécessitant une optimisation.
Bonne chance !
Que lire d'autre :
Source : habr.com
