le script shell : exécution

Dans l’optique d’automatiser un traitement, il est souhaitable de pouvoir enregistrer et rejouer une suite de commandes Unix : c’est l’essence même du script shell, qui n’est donc qu’un fichier texte comportant des lignes de commandes destinées à être  interprétées séquentiellement par un shell ; typiquement un script s’exécute en le passant en argument au shell censé l’interpréter :

bash mon_script.sh

Il est toutefois possible de rendre autonome le script en ajoutant dans sa toute première ligne (“magic-line“) le “sha-bang” renseignant le nom de l’interpréteur chargé d’exécuter le script, puis de rendre exécutable le fichier (via la commande chmod +x mon_script.sh) :

#!/bin/bash
...

Le “sha-bang“, constitué des 2 premiers caractères de la première ligne (#!)  doit être immédiatement suivi du chemin vers l’interpréteur, est en fait un nombre magique (cf. man magic)  – d’où le terme de magic-line) – permettant à un shell de détecter que les caractères suivants représentent un programme chargé d’interpréter le reste du fichier à lancer.
Cette mécanique est générique et permet de rendre ainsi exécutable tout source d’un langage interprété (perl, python, php, tcl, etc…).

Une fois, ces modifications effectuées, le script devient à sont tour une “commande”, et peut être lancé directement :

./mon_script.sh

Lors de l’interprétation d’un script, le shell, contrairement à d’autres interpréteurs plus modernes, ne procède pas à une première passe (python ou php, par exemple, parcourent le source pour effectuer une validation syntaxique et une pré-compilation en p-code, avant de procéder à l’exécution) : les lignes sont évaluées et exécutées (sauf ruptures) les unes à la suite des autres.
Si une erreur est détectée, le shell va s’interrompre et afficher un message d’erreur en cours d’interprétation ; les commandes précédent l’erreur ayant été exécutées.

a – Evaluation des commandes

Chaque ligne des commandes est évaluée avant son exécution ; cette évaluation est faite dans l’ordre suivant :

  1. Isolation des mots séparés par caractères séparateurs (par défaut : espace, tabulation, saut de ligne).
  2. Traitement des caractères de protection ('' "" \).
  3. Substitution des variables ($ ${}).
  4. Substitution des commandes (`` $()).
  5. Substitution des caractères de génération des noms de fichiers ('* ? [] ?() +() *() !() @()).
  6. Traitement des tubes et des redirections.
  7. Lancement de la commande.

b – Rupture de séquence avec les opérateurs du shell

Les opérateurs lient entre elles différentes commandes, et sont au nombre de deux :

La syntaxe d’utilisation est la suivante :

Exemple d’utilisation :

make bzImage && make modules && make modules_install

Cette séquence (typique de la compilation du noyau Linux) va enchainer les compilations :

– du noyau lui-même (make bzImage).
– des modules, si la compilation du noyau a réussi (make modules).
– et enfin, installer les modules (dans /lib/modules/) si leur compilation a réussi (make modules_install).

 c – structures de contrôle :

Au cours de l’exécution d’un script, il peut s’avérer nécessaire de rompre l’ordre séquentiel du code source, soit en raison d’un test (effectuer une action en réponse à une condition donnée) ou pour exécuter plusieurs fois un bloc de commandes. Ces besoins sont couverts syntaxiquement par des structures de contrôle déterminées par des mots-clés au niveau du code source.

1 – Les branchements : structures if, elif et case

Les tests peuvent donc permettre d’éxecuter conditionnellement un partie d’un programme en fonction de son résultat. La structure pour procéder à ces branchements dans le code est la structure if : si la condition est vraie, alors le programme exécute un bloc de code. Mais elle peut se compliquer quelque peu en rajoutant les notions de sinon (else) et de sinon, si (elif). La syntaxe est la suivante :

if test
then
… bloc de code …
[elif test]
then
… bloc de code …
[else]
… bloc de code …
fi

ou encore

if test ; then
… bloc de code …
[elif test] ; then
… bloc de code …
[else]
… bloc de code …
fi

Exemples d’utilisation :

if [ -a ~/.bashrc ] ; then
echo “J’ai bien un fichier de configuration personnalisé pour le shell bash !”
echo “En voici son contenu :”
cat ~/.bashrc
fi
if [ -z $a ] ; then
echo “La variable a est indéfinie !”
exit 1
fi
if (( $a == 10)) ; then
echo “La variable a vaut $a !”
elif [ $a -eq 20 ] ; then
echo “La variable a vaut $a !”
elif test $a -lt 20 ; then
echo “La valeur de la variable a est inférieure à 20 …”
echo “La valeur de la variable a est différente de 10 …”
else
echo “La valeur de la variable a est supérieure à 20 …”
fi

Pour les cas plus complexes, il sera possible de remplacer l’utilisation de nombreuses conditions if – elif qui deviennent rapidement illisibles par la structure case. Sa syntaxe est la suivante :

case $variable in
modele1)
… bloc de code …
;;
modele2)
… bloc de code …
;;
*)
… bloc de code …
;;
esac

Exemple d’utilisation :

case “$1” in
start)
echo “Démarrage du daemon …”
;;
stop)
echo “Arrêt du daemon …”
;;
*)
echo “Usage : $SCRIPTNAME {start|stop}”
exit 3
;;
esac

 2 – Les boucles : structures for, while et until

