Linux sans magie #34 — Écrire un script Bash propre : arguments, "$@", fonctions et gestion des erreurs
Construire un script Bash lisible avec paramètres positionnels, "$@", fonctions, messages d’usage et gestion explicite des erreurs.
Les articles précédents ont présenté les briques séparément : codes de retour, variables, conditions et boucles.
Il est temps de les assembler dans un vrai script.
L’objectif n’est pas de construire un framework Bash, ni d’empiler des options obscures. Nous allons partir d’une structure minimale et lisible : recevoir des arguments, les valider, répartir le travail dans des fonctions et retourner des erreurs compréhensibles.
À la fin, vous disposerez d’un modèle simple que vous pourrez adapter à des scripts d’administration ou d’automatisation.
Un script Bash commence par un interpréteur explicite
Pour un script destiné à Bash, utilisez un shebang explicite :
#!/usr/bin/env bash
Cette ligne demande au système de lancer le script avec bash trouvé dans le PATH.
Le script peut ensuite être exécuté directement s’il possède le droit d’exécution :
chmod +x mon-script.sh
./mon-script.sh
Ou sans modifier ses permissions :
bash mon-script.sh
Dans les exemples de cet article, nous utiliserons bash script.sh. Le lab ne dépend donc pas du bit exécutable.
Les paramètres positionnels : $1, $2, $3
Lorsqu’un script reçoit des arguments, Bash les expose sous forme de paramètres positionnels.
Exemple :
bash exemple.sh production rapport.txt
Dans le script :
$1
$2
correspondent respectivement à :
production
rapport.txt
Le nombre d’arguments est disponible dans $#.
Un premier script minimal peut donc vérifier qu’un argument est présent :
#!/usr/bin/env bash
if [[ $# -lt 1 ]]
then
printf 'Usage : %s <nom>\n' "$0" >&2
exit 2
fi
nom=$1
printf 'Bonjour %s\n' "$nom"
Ici, le script retourne 2 si l’appel est incomplet.
La valeur exacte d’un code d’erreur applicatif dépend de votre convention. L’essentiel est de distinguer le succès 0 d’un échec non nul et de documenter la signification des valeurs choisies.
Pourquoi citer les paramètres
Un argument peut contenir des espaces.
Exemple :
bash exemple.sh 'rapport final.txt'
Cette affectation est sûre :
fichier=$1
Et cette utilisation conserve la valeur comme un argument unique :
printf '%s\n' "$fichier"
Le même principe s’applique à "$1", "$2" et aux autres expansions.
Le quoting reste particulièrement important lorsque vous transmettez les arguments à une autre commande ou à une fonction.
"$@" : retransmettre tous les arguments sans les fusionner
Bash fournit plusieurs paramètres spéciaux. Pour transmettre tous les arguments à une autre commande ou fonction, la forme la plus utile est :
"$@"
Lorsqu’elle est entre guillemets doubles, chaque paramètre positionnel reste un argument distinct.
Créons un exemple :
afficher_arguments()
{
printf 'Nombre : %s\n' "$#"
for argument in "$@"
do
printf '<%s>\n' "$argument"
done
}
afficher_arguments "un" "deux mots" "trois"
Résultat attendu :
Nombre : 3
<un>
<deux mots>
<trois>
La chaîne deux mots reste un seul argument.
C’est précisément ce que l’on veut lorsque l’on retransmet les arguments du script :
traiter "$@"
"$*" et "$@" ne signifient pas la même chose
Avec des guillemets doubles, "$*" rassemble les paramètres positionnels en une seule chaîne, séparée selon le premier caractère de IFS.
À l’inverse, "$@" conserve un argument par paramètre.
Exemple conceptuel avec trois arguments :
un
deux mots
trois
"$@" les transmet comme trois arguments.
"$*" les représente comme un seul argument contenant l’ensemble.
Pour relayer fidèlement les arguments reçus, utilisez donc généralement :
"$@"
et non :
"$*"
Les fonctions structurent le script
Une fonction Bash permet de regrouper une tâche identifiable.
Exemple :
usage()
{
printf 'Usage : %s <source> <destination>\n' "$0"
}
Une autre fonction peut effectuer une validation :
verifier_fichier()
{
local fichier=$1
if [[ ! -f $fichier ]]
then
printf 'Erreur : fichier absent : %s\n' "$fichier" >&2
return 1
fi
return 0
}
Le mot-clé local crée ici une variable locale à la fonction.
Cela évite qu’une variable de travail comme fichier écrase involontairement une variable du même nom ailleurs dans le script.
return quitte une fonction, exit termine le script
La distinction est essentielle.
Dans une fonction :
return 1
termine la fonction et lui donne le statut 1.
Le script appelant peut ensuite décider quoi faire :
if ! verifier_fichier "$source"
then
exit 1
fi
À l’inverse :
exit 1
termine le script entier.
Une fonction utilitaire qui rencontre une erreur récupérable a donc souvent intérêt à utiliser return. Le niveau principal du script décide ensuite s’il faut abandonner avec exit, afficher un autre message ou tenter une autre action.
Valider les arguments avant de travailler
Une structure simple consiste à effectuer les validations dès le début.
Exemple :
if [[ $# -ne 2 ]]
then
usage >&2
exit 2
fi
source=$1
destination=$2
Le script n’essaie pas de continuer avec des paramètres incomplets.
Cette approche évite de découvrir tardivement qu’une variable obligatoire est vide ou absente.
Construire un lab local non destructif
Créons un répertoire temporaire :
LAB_DIR="$(mktemp -d)"
printf 'Lab : %s\n' "$LAB_DIR"
cd "$LAB_DIR"
Créons deux fichiers source :
printf '%s\n' 'alpha' > 'un.txt'
printf '%s\n' 'beta' > 'deux mots.txt'
Nous allons écrire un script qui affiche le contenu des fichiers passés en arguments, sans les modifier.
Créez inspecter.sh :
cat > inspecter.sh <<'EOF'
#!/usr/bin/env bash
usage()
{
printf 'Usage : %s <fichier> [fichier ...]\n' "$0"
}
verifier_fichier()
{
local fichier=$1
if [[ ! -f $fichier ]]
then
printf 'Erreur : fichier absent ou non régulier : %s\n' "$fichier" >&2
return 1
fi
return 0
}
afficher_fichier()
{
local fichier=$1
printf '%s\n' "--- $fichier ---"
cat -- "$fichier"
}
main()
{
if [[ $# -lt 1 ]]
then
usage >&2
return 2
fi
local fichier
for fichier in "$@"
do
if ! verifier_fichier "$fichier"
then
return 1
fi
afficher_fichier "$fichier" || return 1
done
return 0
}
main "$@"
exit $?
EOF
Le script contient quatre niveaux clairs :
usage: explique comment appeler le script ;verifier_fichier: valide un fichier ;afficher_fichier: effectue l’action ;main: orchestre les étapes.
Le niveau principal appelle :
main "$@"
Les arguments du script sont ainsi retransmis à main sans perdre leur séparation.
Vérifier le chemin de succès
Exécutez :
bash inspecter.sh 'un.txt' 'deux mots.txt'
Résultat attendu :
--- un.txt ---
alpha
--- deux mots.txt ---
beta
Puis vérifiez le statut :
printf 'status=%s\n' "$?"
Attention : cette commande doit être exécutée immédiatement après le script si vous voulez observer son statut, avant toute autre commande.
Sur le chemin de succès, le statut attendu est :
status=0
Vérifier une erreur d’usage
Exécutez le script sans argument :
bash inspecter.sh
status=$?
printf 'status=%s\n' "$status"
Le message d’usage doit être affiché sur la sortie d’erreur et le statut attendu est :
status=2
Le script distingue ainsi une erreur d’appel d’une erreur rencontrée pendant le traitement.
Vérifier une erreur de fichier
Exécutez :
bash inspecter.sh 'absent.txt'
status=$?
printf 'status=%s\n' "$status"
Le script doit signaler que le fichier n’est pas disponible et retourner :
status=1
La fonction verifier_fichier utilise return 1, puis main propage l’échec. Le niveau principal termine ensuite le script avec le même statut.
Propager explicitement les erreurs
Cette ligne mérite d’être comprise :
afficher_fichier "$fichier" || return 1
Si afficher_fichier retourne un statut non nul, main s’arrête avec 1.
Le comportement d’erreur est donc visible dans le code.
Pour un script pédagogique ou opérationnel simple, cette gestion explicite est souvent plus facile à relire qu’un comportement reposant entièrement sur des options globales comme set -e, dont les règles d’application dépendent du contexte syntaxique.
Cela ne signifie pas que set -e est inutilisable. Il est simplement hors du périmètre de ce #34 : ici, nous voulons voir clairement où une erreur est vérifiée et comment elle est propagée.
Ne pas masquer le statut que vous voulez conserver
Considérez :
commande
printf 'Commande terminée.\n'
exit $?
Le $? observé par exit correspond ici au statut de printf, pas à celui de commande.
Si vous devez conserver un statut, stockez-le immédiatement :
commande
status=$?
printf 'Commande terminée avec le statut %s.\n' "$status"
exit "$status"
Le même principe vaut après un appel de fonction.
Bonnes pratiques
Commencez par une structure simple et explicite :
validation des arguments
fonctions courtes
main
main "$@"
exit
Citez les paramètres et variables lorsqu’ils deviennent des arguments :
commande "$fichier"
fonction "$@"
Utilisez "$@" lorsque vous devez retransmettre fidèlement les arguments reçus.
Donnez un rôle précis aux fonctions. Une fonction qui valide un fichier ne devrait pas en même temps modifier plusieurs ressources sans raison claire.
Utilisez return pour signaler le succès ou l’échec d’une fonction, puis laissez le niveau supérieur décider s’il doit appeler exit.
Envoyez les messages d’erreur sur la sortie standard d’erreur :
printf 'Erreur : ...\n' >&2
Documentez la signification de vos codes de retour lorsque vous distinguez plusieurs types d’échec.
Sécurité
Un script Bash reçoit souvent des données qu’il ne contrôle pas : chemins, noms, options ou paramètres issus d’un utilisateur ou d’un autre outil.
Le quoting protège la séparation des arguments, mais ne transforme pas une donnée non fiable en donnée sûre pour tous les usages.
Évitez notamment de reconstruire une commande sous forme de chaîne pour l’exécuter ensuite avec eval.
Préférez transmettre directement les arguments :
commande "$argument"
plutôt que de concaténer du texte représentant une commande.
Pour les commandes qui acceptent des chemins pouvant commencer par -, utilisez -- lorsque la commande le supporte, comme dans :
cat -- "$fichier"
Les exemples de cet article ne requièrent aucun privilège élevé et ne modifient que les fichiers du lab.
Coûts
Le lab utilise uniquement Bash et quelques fichiers locaux. Il n’utilise aucun service cloud ni ressource facturable.
Limites et points de vigilance
Cet article reste volontairement centré sur la structure d’un petit script Bash.
Il ne couvre pas encore :
getoptspour analyser des options comme-fou-v;- les tableaux Bash ;
- les traps et le nettoyage automatique avec
trap; set -e,set -uetpipefail;- les fichiers de configuration externes ;
- les tests automatisés de scripts shell ;
- les outils d’analyse statique comme ShellCheck.
Ces sujets deviennent utiles dès que le script grandit, mais ils ne remplacent pas les bases présentées ici : arguments bien cités, fonctions courtes et gestion visible des erreurs.
Rollback ou nettoyage
Le lab a créé un répertoire temporaire et trois fichiers.
Vérifiez le chemin avant suppression :
printf '%s\n' "$LAB_DIR"
Quittez le répertoire :
cd /
Puis supprimez uniquement le lab :
rm -rf -- "$LAB_DIR"
Cette commande est destructive pour le contenu du répertoire indiqué. Vérifiez donc la valeur de LAB_DIR avant de l’exécuter.
Références officielles
- Bash Reference Manual — Shell Parameters — GNU Project
- Bash Reference Manual — Special Parameters — GNU Project
- Bash Reference Manual — Shell Functions — GNU Project
- Bash Reference Manual — Bourne Shell Builtins — GNU Project
- Bash Reference Manual — The Set Builtin — GNU Project
Conclusion
Un script Bash propre n’a pas besoin d’être sophistiqué.
Commencez par valider les arguments, transmettez-les avec "$@", découpez le travail en fonctions et rendez les erreurs visibles avec des statuts explicites.
Cette structure suffit déjà à transformer une suite de commandes en un script lisible, prévisible et plus facile à maintenir.
