Consignes

Le rendu du TP consistera en une archive (fichier .zip, .tar, etc.) comportant votre travail.

L'archive contiendra :

Liens utiles

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 utilisé au TP2) est essentiellement constitué d'un ensemble de scripts shell.

Dans ce TP, vous allez découvrir :

Bien sûr, tous les concepts vus au TP2 comme les redirections restent valides dans un script.

  1. 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 ???
  2. 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
    
  3. 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.

  4. 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 commandes 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.

  1. 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 =.

  2. 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 commande print 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.

  1. 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.

  2. 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
  1. Certaines variables sont définies automatiquement par le shell. Affichez les et cherchez à quoi elles correspondent :

    • HOME

    • PWD

    • OLDPWD

    • SHELL

    • USER

  2. 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

  1. Testez le script suivant et analysez le résultat

    p=97
    q=73
    echo "Le produit $p * $q est égal à $p*$q."
    
  2. 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 :

  1. 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
    
  2. Remplacez la ligne for N in "C'est" "l'histoire" "d'un mec" par for N in *. Lancez le script correspondant et expliquez son résultat.

  3. 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".

  1. 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 puis 1

    • false puis true

    et décrivez attentivement ce qui se passe.

  2. 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 et false.

    • 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.

  3. 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 ?

  4. À 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").

  1. Décrivez une manière de vérifier que test "$i" -lt 10 donne bien une valeur de retour "vraie" lorsque que la variable i 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 commande test provoquera une erreur !

  2. 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.

  3. 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 :

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

Écrivez un script fichiers.sh qui parcourt le répertoire courant et

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 :

  1. La syntaxe des boucles while est

    while CONDITION
    do
      ...
      ...
    done
    
  2. Vous pouvez lire une ligne au clavier avec la commande read VARIABLE, comme dans

    echo "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.

  3. Le principe est le suivant : tant que a est strictement plus petit que b, on prend la moyenne m de a et b.

    • Si le nombre pensé est inférieur ou égal à m, on remplace b par m.

    • Sinon, on remplace a par m+1.

    Lorsque a et b deviennent égaux, c'est qu'on a trouvé le nombre pensé.

  4. 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 !"
    
  5. 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 :

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 :

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.

  1. Écrivez un script qui affiche le répertoire courant, change de répertoire, et affiche à nouveau le répertoire courant.

  2. 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
  3. 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.

  1. Écrivez un script qui affiche la variable A, la modifie et l'affiche à nouveau.

  2. 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 script
    valeur de A à la fin du script
    exécution avec bash SCRIPT.sh
    shell courant sous-shell
    valeur de A au début du script
    valeur de A à la fin du script
  3. 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).

  1. 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 ?

  2. 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