ShellCheck en 2026 : fiabiliser ses scripts Bash
Apprenez à utiliser ShellCheck pour détecter les erreurs Bash, corriger les alertes utiles et l’intégrer simplement à vos scripts et à la CI.
Un script Bash peut fonctionner pendant des semaines, puis échouer au premier nom de fichier contenant un espace, à une variable vide ou à un répertoire absent. Ces incidents ne viennent pas forcément d’une mauvaise logique métier : ils sont souvent causés par les règles parfois déroutantes de l’expansion de paramètres, du découpage en mots et des codes de retour du shell.
ShellCheck est un analyseur statique pour les scripts shell. Il lit le code sans l’exécuter, identifie des constructions risquées et propose généralement une correction. Pour une équipe qui maintient des scripts d’administration, de déploiement ou de sauvegarde, c’est l’un des contrôles les plus rentables à placer avant l’exécution.
Ce guide se concentre sur les problèmes réellement fréquents dans les scripts Bash : variables sans guillemets, substitutions de commande fragiles, changements de répertoire ignorés et erreurs masquées. L’objectif n’est pas d’obéir aveuglément à chaque avertissement, mais de comprendre le risque, de corriger le code de façon lisible et d’automatiser le contrôle.
Pourquoi ShellCheck reste indispensable pour les scripts Bash
Bash est très efficace pour relier des commandes système, manipuler des fichiers et automatiser des tâches simples. Mais le langage laisse une grande place aux comportements implicites. Une variable non entourée de guillemets peut être séparée en plusieurs arguments. Un caractère * peut être interprété comme un motif de fichiers. Une commande en échec peut être oubliée si le script poursuit son chemin.
ShellCheck transforme ces pièges connus en alertes identifiables. Il prend en charge plusieurs dialectes de shell, dont Bash, POSIX sh, dash et ksh. Pour un script destiné à Bash, il est important de l’indiquer explicitement avec le shebang :
#!/usr/bin/env bash
Cette première ligne aide ShellCheck, les éditeurs et les personnes qui reliront le fichier à interpréter correctement la syntaxe. Si votre script doit être strictement compatible avec /bin/sh, utilisez plutôt :
#!/bin/sh
Il ne faut pas confondre ShellCheck et un test fonctionnel. L’outil ne sait pas si votre serveur de destination est accessible, si une archive est valide ou si une API répond correctement. En revanche, il peut signaler avant l’exécution qu’une commande recevra probablement des arguments incorrects ou qu’un contrôle de retour est mal formulé.
Son intérêt est particulièrement net sur les scripts qui touchent à la production :
- sauvegardes avec rsync, tar ou des volumes montés ;
- scripts de déploiement qui manipulent des chemins et des archives ;
- tâches planifiées par cron ou systemd ;
- scripts de maintenance exécutés avec sudo ;
- outils internes qui lisent des fichiers, des journaux ou des variables d’environnement.
ShellCheck ne remplace donc ni la revue de code ni les tests, mais il élimine un ensemble d’erreurs répétitives avant qu’elles n’atteignent le serveur. C’est exactement le type de garde-fou simple qui rend un terminal utile, sans ajouter de folklore au processus.
Installer ShellCheck selon votre environnement
ShellCheck est disponible dans les dépôts de nombreuses distributions Linux. Sur Debian et Ubuntu, l’installation passe généralement par APT :
sudo apt update && sudo apt install shellcheck
Sur les systèmes basés sur Fedora, RHEL ou compatibles, utilisez le gestionnaire de paquets de votre environnement :
sudo dnf install ShellCheck
Sur Arch Linux :
sudo pacman -S shellcheck
Sur macOS, Homebrew propose le paquet :
brew install shellcheck
Vérifiez ensuite que la commande est accessible :
shellcheck --version
Si vous ne souhaitez pas installer l’outil sur la machine hôte, ShellCheck peut aussi être exécuté dans un conteneur. L’image koalaman/shellcheck-alpine est couramment utilisée pour cela. Depuis la racine d’un dépôt, un exemple avec Docker est :
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck-alpine /mnt/scripts/backup.sh
Le montage du répertoire courant dans le conteneur permet à ShellCheck de lire les scripts du projet. Cette approche est pratique pour homogénéiser le contrôle en local et dans une CI, mais une installation native reste souvent la voie la plus rapide sur un poste Linux d’administration.
Dans Visual Studio Code, l’extension ShellCheck permet aussi d’afficher les alertes directement dans l’éditeur, à condition que le binaire soit disponible ou que l’extension soit configurée pour y accéder. L’affichage en direct est utile, mais la commande en ligne reste la référence à automatiser.
Lancer un premier audit sur un script existant
Sur un fichier Bash existant, commencez simplement par :
shellcheck backup.sh
Chaque diagnostic affiche une ligne, une colonne, un niveau de gravité, un message et un identifiant tel que SC2086. Cet identifiant est précieux : il mène à la documentation correspondante sur le site officiel de ShellCheck et permet de retrouver rapidement les mêmes cas dans un dépôt.
Pour analyser plusieurs scripts, vous pouvez transmettre une liste de fichiers :
shellcheck scripts/*.sh
Cette commande est volontairement simple. Elle convient si tous les scripts sont directement dans le dossier visé. Pour un projet plus structuré, évitez de transformer trop vite une recherche de fichiers en longue substitution de commande. Une boucle lisible est souvent plus robuste :
find scripts -type f -name '*.sh' -exec shellcheck {} +
La forme -exec … {} + transmet les fichiers trouvés en arguments à ShellCheck sans casser les chemins qui contiennent des espaces. C’est plus sûr qu’un assemblage de texte. Pour approfondir ce sujet, consultez aussi notre guide sur grep, find et xargs pour les fichiers.
Si des scripts n’ont pas d’extension ou si leur dialecte n’est pas évident, forcez l’analyse Bash :
shellcheck -s bash bin/deploy
Le code de retour de ShellCheck est également utile : il est nul lorsqu’aucun problème au niveau contrôlé n’est trouvé, et non nul lorsqu’il détecte des diagnostics. C’est ce comportement qui rend son intégration en précommit ou en CI naturelle.
Analyser les fichiers inclus avec source
Les scripts Bash sont souvent découpés en bibliothèques chargées avec source ou avec le raccourci .. Par défaut, ShellCheck peut ne pas suivre tous ces fichiers, notamment si le chemin est dynamique. L’option suivante demande de suivre les inclusions :
shellcheck -x bin/deploy
Si le chemin ne peut pas être résolu automatiquement, ajoutez une annotation près de l’inclusion :
# shellcheck source=../lib/common.sh
source "$script_dir/../lib/common.sh"
L’annotation renseigne ShellCheck sans modifier le comportement du script. Elle est préférable à une désactivation globale, car elle documente clairement le fichier attendu.
Corriger SC2086 : protéger les expansions de variables
L’alerte SC2086 fait partie des plus rencontrées. Elle concerne souvent une variable développée sans guillemets. Prenons un script qui crée une archive :
tar -czf $archive $source_dir
Si $archive ou $source_dir contient un espace, Bash peut transmettre plusieurs arguments à tar au lieu d’un seul. Le résultat peut être une erreur, une archive mal nommée ou un comportement inattendu.
La correction habituelle est explicite :
tar -czf "$archive" "$source_dir"
La règle pratique est simple : mettez les expansions de paramètres entre guillemets doubles sauf si vous voulez délibérément le découpage en mots ou l’expansion de motifs. Dans les scripts d’administration, ce cas volontaire est beaucoup plus rare que l’oubli des guillemets.
La même vigilance s’applique aux suppressions et aux copies :
rm -f -- "$file"
cp -- "$source" "$destination"
Le séparateur -- est utile pour les commandes qui le prennent en charge : il indique la fin des options. Ainsi, un fichier nommé -important.log sera traité comme un fichier, et non comme une option de la commande.
Les tableaux Bash constituent une autre source d’erreurs. Pour transmettre exactement chaque élément d’un tableau comme un argument distinct, utilisez :
rsync -a -- "${files[@]}" "$destination/"
À l’inverse, ${files[*]} et une expansion non protégée risquent de fusionner ou de découper les éléments. C’est un point important dans les scripts qui construisent des listes de fichiers avant de les remettre à rsync, curl ou une commande maison. Notre article sur rsync sous Linux complète utilement cette approche pour les synchronisations.
Éviter les substitutions de commande fragiles et SC2046
Une autre alerte courante, SC2046, vise les substitutions de commande non entourées de guillemets. Voici une construction typique :
rm $(find "$tmp_dir" -type f -name '*.tmp')
Elle paraît compacte, mais elle traite la sortie de find comme du texte séparé par des espaces. Un fichier comme rapport final.tmp sera donc découpé en deux arguments. Pire, si aucun fichier n’est trouvé ou si des caractères spéciaux apparaissent, le comportement devient difficile à anticiper.
La bonne correction n’est pas de placer mécaniquement des guillemets autour de toute la substitution. Dans ce cas précis, il faut éviter de faire circuler une liste de noms de fichiers sous forme de texte :
find "$tmp_dir" -type f -name '*.tmp' -delete
Si vous souhaitez d’abord afficher les fichiers puis les supprimer, utilisez une exécution directe :
find "$tmp_dir" -type f -name '*.tmp' -print -delete
Dans d’autres situations, les guillemets sont bien la réponse. Pour récupérer une valeur courte produite par une commande :
current_branch=$(git branch --show-current)
printf 'Branche active : %s\n' "$current_branch"
La substitution elle-même est adaptée ici, car elle stocke une seule valeur. Ce qu’il faut éviter, c’est de réinjecter sans protection une sortie qui représente une liste d’éléments. Les noms de fichiers ne sont pas des lignes de texte ordinaires : ils peuvent contenir des espaces, des tabulations et même des retours à la ligne.
Cette distinction est aussi utile avec curl, jq ou grep. Pour manipuler des données structurées, préférez des formats et des outils adaptés plutôt qu’une chaîne de substitutions difficile à relire. Vous pouvez notamment consulter le guide consacré à jq pour manipuler du JSON.
Traiter les échecs immédiatement : cd, tests et codes de retour
ShellCheck signale fréquemment un changement de répertoire dont l’échec n’est pas contrôlé. Le problème classique ressemble à ceci :
cd "$release_dir"
rm -rf cache
Si le répertoire n’existe pas ou n’est pas accessible, le cd échoue. La commande suivante sera alors exécutée dans le répertoire courant, ce qui peut devenir très dangereux selon le contexte. L’alerte associée est souvent SC2164.
La correction la plus directe est :
cd "$release_dir" || exit 1
Dans une fonction, utilisez plutôt un retour adapté :
cd "$release_dir" || return 1
Cette écriture rend le comportement visible : le script n’effectue pas les opérations suivantes si son prérequis n’est pas satisfait.
Une autre habitude fragile consiste à tester $? après une commande :
backup_database
if [ $? -ne 0 ]; then
exit 1
fi
ShellCheck peut suggérer de tester directement la commande, ce qui évite qu’une ligne intermédiaire écrase son code de retour :
if ! backup_database; then
exit 1
fi
Ou, lorsqu’un échec doit arrêter immédiatement le script :
backup_database || exit 1
Le réglage set -e est parfois présenté comme une solution universelle. Il ne l’est pas : son comportement dépend du contexte, notamment dans les conditions, les listes de commandes et certaines substitutions. Il peut être utile dans un script maîtrisé, mais il ne dispense pas d’écrire des contrôles explicites pour les étapes critiques.
De même, set -u peut aider à détecter les variables non définies, et set -o pipefail évite qu’un pipeline soit considéré comme réussi uniquement parce que sa dernière commande réussit. Ces options doivent être introduites avec des tests adaptés au script existant. ShellCheck aide à repérer les zones qui demandent cette attention, sans transformer automatiquement un ancien script en code sûr.
Lire les alertes sans dégrader la lisibilité du script
Un bon traitement de ShellCheck ne consiste pas à faire taire tous les messages le plus vite possible. Commencez par les avertissements qui peuvent modifier les arguments transmis, masquer une erreur ou toucher à des fichiers inattendus. Les alertes liées à des variables inutilisées ou à des styles plus discutables peuvent venir ensuite.
Il est parfois légitime de conserver une construction signalée. Dans ce cas, désactivez précisément l’identifiant concerné, juste avant la ligne, et ajoutez une raison lisible :
# La séparation en mots est volontaire : l’outil attend une liste d’options.
# shellcheck disable=SC2086
command_with_options $options
Cette pratique doit rester exceptionnelle. Une désactivation globale dans l’en-tête d’un script peut cacher de nouveaux problèmes introduits plus tard. Une annotation locale limite le périmètre et force la personne qui modifie le code à revoir le choix.
Un autre exemple fréquent concerne une affectation qui masque le code de retour d’une commande, souvent signalée par SC2155. Au lieu de déclarer et initialiser dans la même instruction :
local output=$(generate_report)
séparez les deux opérations :
local output
output=$(generate_report)
Cette forme rend possible la vérification immédiate de l’échec de generate_report et reste facile à maintenir. Le code Bash gagne rarement à être plus compact si cette concision masque un comportement important.
Ajouter ShellCheck à Git avant le commit
Le meilleur moment pour détecter un problème est avant l’envoi du commit. Un hook Git local peut lancer ShellCheck sur les scripts suivis par le dépôt. Créez le fichier .git/hooks/pre-commit, rendez-le exécutable avec chmod +x .git/hooks/pre-commit, puis placez-y une logique simple.
#!/usr/bin/env bash
set -u
find scripts -type f -name '*.sh' -exec shellcheck {} +
Ce hook bloque le commit si ShellCheck retourne une erreur. Il convient à un petit dépôt dont les scripts sont tous dans un répertoire identifié. Pour éviter que le dossier .git/hooks reste hors versionnement, vous pouvez stocker les hooks dans un répertoire du projet, par exemple .githooks, puis configurer Git :
git config core.hooksPath .githooks
Cette configuration doit être faite par chaque clone ou documentée dans les consignes de démarrage du projet. Le hook est une aide locale, pas une garantie : Git permet de le contourner, et tous les contributeurs ne l’activeront pas forcément. La CI doit donc rester l’autorité finale.
Vous pouvez aussi limiter le contrôle aux fichiers ajoutés à l’index. C’est plus rapide sur un gros dépôt, mais plus complexe à écrire correctement, notamment pour les renommages et les chemins inhabituels. Dans la plupart des projets de scripts, analyser l’ensemble du dossier reste suffisamment rapide et bien plus simple à relire.
Mettre en place une CI légère avec GitHub Actions ou GitLab CI
Une CI apporte une vérification identique pour chaque branche et chaque demande de fusion. Sur GitHub Actions, un workflow peut installer ShellCheck puis analyser le répertoire des scripts. Placez ce fichier dans .github/workflows/shellcheck.yml :
name: ShellCheck
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y shellcheck
- run: find scripts -type f -name '*.sh' -exec shellcheck {} +
Adaptez scripts au répertoire réel de votre dépôt. Si vos exécutables sont dans bin sans extension, listez-les explicitement ou lancez ShellCheck avec -s bash sur ces fichiers.
Sur GitLab CI, le même principe peut s’appuyer sur l’image conteneur dédiée :
shellcheck:
image: koalaman/shellcheck-alpine
script:
- find scripts -type f -name '*.sh' -exec shellcheck {} +
Dans les deux cas, gardez la règle de CI proche de la commande utilisée en local. Si le développeur lance shellcheck -x localement parce que le projet utilise des fichiers sourcés, la CI doit employer la même option. Sinon, vous obtiendrez des résultats divergents et perdrez la confiance dans le contrôle.
Pour les projets plus exigeants, ShellCheck propose différents formats de sortie, dont un format compatible avec les diagnostics de compilation. Mais ne complexifiez pas la première intégration : un job qui échoue clairement avec le fichier, la ligne et le code SC est déjà très efficace.
Une routine réaliste pour fiabiliser progressivement un dépôt Bash
Sur un dépôt ancien, ShellCheck peut produire beaucoup d’alertes. Ne bloquez pas immédiatement toute l’équipe avec une correction massive et risquée. Commencez par exécuter l’outil, triez les diagnostics et corrigez en priorité les problèmes qui affectent les chemins, les suppressions, les copies, les archives et les accès distants.
- Ajoutez un shebang cohérent à chaque script.
- Corrigez les expansions non protégées et les listes de fichiers construites comme du texte.
- Vérifiez les cd, les commandes critiques et les pipelines.
- Documentez localement les rares exceptions avec une directive ShellCheck précise.
- Ajoutez ensuite le contrôle au hook Git et à la CI.
Les scripts planifiés méritent une attention particulière : une erreur silencieuse dans un cron peut ne devenir visible que lorsque la sauvegarde est déjà manquante. Avant d’automatiser un script validé par ShellCheck, relisez aussi sa planification avec notre article sur crontab et les tâches automatiques fiables, ou envisagez les timers systemd selon votre contexte.
Conclusion : faire de ShellCheck un réflexe, pas une étape pénible
ShellCheck ne rend pas un script Bash parfait et ne remplace pas les tests sur un environnement représentatif. En revanche, il détecte rapidement une large part des erreurs de robustesse qui coûtent du temps en administration : arguments mal découpés, fichiers mal ciblés, échecs ignorés et inclusions mal comprises.
Commencez sur un script qui manipule des fichiers ou déclenche une sauvegarde, corrigez les alertes qui ont un impact concret, puis installez le même contrôle dans votre dépôt et votre CI. Avec cette routine, les scripts restent courts, lisibles et nettement plus sûrs à exécuter en production.