Les boucles sont des structures de contrôle permettant d’itérer un bloc de code autant de fois que nécessaire. L’itération peut être déterminée soit par le nombre de valeurs d’une liste dans le cas de la boucle for, soit par le résultat d’un commande ou d’un test dans le cas des boucles while et until :

La syntaxe d’une boucle for est la suivante :

for var [in liste]
do
… bloc de code …
done
ou
for var [in liste] ; do
… bloc de code …
done

La syntaxe d’une boucle while est la suivante :

while expression
do
… bloc de code …
done
ou
while expression ; do
… bloc de code …
done

La syntaxe d’une boucle until est la suivante :

until expression
do
… bloc de code …
done
ou
until expression ; do
… bloc de code …
done

Exemples d’utilisation :

for i in *; do ls -i; done;
for i in $(cat /etc/passwd | cut -d: -f1); do echo $i; done;
for i in `seq 1 10` ; do echo $i; done
for i in 1 2 3 4 5 6 7 8 9 10 ; do echo $i; done
while true ; do echo “Saisissez un chiffre”; read CHIFFRE; echo “$CHIFFRE erroné. essayez encore…”; done;
REP=0; while (( $REP < 10 )); do echo $REP; REP=`expr $REP + 1`; done;
until false ; do echo “Saisissez un chiffre”; read CHIFFRE; echo “$CHIFFRE erroné. essayez encore…”; done;
REP=0; until [ $REP -eq 10 ] ; do echo $REP; ((REP+=1)); done;

Lors de l’exécution d’une boucle, les commandes internes continue et break permettent de modifier le flux de commandes en, respectivement, passant à l’itération suivante et en interrompant le cours de la boucle.

Exemples d’utilisation :

for i in `seq 1 10`
do
	if (( $i == 5 ))
	then
		continue	# n'affichera pas 5
	fi
	if (( $i > 8 ))	# interromp la boucle pour i=9
	then
		break
	fi
	echo $i
done

clear
while true ; do         # boucle infinie 
        echo "Saisissez un nombre (0 non traité - 999 pour terminer)";
        read NOMBRE;
        if [  ! $(echo $NOMBRE | grep "^[ [:digit:] ]*$") ] ; then
                echo "On vous a demandé un nombre !"
                continue
        fi
        if [ $NOMBRE -eq 0 ] ; then
                continue
        elif [ $NOMBRE -eq 999 ] ; then
                break
        else
                echo "Vous avez saisi le nombre $NOMBRE (999 pour terminer)"; 
                sleep 5
                clear
        fi
done;

d – Les fonctions

Dernière forme de structuration du code, le shell prévoit la possibilité de création de fonctions qui permettent de factoriser des portions de code destinées à être exécutées plusieurs fois dans un script, ou bien encore de créer des librairies de code réutilisables dans différents scripts. Une fonction doit être définie avant son utilisation (son appel) ; la syntaxe pour la définition d’une fonction est la suivante :

nomdelafonction() {
… bloc de code …
}
Une seconde forme est également possible (uniquement avec ksh ou bash)
function nomdelafonction {
… bloc de code …
}

Quant à la syntaxe d’appel (utilisation) d’une fonction est tout simplement :

nomdelafonction [parametre1 parametre2 …]

… à la condition que la fonction appelée ait été définie avant son appel, sous peine d’affichage d’une message d’erreur lors de l’exécution du script.

Une function, comme obéit aux mêmes principes que toute commande Unix : elle retourne un code de retour (un entier compris entre 0 et 255) qui vaut par défaut 0, mais cette valeur peut être modifiée en utilisant la commande interne return [valeur]. Ce code de retour est transmis à la variable spéciale $? dès le retour de la fonction, et peut donc être testé comme de coutume.

Les variables utilisateur sont globales pour l’ensemble d’un script, c’est à dire qu’une variable déclarée dans le script sera accessible et modifiable dans le corps d’une fonction, et qu’à l’identique une variable déclarée ou affectée dans le corps d’une fonction sera accessible au reste du script. Or, dans nombre de cas, les variables de travail d’une fonction n’ont à être accessibles ni publiées à l’extérieur de celle-ci ; la commande interne typeset permet de déclarer des variables locales dans une fonction : elle sont alors ni accessibles ni publiées en dehors du corps de la fonction.

Quant aux paramètres passés à une fonction, ils sont accessibles à celle-ci via le jeu de variables spéciales locales $1, $2, …, $*, $@, $#.

La variable spéciale $0 reste quant à elle accessible, en lecture-seule, et contient toujours le nom du script en cours d’exécution.

Exemples d’utilisation :

#!/bin/bash

function saisie_nombre {
        echo -n "$0> Saisissez un nombre (999 pour terminer) : "
        read NOMBRE;
         if [  ! $(echo $NOMBRE | grep "^[ [:digit:] ]*$") ] ; then
                NOMBRE=""
                return 1
        fi
}

clear
while true ; do         # boucle infinie
        saisie_nombre
        RES=$?
        if [ $RES -ne 0 ] ; then
                echo "On vous a demandé un nombre !"
                continue
        fi
        if [ $NOMBRE -eq 0 ] ; then
                continue
        elif [ $NOMBRE -eq 999 ] ; then
                break
        else
                echo "Vous avez saisi le nombre $NOMBRE (999 pour terminer)";
                sleep 5
                clear
        fi
done;

 suivant …