Consignes
Le rendu du TP consistera en une archive (fichier .zip, .tar, etc.) comportant votre travail.
L'archive contiendra :
-
un fichier texte README contenant les réponses aux questions, vos remarques et commentaires
-
des fichiers ".sh" contenant les scripts demandés.
Liens utiles
-
email des enseignants (Chambéry) gerald.cavallini@univ-smb.fr, pierre.hyvernat@univ-smb.fr, damien.decout@univ-smb.fr, bernard.caron@univ-smb.fr, peio.borthelle@univ-smb.fr, yoann.barszezak@univ-smb.fr
-
email des enseignants (Annecy) Sebastien.Monnet@univ-smb.fr, Leopold.Crestel@univ-smb.fr
Objectifs du TP
L'objectif de ce TP est d'introduire la programmation shell qui permet d'automatiser de nombreuses tâches. Ce TP sert d'introduction au suivant qui prendra la forme d'un "mini projet" où vous devrez appliquer les notions apprises pour l'écriture d'un ou deux scripts complets.
Préliminaires : script
Le shell (ou invite de commandes) permet d'interagir avec le système d'exploitation. Comme vous l'avez vu pendant le TP2, le shell agit comme un interpréteur : vous entrez une commande, le shell l'évalue, affiche le résultat et recommence. On parle de "read-eval-print loop" (REPL). (C'est le même principe que l'interpréteur Python que vous utilisez dans vos cours de programmation.)
Comme Python, le shell peut également lire des commandes dans un fichier texte, en général avec l'extension ".sh". Plutôt que de programme, on parle plutôt de script dans ce cas. Par exemple, GameShell (que vous avez utiliser au TP2) est essentiellement constitué d'un ensemble de scripts shell.
Dans ce TP, vous allez découvrir :
-
les instructions de programmation du shell :
-
variables et affectations
-
fonctions
-
conditionnelles (
if ... then ... else ...
) -
boucles (
for ...
ouwhile ...
)
-
-
les scripts, c'est à dire l'utilisation de fichier pour conserver vos programmes shell.
Bien sûr, tous les concepts vus au TP2 comme les redirections restent valides dans un script.
-
Lancez un terminal, taper les commandes suivantes et observez le résultat
$ pwd --> que se passe t'il ??? $ cd --> que se passe t'il ??? $ ls --> que se passe t'il ??? $ cd /tmp --> que se passe t'il ??? $ pwd --> que se passe t'il ???
-
En utilisant un éditeur de texte, par exemple geany ou gedit, écrivez les même commandes dans un fichier appelé script1.sh dans votre répertoire de travail du TP :
pwd cd ls cd /tmp pwd
-
Vérifiez que le fichier apparait avec la commande
ls
$ ls README script1.sh ...
(Si ce n'est pas le cas, utilisez cd pour vous déplacer dans le répertoire approprié.)
La commande
$ source script1.sh
doit interpréter les lignes de votre fichier dans le shell courant.
Lancez cette commande et comparer le résultat avec ce que vous avez observé au point 1.
-
Les commentaires s'écrivent comme en Python : en les précédant d'un caractère #. Ajoutez un commentaire avec votre nom au début du fichier script1.sh et vérifiez qu'il produit le même résultat qu'auparavant.
Notez vos observations dans votre fichier README.
-
Si vous utilisez un autre shell que bash, il est possible que la commande
source
ne soit pas reconnue. Vous pouvez la remplacer par un point suivi d'un espace :$ . script1.sh
-
Ceci est pratique si vous avez besoin d'exécuter fréquemment une suite de commandes un peu longue : il suffit de les mettre dans un fichier. Bien entendu, si le fichier n'est pas dans le répertoire courant, il faudra donner le chemin complet après
source
.Si la suite de commande est courte, il est également possible de les mettre sur une seule ligne en les séparant par des points virgules, dans le script, ou directement dans le shell :
$ pwd; cd; ls; cd /tmp; pwd
1. Variables et affectation
Il est difficile d'écrire un programme ou un script sans stocker des valeurs dans des variables. Les variables du shell servent essentiellement à stocker des chaines de caractères ou des nombres entiers.
-
L'affectation d'une valeur pour une variable se fait avec le signe
=
:prenom="Pierre" nom="Hyvernat"
ATTENTION, il ne faut pas mettre d'espace autour du signe
=
. -
L'utilisation de la valeur d'une variable se fait en précédant le nom de la variable par le signe
$
. Par exemple,prenom="Pierre" nom="Hyvernat" echo "$prenom"
affichera simplement Pierre. (La commande
echo
est un peu l'équivalent de la commandeprint
de Python.)ATTENTION, si vous oubliez le symbole
$
, vous utiliserez le nom de la variable au lieu de sa valeur.
N'oubliez pas les guillemets lors d'une affectation. Ils ne sont pas toujours obligatoire, mais leur absence peut entrainer des erreurs difficiles à repérer.
-
Essayez de deviner ce qui sera affiché par le script suivant:
prenom="Pierre" nom="Hyvernat" echo "Bienvenu $prenom" echo "Ton $nom est nom"
Testez le script avec
$ source script2.sh
Notez attentivement ce qui se passe, et corrigez les erreurs présentes.
-
Remplacez les guillemets double """ par des guillemets simples "'" et relancez votre script. Comment interprêtez vous le résultat ?
Commentez et analisez les résultats des commandes suivantes dans votre fichier README :
$ nom="Hyvernat"
--> que passe t'il
$ nom
--> que passe t'il
$ $nom
--> que passe t'il
$ echo "$nom"
--> que passe t'il
-
Certaines variables sont définies automatiquement par le shell. Affichez les et cherchez à quoi elles correspondent :
-
HOME
-
PWD
-
OLDPWD
-
SHELL
-
USER
-
-
Testez l'effet de l'affectation suivante dans votre shell :
$ PS1="" --> que se passe t'il
Testez ensuite
$ PS1="><> "
Expliquez ce qui se passe dans votre fichier README.
(Vous pouvez utiliser votre moteur de recherche favori pour avoir des détails sur cette variable.)
arithmétique
-
Testez le script suivant et analysez le résultat
p=97 q=73 echo "Le produit $p * $q est égal à $p*$q."
-
Par défaut, les variables numériques sont en fait considérées comme des chaines de caractères. Pour effectuer des opérations arithmétiques (+, -, *, /, %), il faut mettre l'expression voulue entre
$((
et))
.Modifiez le script pour qu'il affiche effectivement
$ source script3.sh Le produit 97 * 73 est égal à 7081.
2. La boucle for
La boucle for du shell ressemble fortement à celle de Python. Sa syntaxe est
for VARIABLE in LISTE
do
...
done
Les points suivants sont importants :
-
L'indentation n'est pas significative (mais fortement conseillée), et le corps de la boucle se trouve entre les 2 lignes
do
et
done
-
VARIABLE
est un nom de variable. Si cette variable existait déjà, sa valeur sera écrasée. -
LISTE
est la liste des valeurs que l'on veut donner à la variable. Il s'agit d'une liste de chaines de caractères séparées par des espaces.
-
Lancez le script suivant et expliquez pourquoi il n'y a pas de saut de ligne entre un et mec.
for N in "C'est" "l'histoire" "d'un mec" do echo "> $N" done
-
Remplacez la ligne
for N in "C'est" "l'histoire" "d'un mec"
parfor N in *
. Lancez le script correspondant et expliquez son résultat. -
Modifiez le script pour qu'il affiche une liste numérotée des fichiers présents dans le répertoire courant.
Voila par exemple ce qu'il pourrait afficher :
$ source script4.sh 1> README 2> script1.sh 3> script2.sh 4> script3.sh 5> script4.sh 6> tmp
3. Conditionnelles
Comme tous les langages de programmation, le shell a une instruction
if
. Sa syntaxe est
if COMMANDE
then
...
elif
then
...
elif
then
...
else
...
fi
ATTENTION à n'oublier ni le then
ni le
fi
. Les elif
et le else
ne sont par
contre pas obligatoire.
COMMANDE
est une commande du shell dont la valeur de retour sera
interprétée comme "vrai" ou "faux".
-
Le script suivant contient une conditionnelle toute simple :
if CONDITION then echo "C'est VRAI." else echo "C'est FAUX." fi
Remplacez
CONDITION
par-
0
puis1
-
false
puistrue
et décrivez attentivement ce qui se passe.
-
-
La valeur de retour d'une commande shell est stockée dans une variable spéciale
?
. Sa valeur s'obtient donc avec$?
.Affichez les valeurs de retour des commandes
true
etfalse
.-
Qu'est-ce qu'une valeur vraie pour le shell ?
-
Qu'est-ce qu'une valeur fausse pour le shell ?
Décrivez ce que vous avez fait et les résultats dans votre fichier README.
-
-
Regardez les valeurs de retours des commandes suivantes :
$ ls ----> quelle valeur de retour ??? $ ls /tuhsoeat ----> quelle valeur de retour ???
À quoi peut servir la valeur de retour ?
-
À votre avis, qu'affichera le script suivant ?
if echo "false" then echo "vrai" else echo "faux" fi
Testez et expliquez ce que vous observez.
La plupart des tests "intéressants" se font avec la commande
test
du shell. Cette commande fait partie de la norme POSIX et
est donc présente sur la plupart des systèmes.
Par exemple, pour tester que $i
est strictement plus petit que
10, on peut utiliser la commande
$ test "$i" -lt 10
("lt
" : "less than").
-
Décrivez une manière de vérifier que
test "$i" -lt 10
donne bien une valeur de retour "vraie" lorsque que la variablei
est strictement plus petite que 10.Attention, si vous oubliez les guillemets et que la variable
i
n'existe pas (ou vaut la chaine vide), la commandetest
provoquera une erreur ! -
La page de manuel de la commande
test
est disponible avec$ man test
Cherchez le test permettant de vérifier si une chaine de caractère est vide, et ajoutez un test dans le script suivant
prenom="" nom="Hyvernat" echo "Bienvenu $prenom" echo "Ton nom est $nom"
pour que le premier affichage ne soit fait que lorsque la variable
prenom
est non vide. -
La commande
test
a un synonyme plus court, et souvent plus lisible :test UNE CONDITION
est équivalent à[ UNE CONDITION ]
.-
Vérifiez que la version courte fonctionne avec un test simple.
-
La commande
test
se trouve dans le répertoire /usr/bin. Vérifiez que ce répertoire contient également un fichier [.
-
ATTENTION, si vous utilisez la version courte de test
avec les crochets, il ne faut pas oublier de mettre un espace après le
crochet ouvrant "[
" et avant le crochet fermant
"]
".
Les conditions complexes sont possibles en utilisant les options
-a
("and") et -o
("or") de la commande
test
. Elles sont également possibles en utilisant les opérations
&&
et ||
du shell :
-
COMMANDE1 && COMMANDE2
exécute la première commande, puis exécute la seconde seulement si la valeur de retour de la première est "vraie". La valeur de retour finale est celle de la dernière commande exécutée et correspond donc au "et" booléen des 2 valeurs de retour. -
COMMANDE1 || COMMANDE2
exécute la première commande, puis exécute la seconde seulement si la valeur de retour de la première est "fausse". La valeur de retour finale est celle de la dernière commande exécutée et correspond donc au "ou" booléen des 2 valeurs de retour.
Attention à ne pas confondre ces opérateurs avec &
(pour lancer une commande en tache de fond) et |
pour rediriger
l'affichage d'une commande dans une autre.
Quelle différence y a t'il entre
-
test \( 10 -lt "$i" \) -a \( "$i" -lt 20 \)
-
test 10 -lt "$i" && test "$i" -lt 20
Écrivez un script fichiers.sh qui parcourt le répertoire courant et
-
lorsqu'il trouve un répertoire, affiche REP: suivi du nom du répertoire,
-
lorsqu'il trouve un fichier non vide (de taille non nulle), affiche FICHIER: suivi du nom du fichier,
-
lorsqu'il trouve un fichier vide, affiche VIDE: suivi du nom du fichier,
-
dans tous les autres cas, affiche ???: suivi du nom.
Voici un exemple d'exécution
$ source fichiers-correction.sh
VIDE: fichier_vide.txt
FICHIER: fichiers-correction.sh
???: lien_casse
FICHIER: script1.sh
FICHIER: script2.sh
FICHIER: script3.sh
FICHIER: script4.sh
FICHIER: script5.sh
FICHIER: script6.sh
REP: tmp
Vous pouvez obtenir un répertoire, un fichier vide et un lien symbolique cassé avec
$ mkdir tmp
$ touch fichier_vide
$ ln -s /abcde lien_casse
4. Un plus gros exemple (Bonus)
Écrivez un script dichotomie.sh pour faire une recherche dichotomique similaire à ce que vous avez fait au TP1 d'info201 en Python.
Les détails seront donnés plus bas, et voici un exemple d'exécution
$ source dichotomie.sh
Pense à un nombre entre 0 et 32 puis appuie sur la touche 'Entrée'.
--> je choisis 23
Est-ce que ton nombre est inférieur ou égal à 16 ? [on]
n
Est-ce que ton nombre est inférieur ou égal à 24 ? [on]
o
Est-ce que ton nombre est inférieur ou égal à 20 ? [on]
n
Est-ce que ton nombre est inférieur ou égal à 22 ? [on]
n
Est-ce que ton nombre est inférieur ou égal à 23 ? [on]
o
Ton nombre est 23 !
Voici quelques consignes / détails :
-
La syntaxe des boucles
while
estwhile CONDITION do ... ... done
-
Vous pouvez lire une ligne au clavier avec la commande
read VARIABLE
, comme dansecho "Quelle est ta réponse ?" # message pour l'utilisateur read reponse # on lit une ligne au clavier et on la # stocke dans la variable `reponse`
L'utilisateur doit appuyer sur la touche "Entrée" pour valider, mais le caractère "saut de ligne" (
\n
) n'est pas stocké dans la variable. -
Le principe est le suivant : tant que
a
est strictement plus petit queb
, on prend la moyennem
dea
etb
.-
Si le nombre pensé est inférieur ou égal à
m
, on remplaceb
parm
. -
Sinon, on remplace
a
parm+1
.
Lorsque
a
etb
deviennent égaux, c'est qu'on a trouvé le nombre pensé. -
-
Votre script ressemblera donc à
a=0 # plus petit nombre autorisé b=32 # plus grand nombre autorisé echo "Pense à un nombre entre $a et $b puis appuie sur la touche 'Entrée'." while [ "$a" -lt "$b" ] do ... ... ... done echo "Ton nombre est $a !"
-
Le barème prendra en considération les points suivants :
-
Il faut appuyer sur "Entrée" pour que la boucle commence. Autrement dit, le script doit s'arrêter après le premier message.
-
Dans la boucle principale, si l'utilisateur appuie sur autre chose que o (oui) ou n (non), le script devra reposer la question.
-
Dans le cas précédent, le script devra afficher un message d'erreur.
Voici une autre exécution illustrant les 2 derniers points :
$ source dichotomie.sh Pense à un nombre entre 0 et 32 puis appuie sur la touche 'Entrée'. --> je choisis 19 Est-ce que ton nombre est inférieur ou égal à 16 ? [on] n Est-ce que ton nombre est inférieur ou égal à 24 ? [on] y --> "y" au lieu de "o" Les seules réponses autorisées sont 'o' (oui) et 'n' (non). --> message d'erreur Est-ce que ton nombre est inférieur ou égal à 24 ? [on] --> vide" Les seules réponses autorisées sont 'o' (oui) et 'n' (non). --> message d'erreur Est-ce que ton nombre est inférieur ou égal à 24 ? [on] o Est-ce que ton nombre est inférieur ou égal à 20 ? [on] o Est-ce que ton nombre est inférieur ou égal à 18 ? [on] N --> "N" au lieu de "n" Les seules réponses autorisées sont 'o' (oui) et 'n' (non). --> message d'erreur Est-ce que ton nombre est inférieur ou égal à 18 ? [on] n Est-ce que ton nombre est inférieur ou égal à 19 ? [on] o Ton nombre est 19 !
-
5. Fonctions
Comme en Python, il est possible d'encapsuler du code dans des fonctions afin de pouvoir le réutiliser plus facilement. Ces fonctions peuvent être appelées dans le script, ou depuis le shell ayant chargé le script contenant la fonction.
Les fonctions sont définies avec
NOM_FONCTION() {
...
...
}
Contrairement à Python, on ne déclare ni ne donne de noms aux arguments de la fonction. La fonction définie pourra utiliser 1, 2 ou plus d'arguments. Elle pourra même n'en avoir aucun ! C'est donc au programmeur de gérer les arguments à la main. Pour cela, il dispose des variables suivantes :
-
$#
: nombre d'arguments donnés à la fonction lors de l'appel -
$1
: premier argument donné à la fonction (ou la chaine vide s'il n'y avait pas d'argument), -
$2
: deuxième argument donné à la fonction (ou la chaine vide s'il n'y avait pas de deuxième argument), -
... jusqu'à
$9$
pour le neuvième argument donné à la fonction (ou la chaine vide), -
$@
: la liste des arguments donnés à la fonction, séparés par des espaces.
La variable $@
est particulièrement utile pour faire une boucle
sur les arguments de la fonction avec for x in "$@"
.
Pour appeler la fonction, il suffit d'écrire
$ NOM_FONCTION ARG1 ARG2 ...
Attention, il n'y a pas de parenthèses ou de virgule comme en Python.
Écrivez une fonction show_args
avec le comportement suivant :
-
elle affiche le nombre total d'arguments reçus,
-
elle affiche chaque argument, dans l'ordre, avec son numéro.
Voici un exemple d'exécution :
$ source script_show_args.sh
$ show_args Il fait chaud et beau.
> 5 argument(s)
argument 1: Il
argument 2: fait
argument 3: chaud
argument 4: et
argument 5: beau.
$ show_args Il fait "beau et" chaud.
> 4 argument(s)
argument 1: Il
argument 2: fait
argument 3: beau et
argument 4: chaud.
Si les guillemets ne sont pas pris en compte (dans le second appel), vérifiez
votre boucle : il faut faire for x in "$@"
plutôt que for x
in $@
.
6. Pour aller plus loin
6.1. Sous-shells
Lorsqu'on lance un script avec
$ source SCRIPT.sh
les commandes sont exécutées dans le shell courant. Il est également possible de lancer un script dans un sous-shell temporaire (c'est à dire un autre shell s'exécutant dans le shell courant) avec
$ bash SCRIPT.sh
Lorsque le script se termine, ce shell temporaire se termine également. Cela a des conséquences importantes sur l'environnement du script.
-
Écrivez un script qui affiche le répertoire courant, change de répertoire, et affiche à nouveau le répertoire courant.
-
Lancez ce script avec
-
source SCRIPT.sh
-
bash SCRIPT.sh
et cherchez les différences de comportement pour le shell courant. Vous pouvez par exemple remplir les tableaux suivants :
exécution avec source SCRIPT.sh shell courant répertoire courant au début du script répertoire courant à la fin du script exécution avec bash SCRIPT.sh shell courant sous-shell répertoire courant au début du script répertoire courant à la fin du script -
-
Expliquez précisemment la différence principale entre les 2 manières d'exécuter le script dans votre fichier README.
Par défaut, les variables du shell sont locales au shell courant.
-
Écrivez un script qui affiche la variable
A
, la modifie et l'affiche à nouveau. -
Lancez ce script avec
-
source SCRIPT.sh
-
et bash SCRIPT.sh
et cherchez les différences de comportement pour le shell courant. Vous pouvez par exemple remplir les tableaux suivants :
exécution avec source SCRIPT.sh shell courant valeur de A
au début du scriptvaleur de A
à la fin du scriptexécution avec bash SCRIPT.sh shell courant sous-shell valeur de A
au début du scriptvaleur de A
à la fin du script -
-
Décrivez précisemment ce que vous observez dans votre fichier README et donnez une explication.
Il est possible "d'exporter" la valeur d'une variable dans les nouveaux shells. Il suffit pour ceci de déclarer la variable avec
export VARIABLE
(où VARIABLE
est le nom de la variable à exporter).
-
Exportez la variable
A
dans votre shell courant, et lancez le script de la question précédente avec-
source SCRIPT.sh
-
et bash SCRIPT.sh.
Quelles différences observez vous avec le comportement sans exporter la variable
A
? -
-
Ces 2 exécutions ont elles le même comportement maintenant que la variable
A
est exportée ? Décrivez précisemment ce que vous constatez dans votre fichier README.
Il est possible d'utiliser le résultat d'une commande comme valeur d'une
variable avec VAR=$(COMMANDE)
. Dans ce cas, la variable prendra
comme valeur le résultat affiché par la commande. Par exemple :
NOW=$(date)
echo "La date d'aujourd'hui est '$NOW'."
Décrivez une expérience (et sa conclusion) pour décider si
$(COMMANDE)
utilise le shell courant ou un sous-shell pour lancer
la commande.
6.2. Arguments d'un script
Lorsqu'on lance un script dans un sous-shell (avec bash SCRIPT.sh), les arguments donnés après le nom du
fichier sont passés au script comme pour une fonction usuelle : dans
$1
, $2
, etc.
Transformez votre script6.sh pour qu'on puisse lui donner un prénom (facultatif) et un nom (obligatoire) :
$ bash script6.sh
Erreur, il faut donner un nom !
$ bash script6.sh Tartempion
Ton nom est Tartempion
$ bash script6.sh Arthur Tartempion
Bienvenu Arthur
Ton prénom est Tartempion