Vous débutez tout juste dans les scripts Bash ? 10 conseils simples pour bien faire les choses
Agence web » Actualités du digital » Vous débutez tout juste dans les scripts Bash ? 10 conseils simples pour bien faire les choses

Vous débutez tout juste dans les scripts Bash ? 10 conseils simples pour bien faire les choses

Les scripts Bash sont puissants, mais la puissance implique de grandes responsabilités. Il est très facile qu'un code bâclé ou mal planifié cause de réels dégâts, c'est donc une bonne idée d'être prudent et de pratiquer une programmation défensive.

Heureusement, Bash dispose de plusieurs mécanismes intégrés pour vous protéger. Beaucoup d’entre elles impliquent des mises à jour de la syntaxe qui ont remplacé des méthodes plus anciennes et problématiques. Vous pouvez utiliser ces suggestions pour réduire les risques de bogues, déboguer vos programmes et gérer les cas extrêmes.

Utilisez une bonne ligne Shebang

La première ligne de votre script shell doit toujours être un commentaire spécial, connu sous le nom de shebang ou hashbang, qui déclare quel interpréteur doit exécuter le script. Il peut s'agir du nom d'un shell, d'un langage de programmation ou, en théorie, de toute autre commande. Vous pouvez vous en sortir sans un shebang, mais pour rendre votre script autonome et lui faire annoncer la langue dans laquelle il a été écrit, un shebang est essentiel.

Il existe deux écoles de pensée principales sur la manière dont vous devez structurer votre shebang. Le premier est plus traditionnel et ressemble à ceci :

        #!/bin/bash
echo "Hello, world"

Cette ligne shebang indique à tout shell qui exécute le script qu'il doit le transmettre à un programme qui réside dans /bin/bash. Même si cette approche convient et devrait fonctionner presque tout le temps, certaines personnes préfèrent la suivante :

        #!/usr/bin/env bash
echo "Hello, world"

Avec un seul argument, la commande env l'exécute simplement, c'est-à-dire que ce shebang fera env exécuter bash et lui transmettre le script. La grande différence ici est que env utilise un nom de commande plutôt qu'un chemin complet vers un exécutable. C'est le même genre de différence que vous obtenez sur la ligne de commande lorsque vous exécutez un programme :

        ls -l *.md
/bin/ls -l *.md

Un simple nom de commande, sans chemin, exécutera la version de cette commande la plus logique dans son contexte. Il peut s'agir d'une fonction shell, d'un alias ou d'un fichier programme situé quelque part dans votre PATH. Il est important de noter que si vous avez des versions dans, par exemple, /bin, /usr/local/bin et ~/.local/bin, env exécutera généralement la version « la plus locale », ce qui est généralement ce que vous souhaitez.

L'approche env présente l'avantage que peu importe si votre programme bash se trouve dans /bin, /local/bin, ~/bin ou ailleurs, tant qu'il se trouve dans votre PATH. Il s’agit de l’option la plus portable : elle fonctionnera sur des systèmes plus diversifiés, qui peuvent ne pas être configurés exactement de la même manière que le vôtre.

Pendant ce temps, la version /bin/bash garantira que le programme dans un emplacement spécifique s'exécutera, même si un autre est installé ailleurs. Cela peut être une option plus sécurisée, puisqu'une autre version de bash ne peut pas détourner le script.

Aucune des deux approches n’est plus correcte que l’autre ; ils sont simplement différents. L’important est de comprendre les différences et de choisir l’approche adaptée à votre situation. Si vous écrivez simplement des scripts pour votre propre usage, cela ne devrait pas avoir trop d'importance de toute façon.

Citez toujours vos variables

Peu de choses ont causé plus de problèmes sous Linux que son approche des espaces, qui sépare les commandes de leurs arguments, et chaque argument des autres. Si vous n'y faites pas attention, les espaces peuvent facilement causer des problèmes, surtout lorsque vous commencez à travailler avec des variables.

Prenons cet exemple :

        #!/bin/bash

FILENAME="docs/Letter to bank.doc"
ls $FILENAME

