Fond d'Ă©cran Shell par manapi Le
débogage des scripts bash, c'est comme chercher une aiguille dans une botte de foin, en particulier lorsque de nouveaux ajouts apparaissent dans une base de code existante sans considération en temps opportun des problèmes de structure, de journalisation et de fiabilité. Vous pouvez vous retrouver dans de telles situations à la fois à cause de vos propres erreurs et lors de la gestion de fouillis complexes de scripts.
L'équipe Mail.ru Cloud Solutions atraduit un article avec des directives qui vous aideront à mieux rédiger, déboguer et gérer vos scripts. Croyez-le ou non, rien ne vaut la satisfaction d'écrire un code bash propre et prêt à l'emploi qui fonctionne à chaque fois.
Dans cet article, l'auteur partage ce qu'il a appris ces dernières années, ainsi que certaines erreurs courantes qui l'ont pris au dépourvu. Ceci est important car chaque développeur de logiciel, à un moment donné de sa carrière, travaille avec des scripts pour automatiser les tâches de travail de routine.
Manipulateurs de pièges
La plupart des scripts bash que j'ai rencontrés n'ont jamais utilisé de mécanisme de nettoyage efficace lorsque quelque chose d'inattendu se produit pendant l'exécution du script.
Des choses inattendues peuvent survenir de l'extérieur, par exemple, recevoir un signal du noyau. La gestion de tels cas est extrêmement importante pour garantir que les scripts sont suffisamment robustes pour s'exécuter sur les systèmes de production. J'utilise souvent des gestionnaires d'exit pour répondre à des scénarios comme celui-ci:
function handle_exit() {
// Add cleanup code here
// for eg. rm -f "/tmp/${lock_file}.lock"
// exit with an appropriate status code
}
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap
Est une commande shell intégrée qui vous aide à enregistrer une fonction de nettoyage à appeler en cas de signaux. Cependant, une attention particulière doit être portée aux gestionnaires tels que ceux SIGINT
qui interrompent le script.
De plus, dans la plupart des cas, vous ne devriez qu'attraper
EXIT
, mais l'idée est que vous pouvez réellement personnaliser le comportement du script pour chaque signal individuel.
Définir les fonctions intégrées - Sortie rapide en cas d'erreur
Il est très important de réagir aux erreurs dès qu'elles se produisent et d'arrêter rapidement l'exécution. Rien de pire que de continuer avec une commande comme celle-ci:
rm -rf ${directory_name}/*
Veuillez noter que la variable
directory_name
n'est pas définie.
Pour gérer de tels scénarios, il est important d'utiliser des 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 se termine dès qu'il rencontre un code de sortie différent de zéro, des variables non définies, des commandes redirigées non valides, etc.
#!/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: les fonctions intégrées telles que
set -o errexit
quitteront le script dès qu'un code retour "brut" (autre que zéro) apparaît. Par conséquent, il est préférable d'introduire une gestion des erreurs personnalisée comme:
#!/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
Un script comme celui-ci vous oblige à faire plus attention au comportement de toutes les commandes du script et à anticiper la possibilité d'une erreur avant qu'elle ne soit prise par surprise.
ShellCheck pour détecter les erreurs pendant le développement
Cela vaut la peine d'intégrer quelque chose comme ShellCheck dans vos pipelines de développement et de test pour valider votre code bash par rapport aux 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 recommande fortement de l'utiliser.
Utilisation de vos codes de sortie
Les codes de retour POSIX ne sont pas simplement zéro ou un, mais zéro ou non nul. Utilisez ces fonctionnalités pour renvoyer des codes d'erreur personnalisés (entre 201 et 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: soyez particulièrement prudent avec les noms de variables que vous définissez pour éviter de surcharger accidentellement les variables d'environnement.
Fonctions de l'enregistreur
Une journalisation agréable et structurée est importante pour comprendre facilement les résultats de l'exécution de votre script. Comme avec d'autres langages de programmation de haut niveau, j'utilise toujours mes propres fonctions de journalisation dans mes scripts bash comme
__msg_info
, __msg_error
et ainsi de suite.
Cela permet de fournir 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 une sorte de mécanisme dans mes scripts
__init
où ces variables de journalisation et d'autres variables système sont initialisées ou définies sur 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
Lorsqu'un tel script est exécuté, il est garanti que les paramètres à l'échelle du système sont définis sur leurs valeurs par défaut, si nécessaire, ou au moins initialisés avec quelque chose d'approprié, si nécessaire.
Je base généralement mon choix de ce qu'il faut initialiser et de ce qui ne doit pas être un compromis entre l'interface utilisateur et les détails de la configuration dans lesquels l'utilisateur peut / doit se plonger.
Architecture pour la réutilisation et le nettoyage de l'état du système
Code modulaire / réutilisable
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
Je garde un référentiel séparé que je peux utiliser pour initialiser un nouveau projet / script bash que je souhaite développer. Tout ce qui peut être réutilisé peut être stocké dans le référentiel et récupéré dans d'autres projets qui souhaitent utiliser cette fonctionnalité. 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, telles que les rapports de Slack, conservées séparément common/*
et se connectent dynamiquement à d'autres scénarios, tels que daily_database_operation.sh
.
Laisser un système propre derrière
Si vous chargez certaines ressources pendant l'exécution du script, il est recommandé de stocker toutes ces données dans un répertoire partagé avec un nom aléatoire, par exemple
/tmp/AlRhYbD97/*
. Vous pouvez utiliser des générateurs de texte aléatoire pour choisir un nom de répertoire:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Une fois le travail terminé, le nettoyage de ces répertoires peut être assuré dans les gestionnaires de crochet décrits ci-dessus. Si vous ne prenez pas soin de supprimer les répertoires temporaires, ils s'accumulent et provoquent à un moment donné des problèmes inattendus sur l'hôte, comme un disque plein.
Utilisation des fichiers de verrouillage
Souvent, vous devez vous assurer qu'une seule instance d'un script s'exécute sur un hôte à un moment donné. Cela peut être fait en utilisant des fichiers de verrouillage.
Je crée généralement des fichiers de verrouillage
/tmp/project_name/*.lock
et vérifie leur présence au début du script. Cela permet de terminer correctement le script et d'éviter les changements d'état du système inattendus par un autre script exécuté en parallèle. Les fichiers de verrouillage ne sont pas nécessaires si vous avez besoin du même script pour s'exécuter en parallèle sur un hôte donné.
Mesurer et améliorer
Nous devons souvent travailler avec des scripts qui s'exécutent sur une longue période, comme les opérations quotidiennes de base 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 diviser le script en petits scripts séparés 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 voir le runtime avec:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Cela m'aide à identifier les problèmes / zones lentes dans les scripts qui nécessitent une optimisation.
Bonne chance!
Quoi d'autre Ă lire: