Initiation aux scripts shell pour automatiser des tâches - Structures de contrôle

Écrit par Jean-Michel CHRISOSTOME. Publié dans Blog

Après avoir survécu à la canicule, nous repartons en quête de l'automatisation de tâches avec notre fidèle ami le Shell. Dans les articles précédents, nous avons vu comment créer un script Shell, puis la création et consultation de variables. Maintenant, afin de pouvoir analyser et réagir avec les données contenues dans ces variables, nous allons avoir besoin de structures de contrôle.

Les structures de contrôle sont le pilier fondamental de la programmation. Ce sont ces structures qui vont nous permettre de prendre une décision si une valeur spécifique a été atteinte. A nous de bien préciser laquelle (lesquelles). Avec les structures de contrôle dont nous allons parler ci-dessous et qui ont déjà été vues dans d'autres langages (voir article), il est possible de traiter la plupart des cas de figures connus en Shell.

Bonnes pratiques

Voici un petit rappel sur les bonnes pratiques à respecter lorsque vous faîtes de la programmation (peu importe le langage).

Bien commenter son code

En Shell, tout ce qui suit un # jusqu'à la fin de la ligne est considéré comme un commentaire. Un commentaire c'est du texte qui n'est pas interprété mais dont la présence doit être utile au lecteur pour bien comprendre le code. Même si vous ne le faîte pas pour les autres, faîtes le au moins pour vous, car relire son propre code 6 mois après peut s'avérer parfois compliqué.

La condition if

La condition if (SI) est une structure de contrôle indispensable à tout langage de programmation. C'est une base pour d'autres structures plus complexe et permet de prendre une décision face à un cas de figure défini.

SI [ $ELEMENT = VALEUR ]
ALORS
    # Instruction à exécuter
SINON
    # Instruction à exécuter
FIN

Pour la suite de cet article, nous aurons besoin de vérifier si un dossier est vide ou pas. Une commande et un if vont nous permettre d'y parvenir simplement.

#!/bin/sh
 
dossier="/tmp/mon_dossier"
# Cette méthode permet d'obtenir le nombre de fichiers présent dans $dossier
# La variable $# contient normalement le nom d'argument passé au script, ici il s'agit du nombre d'éléments retournés
set - ls $dossier
# Si il y a 0 fichier, le dossier est vide
if [ $# = 0 ]
then
    echo "$dossier est vide"
else
    echo "$dossier non vide"
fi
 

La structure case (switch)

Cette structure particulière, permet d'écrire simplement des instructions pour chaque cas de figure à traiter. Attention toutefois, ce n'est qu'une facilité d'écriture, car dans les faits, cette structure se comporte comme un if, else if, else.

Une petite représentation logique pour case

CAS $variable POUR
    Valeur1)
        # Instructions spécifiques si $variable = Valeur1
        # Ne pas oublier de terminer chaque condition par un ;; (break dans les autres langages) sinon les autres conditions seront exécutées aussi.
        ;;
    ValeurX)
        # Instructions spécifiques si $variable = ValeurX uniquement
    ValeurY)
        # Instructions spécifiques si $variable = ValeurX et ValeurY
        ;;
    *)
        # Instructions à exécuter pour tous les autres cas de figure
FIN

Voici un exemple d'utilisation de la structure case en shell. On retrouve très souvent cette structure dans les scripts de lancement et d'arrêt des services dans /etc/init.d sur Linux.

#!/bin/sh
 
# On prend une décision en fonction du premier argument passé au script
case $1 in
    start)
        echo "Démarrage du service"
        ;;
    stop)
        echo "Arrêt du service"
        ;;
    reload)
        reload=1
    restart)
        if [ reload = 1 ]
        then
            echo "Rechargement du fichier de configuration"
        else
            echo "Redémarrage du service"
        fi
        ;;
    *)
        echo "Erreur! Les argument autorisé en paramètres sont : start, stop, reload, restart"
esac
 

La fonction

En shell, la fonction est une structure de contrôle qui se comporte comme l'appel à un script externe. Il faut voir ça comme un script dans un script. Un fonction est une série d'instructions regroupées dans un espace logique que nous pouvons exécuter avec des arguments et qui peut retourner une valeur.

Voici la représentation logique d'une fonction

function nom_fonction {
    # Instruction à exécuter
    return code_retour
}

# Pour appeler la fonction
nom_fonction arg1 arg2
# Afficher le code retour de la fonction
echo $?

Voici une petite fonction en shell qui traite des arguments et retourne une valeur.

#!/bin/sh
 
function multiplie {
    ret=`expr $1 "*" $2`
 
    return $ret
}
 
echo -en "2 multiplié par 3 = "
multiplie 2 3
# Affiche le code retour de la fonction appelée
echo $? # Affichera 6
 
 

La boucle while

Si vous devez traiter un fichier texte par ligne, alors cette boucle sera sans doute votre meilleure amie. En effet, la boucle while (TANT QUE) répète les mêmes instructions tant qu'une condition est vraie. Plutôt que de réécrire les mêmes instructions pour traiter (par exemple) chaque ligne d'un fichier, while vous propose de les réutiliser tant qu'il y a des lignes à traiter.

Voici ce que donne une boucle while logiquement.

TANT QUE condition vraie
ALORS
    # Instructions à exécuter
FIN

Afin d'illustrer l'utilisation de cette boucle, nous allons traiter un fichier texte ligne par ligne en shell.

#!/bin/sh
 
# Le fichier à traiter est passé en argument
$fichier = $1
# On définit le symbole séparateur de champs
IFS=';'
# Numéro de ligne courant
nl=1
 
# On teste si le fichier existe
if [ -f $fichier ]
then
    # Pour chaque ligne du fichier lue, on applique le séparateur de champs ; qui sera remplacé par un espace afin de séparer les champs
    cat $fichier | while read ligne
    do
        # ajout.pl ajoute la ligne avec les champs à une base de données existante
        ajout.pl $ligne
        # Cas d'erreur
        if [ $? = 1 ]
        then
            echo "Erreur de traitement à la ligne $nl du fichier $fichier"
        fi
        # Incrémentation de la ligne en cours
        nl=expr $nl "+" 1
    done
else
    echo "Le fichier $fichier n'existe pas"
    return 1
fi
 

La boucle for

La boucle for va nous permettre comme son nom l'indique, d’exécuter en boucle la même série d'instructions pour tous les éléments d'une liste. Autrement dit, plutôt que de réaliser le même travail pour les 10 éléments d'une liste, nous allons l'écrire une fois et la structure l'appliquera à chacun des éléments.

Voici à quoi ressemble une boucle for d'un point de vue logique

POUR $ELEMENT DANS $LISTE
    FAIRE
    # Instructions à exécuter
FIN

Un petit exemple pour illustrer son utilité. Nous avons des agences situées dans différentes villes en France : Auxerre, Beauvais, Bordeaux, Grenoble, Lyon, Nantes, Paris, Valence qui déposent des fichiers sur notre serveur. Tous les soirs à 19h, un script planifié (par Cron) s’exécute pour traiter les fichiers envoyés par les agences puis les supprime après traitement. On part du principe que le traitement archive les fichiers.

Voici en Shell ce que cela pourrait donner... Bien entendu, il est possible de faire mieux (autrement), mais c'est pour illustrer un exemple d'utilisation.

#!/bin/sh
 
# La fonction liste le dossier passé en argument
# Retourne 0 si le dossier est vide, sinon, retourne la liste des fichiers
function liste_dossier {
    # Le premier argument de la fonction est le dossier à contrôler
    set - ls $1
    # Si il y a 0 fichier, le dossier est vide
    if [ $# = 0 ]
    then
        return 0
    # Si il y a des fichiers, on retourne la liste
    else
        return $*
    fi
}
 
# On définit la liste des agences (attention au respect des majuscules/minuscules dans le nom des dossiers)
agences='Auxerre Beauvais Bordeaux Grenoble Lyon Nantes Paris Valence'
# on définit le dossier racine des agences
racine='/var/www/data/agences'
# Pour chaque agence
for $a in $agences
do
    # Constitution du chemin vers le dossier de l'agence
    dossier=${racine}"/"{$a}
    # Analyse du dossier de l'agence
    $fichiers=liste_dossier $dossier
    if [ $fichiers = 0 ]
    then
        # Un peu d'affichage à titre informatif
        echo "Pas de fichier à traiter pour l'agence $a"
    else
        # Pour chaque fichier de l'agence, on exécute le traitement: traitement.pl (script en perl)
        for $f in $fichiers
        do
            echo "Traitement du fichier $f de l'agence $a"
            # On concatène (assemble) les variables afin de générer le chemin vers le fichier à traiter
            /home/spec/bin/traitement.sh $dossier"/"$f
            # Contrôle d'erreur du traitement
            if [ $? = 0 ]
            then
                echo "Traitement sans erreur pour le fichier $f de l'agence $a"
            else
                echo "Le traitement a rencontré des erreurs dans le fichier $f de l'agence $a"
            fi
        done
    fi
done
 
Astuce : Ne cherchez pas d'information sur les conditions possibles dans man if ou man while, mais dans man test, toutes les conditions applicables aux structures étudiées plus haut (if, while) sont représentées dans cette manpage.

Dans les prochains articles consacrés au shell, nous ferons le tour de quelques commandes utiles pour vos scripts.