Lorsque Bash développe une variable, il le fait de manière très littérale ; la dernière ligne finira par être l'équivalent de :

        ls docs/Letter to bank.doc

Étant donné que les espaces séparent les arguments, Bash interprétera cela comme un appel à ls avec trois arguments : « docs/Letter », « to » et « bank.doc : »

Pour éviter ce problème, assurez-vous de toujours citer les variables lorsque vous les utilisez, comme ceci :

        ls "$FILENAME"

Vous avez peut-être repéré des scripts qui mettent également les noms de variables entre accolades, comme ceci :

        ls "${FILENAME}"

C'est une autre bonne idée, même si ce n'est pas nécessaire dans cet exemple spécifique. Mettre un nom de variable entre accolades facilite le suivi avec un autre texte littéral, comme :

        echo "_${FILENAME}_ is one of my favourite files"

Sans les accolades, Bash essaierait de trouver une variable nommée FILENAME_ et échouerait.

Arrêtez votre script en cas d'erreur

Peu de choses sont aussi risquées qu’un échec incontrôlé. Dans un script shell, vous pouvez appeler de nombreuses commandes différentes, en espérant qu'elles réussissent. Vous devriez vérifier cela attentivement, mais voici un filet de sécurité utile qui vous aidera quand même à vous protéger :

        set -e

Le manuel Bash décrit la fonction de ce paramètre comme suit :

Quittez immédiatement si un pipeline, qui peut consister en une seule commande simple, une liste ou une commande composée, renvoie un statut différent de zéro.

En termes simples, votre script s'arrêtera si quelque chose ne va pas et que vous ne l'avez pas encore géré. Prenons cet exemple :

        #!/bin/bash

touch /file
echo "Now do something with that file..."

Le script ici suppose que le toucher réussira, mais cette hypothèse est dangereuse :

Ajouter un appel à définir -e entraînera l'arrêt du script dès que la commande touch échoue :

La commande set peut modifier diverses options qui contrôlent le fonctionnement du shell. Voir aussi, par exemple, le paramètre pipefail :

        set -o pipefail

Cela garantit qu'un pipeline se terminera avec un statut différent de zéro, pour indiquer une défaillance, si l'un de ses composants tombe en panne. Par défaut, une panne qui survient au début d’un pipeline peut facilement passer inaperçue.

Payez au suivant : arrêtez-vous en cas d'échec

Échouer sur erreur est un fourre-tout important, mais vous devez également chercher à gérer des échecs spécifiques et prendre les mesures appropriées. Un moyen simple de vérifier l’échec consiste à vérifier l’état de sortie d’une commande.

Vous pouvez vérifier l'état de sortie d'une commande en inspectant le $? variable après l'avoir exécutée :

        cd "$DIR"

if ( $? -ne 0 ); then
exit
fi

En raccourci, vous pouvez également utiliser les opérateurs logiques Bash :

        cd "$DIR" || (echo "bad"; exit)

Déboguer chaque commande

Une autre option shell très précieuse est xtrace :

        set -o xtrace

Cette option amène le shell à imprimer les commandes avant de les exécuter, ce qui est très utile lors du débogage :

Le shell imprime désormais chaque commande au fur et à mesure de son exécution, y compris ses arguments.

Il existe de nombreuses autres options qui peuvent vous aider à contrôler le comportement du shell à l'aide de set. Je recommande fortement de lire l'ensemble intégré dans le manuel de Bash.

Utiliser des paramètres longs lors de l'appel d'autres commandes

Les commandes Linux peuvent prêter à confusion car elles ont tendance à utiliser des options à une seule lettre :

        rm -rf filename

Avec les commandes couramment utilisées, c'est moins un problème, mais il y a tellement de commandes et d'options que vous finirez forcément par rencontrer quelque chose d'inconnu. De bonnes pratiques de programmation doivent garantir que votre script est lisible, que ce soit par un autre membre de votre équipe, quelqu'un avec qui vous n'avez jamais communiqué ou vous-même à un moment donné dans le futur.

Voici un équivalent beaucoup plus lisible de la commande précédente :

        rm --recursive --force filename

