Préparation
Un shell-script (ou script shell) est un fichier texte simple. Il peut donc être écrit avec votre éditeur de texte préféré. La toute première ligne du fichier doit contenir le nom de l'interpréteur à utiliser (cela est valable pour tous les langages de script). Cette ligne doit commencer par #! suivi de l'exécutable à utiliser. Pour un shell-script bash, on aura donc en début de fichier :
#!/bin/bash
Ce fichier doit également être exécutable. Pour cela on lui attribuera ces droits à l'aide de chmod. Si le fichier s'appelle mon_programme, on peut le faire de cette manière :
> chmod +x mon_programme
Pour plus de détails sur les options de chmod, vous pouvez lire les explications sur la gestion des permissions ou consulter sa page de manuel.
A présent, on peut dans ce fichier mettre des commandes shell ou appeler des programmes comme on le ferait dans une ligne de commande. Ensuite il sera invoqué en tapant son nom comme pour n'importe quel autre exécutable.
On peut insérer des commentaires dans un script. Ce sont des lignes qui seront ignorées par le bash. A partir d'un # tout le reste de la ligne est considéré comme un commentaire
#!/bin/bash
#Ceci est un commentaire
echo ceci est affiché #Un autre commentaire
Un script se termine lorsque la dernière ligne a été exécutée. On peut toutefois forcer sa sortie avant en utilisant la commande exit. Le code de retour du script sera celui de la dernière commande exécutée. Cela à moins de préciser une valeur numérique à la suite de exit. C'est alors celle-ci qui sera renvoyée.
Des exemples de shell-scripts se trouvent dans la boîte à outils et peuvent être consultés pour compléter ce cours.
Les variables spéciales
Lorsqu'un shell-script est exécuté, bash initialise certaines variables particulières qui sont alors accessibles en lecture seule. C'est à dire qu'elles ne peuvent pas être directement modifiées par une affectation.
La première pré-définie est $$ qui contient le PID du script. Ce Process ID, ou en français l'identifiant de processus, est un nombre qui à un moment donné identifie de manière unique une instance de programme en cours d'exécution sur le système. Cette valeur peut servir par exemple à générer un nom de fichier temporaire ou pour envoyer un signal.
Ensuite on trouve $? dont la valeur va changer au fil de l'exécution du script. Il contiendra le code de retour de la dernière commande exécutée. A noter qu'un code de retour de 0 signifie qu'il n'y a eu aucune erreur. Si cette valeur est différente de 0, il s'agit d'un code d'erreur.
Il y a aussi les paramètres passés lors de l'appel au script. La variable $* contient l'ensemble de toutes ces valeurs et $# permet de connaître leur nombre. Pour accéder à un paramètre en particulier, on utilisera une des variables positionnelles $1, $2, $3, ... Pour celles après 9, il faut encadrer le nombre par des accolades : ${10}, ${11}, ${12}, ...
La variable $0 contient le nom du script tel qu'il a été lancé, c'est-a-dire avec les spécifications de chemin éventuelles.
Réaliser des tests
Pour réaliser un test sur des variables ou des fichiers, on utilisera comme instruction la commande intégrée test qui permet d'opérer des comparaisons. Voici des exemples des opérations les plus courantes. Pour le moyen de réaliser d'autres tests (sur le type du fichier par exemple), consultez la page de manuel du programme test ou celle de bash pour la commande intégrée. Est indiqué en commentaire dans quelles conditions le test est vérifié, c'est-à-dire quand test renverra 0.
test -e /tmp/mon_fichier # /tmp/mon_fichier existe
test "$VAR" = "valeur" # La variable VAR contient la chaîne valeur
test "$VAR1" != "$VAR2" # Les 2 variables ont des contenus différents
test "$VAR" -lt "3" # VAR contient un numérique inférieur à 3
test -n "$VAR" # VAR contient une chaîne de longueur non nulle
test -z "$VAR" # VAR contient une chaîne de longueur nulle
Si on utilise des variables pour les expressions de test, il vaut mieux les mettre entre guillemets. Car dans le cas où cette variable ne serait pas initialisée, un argument serait manquant. Alors que de cette manière, il y a une chaîne vide dans le pire des cas. Ceci est particulièrement utile avec les options -n et -z.
Si on essaye de faire une comparaison arithmétique avec une des valeurs qui ne serait pas un nombre, on obtient une erreur. La liste de ces opérations est : -eq (=), -ne (!=), -lt (<), -le (<=), -gt (>), -ge (>=). Entre parenthèses étant indiqués leurs équivalents mathématiques.
On peut demander à inverser le résultat du test en utilisant l'opérateur de négation ! (point d'exclamation). On peut aussi utiliser -a et -opour respectivement avoir un ET et un OU logique entre plusieurs expressions. Voici un exemple :
test "$VAR" -lt 3 -o "$VAR" -ge 8
Celui-ci sera vérifié si VAR contient un numérique inférieur à 3 ou bien supérieur ou égal à 8.
Une version abrégée de test existe. Il suffit de remplacer son nom par [ et de mettre un ] à la fin de l'expression. Cela permet de mieux faire ressortir les tests. Sur d'anciens systèmes, [ était un lien symbolique vers l'exécutable test. A présent, les deux sont différents et sont intégrés au shell.
Les structures de contrôle
Dans les scripts, il est utile de faire des tests pour n'exécuter certaines parties que quand les conditions requises sont présentes. Cela se fait à l'aide de if. Sa syntaxe est la suivante :
if instructions
then
#1er bloc d'instructions
else
#2nd bloc d'instructions
fi
Juste après le if, on trouve un nombre quelconque d'instructions (pouvant être sur plusieurs lignes). Elles seront toutes exécutées jusqu'à ce qu'un then soit trouvé. Ensuite ce qui importera est le code de retour de la dernière instruction. Si la valeur est 0 (ce qui correpond à aucune erreur pour bash) le premier bloc d'instructions est alors exécuté. Dans le cas contraire, ce sera le second. Le else et le second bloc d'instructions sont facultatifs. Si le résultat est faux et que l'on n'a pas cette deuxième partie, rien ne sera fait. Le fi devra en tous les cas être présent pour indiquer la fin du bloc de test.
Dans ce genre de situation, la commande test sera très utile. Par exemple pour tester que la variable VAR contient la chaîne VALEUR (en version abrégée) :
if [ "$VAR" = "VALEUR" ]
then
echo VAR contient bien VALEUR
else
echo VAR ne contient pas VALEUR
fi
Pour comparer une variable à plusieurs valeurs, on peut utiliser des if imbriqués (else if peut alors être abrégé en elif). Mais il y a un moyen plus simple en utilisant case in. Un exemple permettra de mieux comprendre son utilisation :
case $variable in
valeur1)
#1er bloc d'instructions
;;
valeur2)
#2ème bloc d'instructions
;;
valeur3)
#3ème bloc d'instructions
;;
*)
#Traitement des autres valeurs
;;
esac
Entre case et in se trouve la chaîne à comparer. Ensuite on a un nombre quelconque de motifs qui sont terminés par une parenthèse fermante. Le bloc d'instructions qui suit indique ce qu'il faut faire si la chaîne correspond à ce modèle. La fin de celui-ci est indiqué par ;;(double point virgule).
On peut aussi indiquer un bloc d'instructions devant être exécutée si aucune correspondance n'est trouvée. Il se note par * (étoile). Il est facultatif. S'il n'est pas présent et que la chaîne à tester ne correspond à aucune des valeurs, aucun des traitements n'est effectué. Si une valeur correspond, les instructions sont exécutées et le déroulement du script se poursuit après esac qui indique la fin de ce bloc de tests.
On a parfois besoin de faire un même traitement plusieurs fois de suite. Pour cela bash met à disposition plusieurs types de boucles.while et until permettent cela de manières quasiment similaires.
while instructions
do
# Traitement à répéter
done
until instructions
do
# Traitement à répéter
done
Pour le while, le traitement est répété tant que la dernière des instructions précédant le mot do se déroule bien (renvoit 0).
Pour le until, le traitement est répété jusqu'à ce que la dernière des instructions précédant le mot do se déroule bien (renvoit 0).
La fin de ces instructions est indiquée par done.
Il existe un autre type de boucle différent qui s'utilise avec for. Contrairement aux précédentes, le nombre d'itérations est connu à l'avance. Sa syntaxe générale est de la forme :
for variable in liste_de_valeurs
do
# Traitement à répéter
done
La liste suivant le in est constituée de plusieurs valeurs séparées par des espaces. Le traitement sera alors répété et on aura accès à $variable qui prendra successivement ces valeurs dans l'ordre indiqué. Dès que toutes les valeurs auront été parcourues, la boucle s'arrêtera.
Voici un petit exemple qui va afficher à l'écran les nombres de 1 à 10 :
for i in 1 2 3 4 5 6 7 8 9 10
do
echo $i
done
On peut bien sûr mettre comme liste de valeurs le résultat d'un programme en utilisant les guillemets inversés. D'une manière générale, tout ce qui est une chaîne de caractères peut se trouver là. Si l'on a besoin qu'une des valeurs contienne des espaces, on l'encadrera de guillemets. Cette liste est soumise aux expansions réalisées par le shell. Voici donc un moyen de répéter un traitement (plus utile que celui-là dans la pratique) sur tous les fichiers d'extension .txt du répertoire courant.
for fichier in *.txt
do
echo $fichier
done
Si on ne spécifie pas de liste de valeurs (en omettant le in), la boucle travaillera sur les variables positionnelles ($1, $2, ...)
Manipulation des paramètres
Un shell-script un peu perfectionné pourra accepter de nombreux paramètres et options. Ceux-ci pourront éventuellement être facultatifs et l'ordre ne doit pas importer. Faire cela avec ce qui a été vu jusque-là serait fastidieux. Heureusement des facilités sont fournies.
En tout premier, shift permet de décaler les variables positionnelles. Son appel sans paramètre va affecter à $1 la valeur de $2, à $2 la valeur de $3 et ainsi de suite. Si on lui passe une valeur numérique, le décalage se fera de cette valeur plutôt que d'un rang.
bash fournit la commande integrée getopts qui permet de facilement parcourir tous les paramètres. Voici un exemple classique d'utilisation qui sera expliqué ensuite :
while getopts a:bc:d option
do
case $option in
a)
echo "option a : $OPTARG"
;;
b)
echo "option b"
;;
c)
echo "option c : $OPTARG"
;;
d)
echo "option d"
;;
esac
done
getopts va successivement mettre dans la variable passée en paramètre (ici option) les différents paramètres lors de l'appel au script. Il les vérifie en même temps et retourne une erreur si un de ceux-ci ne correspond pas. Pour connaître les valeurs autorisées, on lui spécifie les options possibles. Ici on avait la chaîne a:bc:d qui jouaient ce rôle. Elle indique que les options a, b, c et d sont autorisées uniquement. Une lettre suivie de : (deux points) indique qu'un paramètre est nécessaire pour cette option. getopts le mettra alors dans la variable d'environnement $OPTARG pour permettre d'accéder à sa valeur.
En supposant que le code précédent ait été inséré dans un shell-script s'appelant mon_script, voici des exemples d'exécution qui produiront tous le même résultat. Pour ce script, l'ordre d'affichage sera bien sûr différent. Mais un véritable programme sauvegardera dans des variables d'environnement les options pour les utiliser par la suite.
> mon_script -a paramètre -bd
> mon_script -b -aparamètre -d
> mon_script -dba paramètre
Ce que l'on remarquera principalement, c'est que les options peuvent ou non être regroupées, -bd étant identique à -b -d. Et également le paramètre passé à l'option -a peut ou non être séparé par un espace.
Gestion des signaux
Les signaux constituent un moyen de communication entre plusieurs programmes. Ceci est plutôt utilisé dans des programmes en C, mais peut très bien être utilisé dans un script.
Pour obtenir la liste des signaux disponibles, tapez la commande :
> kill -l
La commande kill (qui est intégrée au bash) ne vous est pas inconnue si vous utilisez GNU/Linux depuis quelques temps. Elle permet d'envoyer un signal à un programme. Or certains signaux vont provoquer l'arrêt du programme. C'est le cas du signal SIGTERM (numéro 15) notamment qui est celui envoyé par défaut par kill. Dans le cas d'un programme récalcitrant (qui ne répond plus ou a choisi d'ignorer ce signal), il peut être stoppé, sans qu'il ne puisse rien y faire, par l'envoi d'un signal SIGKILL (9). La commande kill prend comme argument le numéro du signal précédé du signe -, puis l'identifiant du programme (PID).
Dans un shell-script, on peut définir comment il doit réagir aux différents signaux qu'il recevra. Cela se fait à l'aide de la commande trapqui définit quoi exécuter pour chaque signal. On l'utilise comme ceci :
trap commande liste_signal
liste_signal contient la liste des signaux concernés par cette commande. Ils peuvent être indiqués par leurs noms ou par leurs numéros. En n'indiquant aucune commande, cela permet de rétablir le gestionnaire de signal à sa valeur par défaut. Bien évidemment, le signal SIGKILL ne peut pas être intercepté à l'aide de cette commande.
Si le signal a pour valeur 0, la commande associée sera appelée lorsque le shell sera terminé. Il s'agit de l'utilisation la plus courante de trap dans un shell-script. On définit une fonction de nettoyage, et on l'associe au signal 0 (EXIT). Cela permet que même si le script est interrompu, on remette les choses en place avant de quitter. Voici un tel exemple :
function nettoyage()
{
# Tout nettoyer
# Par exemple supprimer les fichiers temporaires
rm -f /tmp/mon_fichier
}
trap "nettoyage" 0
Regrouper des commandes
Pour exécuter une suite de commandes sur une seule ligne, on peut les placer les unes à la suite des autre en les séparant par des points-virgules. Dans un script, cela n'a que peu d'intérêt. Mais il est possible de les entourer par des parenthèses ( ) ou des accolades { }. Alors cela sera considéré comme un sous-script et l'expression totale vaudra le code de retour de la dernière commande. Voici des exemples des deux notations :
( echo "liste des fichiers"; ls; )
{ echo "liste des fichiers"; ls; }
Il est important de respecter l'espace entre les commandes et les délimiteurs. Il y a une différence entre ces deux notations. Celle avec des parenthèses, va exécuter les commandes dans un sous-shell. Celle avec les accolades va le faire dans le shell courant. Cela peut provoquer un comportement différent si l'on utilise à l'intérieur des variables extérieures qui n'ont pas été exportées.
Si l'on souhaite utiliser plusieurs fois un groupe de commandes, on peut les regrouper dans une fonction. Comme déjà abordé dansl'utilisation du bash, une fonction se déclare sous la forme :
function ma_fonction()
{
#Liste des commandes
}
Dans un script, on peut mettre les différentes commandes sur plusieurs lignes pour augmenter la lisibilité. On peut ne pas mettre le mot-clé function ou les parenthèses. Mais au moins un des deux est obligatoire.
Les variables passées en paramètre lors de l'appel à la fonction sont accessibles par les variables positionnelles. Cela se passe de la même manière que pour accéder aux variables du script avec $1, $2, ... mais aussi $* et $#. Par conséquent si une fonction à l'intérieur d'un script doit accéder à un des paramètres du script, il faut lui passer explicitement lors de l'appel car elle ne les voit pas directement.
Une fonction est terminée lorsqu'il y a une accolade fermante. Mais parfois on souhaite en sortir avant, à la suite d'un test par exemple. Pour cela on peut utiliser la commande return. On peut lui passer une valeur. Ce sera le code de retour de la fonction qui peut être utilisé directement pour un test ou dans une boucle, ou alors par la variable $?. Comme pour exit, le code de retour sera en l'absence de cette valeur celui de la dernière commande exécutée.
function une_fonction()
{
echo paramètre : $1
return 5
}
une_fonction test
echo retour : $?
On aura alors, si le script s'appelle essai_fonction :
> essai_fonction
paramètre : test
retour : 5
0 Commentaire:
Enregistrer un commentaire