Retour sur Bash : vers un code robuste et sécurisé
- Le fossé entre le script local et l’automatisation
- Hygiène du code : Traquer les hallucinations et les effets de bord
- Optimisation pragmatique : Chasser le surcoût inutile
- Configuration et Isolation : Anticiper le plantage
- Débogage avancé : Dompter les codes de retour et les signaux
- Savoir pivoter quand Bash atteint ses limites
- Le Prompt de Validation : Notre Template de Production
Il y a quelques semaines, j’ai partagé une série d’articles constituant une formation sur Linux et l’utilisation de Bash. J’ai également déjà eu l’occasion de vous partager quelques articles sur l’automatisation et le Cloud. Je voulais consolider cette dernière partie en vous partageant des éléments de formation, enrichis pour l’occasion, que j’avais eu l’occasion de dispenser au cours de mes missions de développeur et de Product Owner Cloud.
J’avais terminé ma précédente série sur de bonnes pratiques pour la rédaction de scripts Bash, mais il restait quelques éléments clés à prendre en compte, surtout pour automatiser ses déploiements via des outils de CI/CD.
Notez que dans cette nouvelle série, nous considérons que les notions abordées dans la précédente série sur Linux et Bash sont acquises.
Le fossé entre le script local et l’automatisation
Aujourd’hui, nous rédigeons de moins en moins nos scripts Bash à partir d’une page blanche. Nous interrogeons une IA (Cursor, ChatGPT, Claude, etc.), nous obtenons un script, nous le testons rapidement en local (dans le meilleur des cas), et … c’est parti en production.
Et là, surprise : ça ne marche pas… La CI est rouge. D’où vient ce décalage ? En local, nous sommes dans un environnement interactif, souvent permissif, avec nos propres variables d’environnement et un état persistant. La CI/CD, elle, s’exécute dans un conteneur vierge, éphémère et sans concession.
Pour éviter les catastrophes silencieuses ou les déploiements partiels corrompus, nous devons impérativement vérifier que l’IA a correctement configuré le script.
Pour cela, nous allons voir quelques clés d’un script robuste, facile à maintenir, et capable de s’arrêter proprement à la moindre alerte plutôt que de détruire silencieusement notre environnement de production.
Le garde-fou absolu : set -euo pipefail
C’est la première chose à inspecter tout en haut du script. Par défaut, Bash est d’une permissivité coupable : si une commande échoue, il passe tranquillement à la suite. Cette ligne est donc absolument nécessaire.
set -e(Exit on error) : ce premier paramètre ordonne au script de s’arrêter immédiatement dès qu’une commande renvoie un code d’erreur (différent de 0). Sans cela, le script poursuit son exécution, et passe à l’étape suivante, même s’il lui manque des éléments… Le meilleur moyen de créer un état inconsistant.set -u(Nounset) : il s’agit là de traiter les variables non définies comme des erreurs. C’est le pare-balle contre les hallucinations de variables de l’IA. Un simplerm -rf ${MY_VAR}/*oùMY_VARest vide se transforme enrm -rf /*, engendrant un grand ménage du système (ce qui est rarement souhaitable).set -ubloque le script avant le drame.set -o pipefail: si l’IA enchaîne des commandes avec des pipes (commande1 | commande2), Bash ne regarde par défaut que le code de retour de la dernière commande. Sicommande1implose mais que le tri ou le formatage decommande2réussit, le script renvoie un succès.pipefailgarantit que la moindre défaillance dans la chaîne lève une alerte.
Hygiène du code : Traquer les hallucinations et les effets de bord
Les LLM ont tendance à générer du code Bash fonctionnel mais générique, omettant les règles élémentaires d’isolation. Lors de notre relecture, nous pouvons appliquer ces trois filtres de contrôle :
L’encapsulation des variables
Nous devons vérifier que l’IA utilise ${ma_var} qui est une syntaxe plus rigoureuse que $ma_var. Cela élimine toute ambiguïté lors de la concaténation (ex: ${prefixe}_file.txt) et rend le code lisible sans surcharger l’interprétation de Bash.
Le mot-clé local dans les fonctions
Une variable déclarée à l’intérieur d’une fonction en bash, sans préciser sa portée, deviennent globales et écrasent potentiellement d’autres variables du script. En forçant l’utilisation systématique de local ma_variable="..." au sein des fonctions, nous nous épargnons des effets de bord difficilement prévisibles.
Le nettoyage des arguments anonymes
Dans les scripts, Bash utilise de nombreuses variables « anonymes » : elles n’ont pas de nom explicites, et sont créées automatiquement par Bash.
Par exemple $1, $2 ou $3 représentent des arguments passés au script, ou bien à la fonction, tandis que $?contient le code de retour de la dernière commande utilisée.
Ces arguments positionnels bruts sont illisibles lors d’un débogage dans les logs de CI. Ils doivent être immédiatement mappés dans des variables explicites dès le début du script ou de la fonction :
local version="${1:-default}"
local environment="${2}"
De la sorte, nous sommes en mesure de comprendre au premier coup d’œil ce qu’elles représentent. D’autre part, le contenu de ces variables peut changer selon le contexte. Le fait de stocker leur valeur à un instant donné dans une variable dédiée évite de perdre cette information.
Optimisation pragmatique : Chasser le surcoût inutile
Lors de la génération de scripts, il peut être tentant de s’appuyer sur des outils très puissants, mais aussi très coûteux. Or, un bon script doit limiter l’instanciation de sous-processus lourds et réduire au maximum les dépendances disproportionnées, voire inutiles.
Par exemple, utiliser des outils externes comme sed, awk ou cut pour de simples manipulations de chaînes de caractères (extraire une extension, nettoyer un chemin) est un gaspillage de ressources. Bash possède des fonctionnalités de manipulation native bien plus rapides. Par exemple :
${str#*-}supprime le préfixe le plus court correspondant au motif.${str%/*}supprime le suffixe le plus court (idéal pour récupérer le dossier parent d’un fichier).
Si nous repérons une usine à gaz à base de echo "$var" | sed ..., nous devons nous demander s’il ne serait pas plus simple d’utiliser une expansion de paramètre native de Bash, et encourager notre IA favorite à exploiter ce genre de modèles lorsque c’est pertinent.
Configuration et Isolation : Anticiper le plantage
Pour être capables de comprendre une erreur dans nos logs de CI/CD, le script doit être prévisible et isolé.
L’isolation des répertoires par les Subshells
Si le script généré doit changer de répertoire pour exécuter une tâche (un cd pour entrer dans un sous-dossier de configuration par exemple), nous devons nous assurer que l’IA a encapsulé cette logique dans un subshell via des parenthèses ( ... ) :
(
cd /tmp/deploy-assets
npm install
)
# Ici, nous sommes automatiquement revenus au répertoire initial, sans casser le reste de la pipeline.
Sans cela, si une étape échoue après un cd, toutes les commandes suivantes s’exécuteront au mauvais endroit, corrompant notre espace de travail.
La gestion des options avec getopts
Lorsque nous générons des scripts simples, nous allons souvent commencer par un traitement très simple des arguments, en lisant manuellement des variables comme $1, $2 ou $3. Mais lorsque notre script se complexifie, cette approche peut rapidement devenir casse-cou.
C’est à ce moment-là qu’il peut être pertinent de basculer versgetopts. Cela standardise l’interface de notre script (ex: ./deploy.sh -e prod -v v1.2) et permet de lever des erreurs claires en cas de mauvaise configuration (via le pattern ?).
Nous n’aurons plus à manipuler directement getopts à la main, mais je voulais tout de même vous fournir un petit exemple concret, car la lecture n’en est pas toujours évidente, au premier abord :
#!/bin/bash
# ...
# L'analyse des options avec getopts :
# - Le ":" au tout début active le mode "silencieux" (évite les messages d'erreur natifs de Bash)
# - "e:v:h" signifie que -e et -v attendent un argument (grâce aux deux-points), et -h est un simple drapeau
while getopts ":e:v:h" opt; do
case "${opt}" in
e)
env="${OPTARG}"
;;
v)
version="${OPTARG}"
;;
h)
usage
;;
\?)
# Intercepte une option inconnue (ex: ./script.sh -z)
echo "Erreur : Option invalide -> -${OPTARG}" >&2
usage
;;
:)
# Intercepte l'absence d'argument pour une option qui en requiert un (ex: ./script.sh -e)
echo "Erreur : L'option -${OPTARG} requiert un argument." >&2
usage
;;
esac
done
# Validation des paramètres obligatoires après la boucle
if [[ -z "${env}" || -z "${version}" ]]; then
echo "Erreur : Les options -e et -v sont obligatoires." >&2
usage
fi
# Le reste du script peut s'exécuter en toute sécurité
echo "--> Paramètres validés avec succès."
echo "--> etc..."
Débogage avancé : Dompter les codes de retour et les signaux
Quand la pipeline passe au rouge, comment identifier le coupable ?
Le piège de la variable $?
Du code généré à la va-vite peut contenir desif [ $? -eq 0 ] pour tester le succès d’une commande. C’est une mauvaise pratique majeure. La valeur de $? est volatile : la moindre commande intermédiaire, y compris un simple echo "Vérification en cours", va écraser le code de retour que nous cherchions à analyser. Si le code doit inspecter un statut, il doit le stocker immédiatement : status=$?.
Le filet de sécurité : trap
C’est l’outil ultime pour le débogage en CI/CD. La commande trap permet d’intercepter les signaux de sortie ou d’erreur (EXIT, ERR) pour exécuter une fonction de nettoyage ou de log personnalisée, quoi qu’il arrive (même si le script plante au milieu).
C’est ce qui fait la différence entre un script amateur et un outil de production de niveau entreprise.
Savoir pivoter quand Bash atteint ses limites
Bash n’est pas non plus une solution universelle pour tous les cas de figure. Il peut arriver que nous atteignions les limites, soit parce que le volume de scripts devient conséquent, soit parce que nous abordons des cas d’usage particuliers où Bash manque de souplesse (typiquement le cas de la manipulation de chaînes de caractères ou d’objets complexes).
Dans ces cas là, revoir sa stratégie et explorer d’autres langages, comme Python devient une nécessité. Nous avons déjà eu l’occasion d’aborder et développer ce sujet, par le passé : Bash ou Python, pour l’automatisation ?.
Le Prompt de Validation : Notre Template de Production
Pour ne plus avoir à corriger manuellement les approximations de l’IA, nous pouvons utiliser ce template comme structure de référence et le fournir directement à notre LLM en lui disant : « Génère le script demandé en respectant scrupuleusement cette architecture de code. »
#!/bin/bash
# ==============================================================================
# Titre : script_name.sh
# Description : Renseigne ici la description du script.
# ==============================================================================
set -euo pipefail
cleanup() {
local exit_code=$?
echo "--> Libération des ressources et nettoyage (Code de sortie : ${exit_code})...."
# Ajoute ici toutes les suppressions de fichiers temporaires, fermeture de tunnels, etc.
}
trap cleanup EXIT
# Fonction d'aide / Usage
usage() {
echo "Usage: $0 ..." # Remplace les points de suspensions par les arguments du script
echo " -h : display this help then exit."
# Documente chacun des arguments à la suite.
exit 1
}
main() {
local version=""
local environment=""
# Replace with the suitable arguments for the script here.
while getopts "v:e:" opt; do
case "${opt}" in
v) version="${OPTARG}" ;;
e) environment="${OPTARG}" ;;
*) usage ;;
esac
done
if [[ -z "${version}" || -z "${environment}" ]]; then
echo "Erreur : Paramètres manquants."
usage
fi
echo "Début d'exécution..."
# Integrate the core logic here
}
# Lancement du script avec transmission de tous les arguments globaux
main "$@"
Avec l’IA, notre valeur ajoutée réside dans notre capacité à imposer cette rigueur structurelle. L’IA produit la force brute ; nous apportons le cadre, la sécurité et la maîtrise de la production.
Write a comment