De nombreuses commandes modernes, ou versions modernes de commandes établies de longue date, prennent en charge des options longues comme celle-ci, qui commencent par un « — » et sont des mots complets plutôt que des lettres simples. Vous ne pouvez pas les combiner comme les options à une seule lettre, mais elles sont beaucoup plus lisibles.

Vous ne devriez pas avoir à saisir ces options dans leur intégralité à chaque fois que vous utilisez une commande si vous vous souvenez de l'alternative la plus courte. Mais dans vos propres scripts shell, en particulier ceux que vous pouvez partager avec d'autres, l'utilisation d'options longues est une forme de code auto-documenté que vous devriez toujours viser.

Utiliser la notation moderne pour la substitution de commandes

Dans les scripts Bash, il existe deux manières d'exécuter une commande et de capturer sa sortie dans une variable :

        VAR=$(ls)
VAR2=`ls`

Vous verrez les deux être utilisés, alors lequel est le meilleur ?

L’approche backtick est en fait obsolète ; c'est un peu plus gênant pour diverses raisons, comme le fait qu'il ne prend pas très bien en charge la nidification. Préférez donc toujours la forme moderne, en utilisant des parenthèses.

Déclarer les valeurs par défaut

Autre syntaxe avancée pratique, celle-ci vous permet de spécifier les valeurs de variables par défaut sans écrire de code supplémentaire pour vérifier une chaîne vide :

        CMD=${PAGER:-more}

Dans cet exemple, la valeur de $CMD sera la valeur de la variable d'environnement PAGER, si elle est définie, et « plus » sinon.

Vous pouvez même imbriquer les valeurs par défaut. Cela vous permet de prendre en charge un argument de ligne de commande, avec des solutions de secours pour une variable d'environnement, puis une valeur par défaut, par exemple :

        DIR=${1:-${HOME:-/Users/bobby/home}}
    

Soyez explicite sur les options avec un double tiret

Tout comme les espaces dans les noms de fichiers peuvent être problématiques, de nombreux autres caractères le peuvent également. Un exemple classique est le cas d'un fichier commençant par « -: »

        echo "nothing much" > -a-silly-filename

Vous pouvez confirmer l'existence de ce fichier en listant son répertoire :

Mais interagir directement avec le fichier, en utilisant son nom, posera des problèmes :

Comme la plupart des commandes, ls s'attend à ce qu'un argument commençant par un « – » soit une option, d'où l'erreur « option non reconnue ». Cela peut sembler un problème trivial, mais cela devient bien pire si vous considérez une commande comme celle-ci :

        rm *

Si vous avez un fichier dans votre répertoire nommé « -rf », cela pourrait conduire à un désastre !

Vous pouvez éviter beaucoup de problèmes en gardant les noms de fichiers simples : les noms az minuscules sans aucun autre caractère ne devraient jamais poser de problème. Cependant, vous devez toujours programmer de manière défensive dans vos propres scripts et programmes pour éviter les problèmes.

La meilleure protection contre ce genre de problème est la syntaxe « double tiret », qui ressemble à ceci :

        rm -- *.md

Le double tiret déclare explicitement que « tout ce qui suit est un argument », ce qui signifie que rm n'interprétera pas les noms de fichiers étranges comme s'il s'agissait d'options.

Utiliser des variables locales dans les fonctions

Vous avez peut-être entendu dire que les variables globales sont dangereuses ou déconseillées. Même si la vérité est plus nuancée, il est souvent judicieux d'éviter les variables globales, à moins de savoir vraiment ce que vous faites.

Dans un script shell, les variables sont globales par défaut, même à l'intérieur des fonctions :

        #!/bin/bash

function run {
    DIR=`pwd`
    echo "doing something..."
}

DIR="/usr/local/bin"
run
echo $DIR

Il est facile de réutiliser accidentellement un nom de variable et d'oublier que vous modifiez sa valeur dans l'ensemble de votre script, et pas seulement dans la fonction en cours d'exécution. Le correctif est cependant simple : il suffit de le déclarer comme variable locale :

        function run {
    local DIR=`pwd`
}

★★★★★