Meilleures pratiques en matière de scripts Bash : un guide rapide pour des scripts Bash fiables et performants

Meilleures pratiques en matière de scripts Bash : un guide rapide pour des scripts Bash fiables et performants
Papier peint coquillage par manapi

Déboguer des scripts bash, c'est comme chercher une aiguille dans une botte de foin, en particulier lorsque de nouveaux ajouts apparaissent dans la base de code existante sans prendre en compte en temps opportun les problèmes de structure, de journalisation et de fiabilité. Vous pouvez vous retrouver dans de telles situations soit à cause de vos propres erreurs, soit en gérant des piles de scripts complexes.

Équipe Solutions Cloud Mail.ru traduit un article avec des recommandations qui vous aideront à mieux écrire, déboguer et maintenir vos scripts. Croyez-le ou non, rien ne vaut la satisfaction d'écrire du code bash propre et 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 quelques erreurs courantes qui l’ont pris au dépourvu. Ceci est important car chaque développeur de logiciels, à un moment donné de sa carrière, travaille avec des scripts pour automatiser les tâches de travail de routine.

Gestionnaires de pièges

La plupart des scripts bash que j'ai rencontrés n'utilisent jamais de mécanisme de nettoyage efficace lorsque quelque chose d'inattendu se produit lors de l'exécution du script.

Des surprises peuvent survenir de l’extérieur, comme la réception d’un signal du noyau. La gestion de tels cas est extrêmement importante pour garantir que les scripts sont suffisamment fiables pour être exécutés sur les systèmes de production. J'utilise souvent des gestionnaires de sortie 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 intégrée au shell qui vous aide à enregistrer une fonction de nettoyage qui est appelée en cas de signal. Toutefois, des précautions particulières doivent être prises avec les manipulateurs tels que SIGINT, ce qui provoque l'abandon du script.

De plus, dans la plupart des cas, vous ne devriez attraper que EXIT, mais l'idée est que vous pouvez réellement personnaliser le comportement du script pour chaque signal individuel.

Fonctions d'ensemble intégrées - terminaison 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 de pire que de continuer à exécuter une commande comme celle-ci :

rm -rf ${directory_name}/*

Veuillez noter que la variable directory_name non déterminé.

Il est important d'utiliser des fonctions intégrées pour gérer de tels scénarios set, Comme set -o errexit, set -o pipefail ou set -o nounset au début du scénario. Ces fonctions garantissent que votre script se terminera dès qu'il rencontrera un code de sortie non nul, l'utilisation de variables non définies, des commandes non valides transmises via un tube, 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

Note: fonctions intégrées telles que set -o errexit, quittera le script dès qu'il y aura un code retour "brut" (autre que zéro). Il est donc préférable d'introduire une gestion personnalisée des erreurs, 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 à être plus prudent quant au comportement de toutes les commandes du script et à anticiper la possibilité d'une erreur avant qu'elle ne vous surprenne.

ShellCheck pour détecter les erreurs lors du développement

Cela vaut la peine d'intégrer quelque chose comme Vérification de la coque dans vos pipelines de développement et de test pour vérifier 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 dans le code que j'aurais pu manquer lors du développement. Il s'agit d'un outil d'analyse statique pour vos scripts bash et je recommande fortement de l'utiliser.

Utiliser vos propres codes de sortie

Les codes de retour dans POSIX ne sont pas seulement zéro ou un, mais zéro ou une valeur non nulle. Utilisez ces fonctionnalités pour renvoyer des codes d'erreur personnalisés (entre 201 et 254) pour divers cas d'erreur.

Ces informations peuvent ensuite être utilisées par d'autres scripts qui enveloppent les vôtres 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
}

Note: veuillez être particulièrement prudent avec les noms de variables que vous définissez pour éviter de remplacer accidentellement les variables d'environnement.

Fonctions de journalisation

Une journalisation belle et structurée est importante pour comprendre facilement les résultats de votre script. Comme pour d'autres langages de programmation de haut niveau, j'utilise toujours des fonctions de journalisation natives dans mes scripts bash, telles que __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 autres variables système sont initialisées ou définies sur les valeurs par défaut. Ces variables peuvent également être définies à partir des options 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 garantit que les paramètres à l'échelle du système sont définis sur les valeurs par défaut s'ils sont requis, ou au moins initialisés sur quelque chose d'approprié si nécessaire.

Je base généralement le choix de ce qu'il faut initialiser et de ce qu'il ne faut pas faire sur un compromis entre l'interface utilisateur et les détails des configurations que l'utilisateur peut/devrait approfondir.

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 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 un référentiel et récupéré par d'autres projets souhaitant utiliser cette fonctionnalité. Organiser les projets de cette manière réduit considérablement la taille des autres scripts et garantit également que la base de code est petite et facile à tester.

Comme dans l'exemple ci-dessus, toutes les fonctions de journalisation telles que __msg_info, __msg_error et d'autres, tels que les rapports Slack, sont contenus séparément dans common/* et connectez-vous dynamiquement dans d'autres scénarios comme daily_database_operation.sh.

Laissez derrière vous un système propre

Si vous chargez des 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 ex. /tmp/AlRhYbD97/*. Vous pouvez utiliser des générateurs de texte aléatoires pour sélectionner le nom du répertoire :

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Une fois les travaux terminés, le nettoyage de ces répertoires peut être assuré dans les gestionnaires de hook évoqués ci-dessus. Si les répertoires temporaires ne sont pas pris en charge, ils s'accumulent et provoquent à un moment donné des problèmes inattendus sur l'hôte, comme un disque plein.

Utiliser des fichiers de verrouillage

Vous devez souvent vous assurer qu'une seule instance d'un script est exécutée sur un hôte à un moment donné. Cela peut être fait en utilisant des fichiers de verrouillage.

Je crée habituellement des fichiers de verrouillage dans /tmp/project_name/*.lock et vérifiez leur présence au début du script. Cela permet au script de se terminer correctement et d'éviter des modifications inattendues de l'état du système par un autre script exécuté en parallèle. Les fichiers de verrouillage ne sont pas nécessaires si vous avez besoin que le même script soit exécuté en parallèle sur un hôte donné.

Mesurer et améliorer

Nous devons souvent travailler avec des scripts qui s'exécutent sur de longues périodes, comme les opérations quotidiennes de bases de données. De telles opérations impliquent 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 statut et leur temps d'exécution en utilisant :

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Plus tard, je peux voir 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 qui nécessitent une optimisation.

Bonne chance!

Quoi lire d'autre :

  1. Allez et caches GPU.
  2. Un exemple d'application basée sur des événements basée sur des webhooks dans le stockage d'objets S3 de Mail.ru Cloud Solutions.
  3. Notre chaîne de télégrammes sur la transformation numérique.

Source: habr.com

Ajouter un commentaire