MO101 - Introduction à Linux - Feuille TD4

Bienvenue dans le quatirème TD du cours MO101 !

Au cours de ce TD, nous allons appronfondir nos connaissances des commandes de traitements de chaines de caractères sed et des scripts Bash.

Partie -1 - Production de documents

Commencez par récupérer l'archive pour ce TD, et désarchivez la dans le répertoire mo101

archive_td5.tar.gz

Q1) Donnez le code LaTeX associé au document PDF 'integration.pdf'

Voici quelques indications qui vous aideront à faire cet exercice :


    Cf le cours pour les différentes constructions LaTeX utilisées pour générer ce document


Partie 0 - Préliminaires

Récupération des archives pour ce TD en ligne de commande

archive_td4.tar.gz

Q1) Depuis votre répertoire personnel, placez vous dans le répertoire mo101


Réponse:

    cd mo101

Q2) Téléchargez l'archive avec la commande wget


Réponse:

    wget https://perso.ensta-paris.fr/~chapoutot/teaching/mo101/archive_td4.tar.gz

Pour récupérer l'adresse de l'archive, il faut faire clique droit sur le lien ci-dessus et faire copier le lien.

Q3.b) À partir de votre dossier mo101, désarchivez l'archive du TD4 à la ligne de commande.


Réponse :

    tar -xzvf archive_td4.tar.gz

Vous devriez maintenant avoir son contenu dans un dossier "td4". Rendez-vous dans ce sous-dossier, et répondez aux prochaines questions.

Partie 1 - Arborescence de fichiers (révision des manipulations simples)

Q1. Allez dans le répertoire "arborescence".


Réponse :
    cd arborescence

Q2. Créez les répertoires permettant d'avoir l'arborescence suivante

arborescence
|-- rep1/
    |-- srep1/
    |-- srep2/
|-- rep2/
    |-- srep1/
    |-- srep2/
|-- rep3/
    |-- srep1/
    |-- srep2/

Réponse :
    mkdir -p rep1/srep1 rep1/srep2
    mkdir -p rep2/srep1 rep2/srep2
    mkdir -p rep3/srep1 rep3/srep2

on peut aussi faire un script Bash si on veut

Q3. Allez dans le répertoire rep3/srep2.


Réponse :
    cd rep3/srep2

Q4. Créez un fichier nommé "message.txt" dont le contenu est la chaine de caractères "for you eyes only".


Réponse :
    echo "for your eyes only" > message.txt

Q5. Créez une archive nommée "secret.tar.gz" contenant le fichier nouvellement crée.


Réponse :
    tar -cvzf secret.tar.gz message.txt

Q6. Copiez cette archive dans le répeteroire rep1/srep1


Réponse :
    cp secret.tar.gz ../../rep1/srep1/secret.tar.gz

Q7. Supprimez le fichier message.txt du répertoire courant


Réponse :
    rm message.txt

Q8. Allez dans le répertoire rep1/srep1


Réponse :
    cd ../../rep1/srep1

Q9. Désarchivez l'archive secret.tar.gz


Réponse :
    tar -xvzf secret.tar.gz

Q10. Retournez dans le répertoire td4


Réponse :
    cd ../../..

Q11. Supprimez le repertoire "arborescence"


Réponse :
    rm -rf arborescence

Partie 1 - Faire part de naissance

L'objectif de cet exercice est de générer automatiquement des faire-part de naissance à partir de modèles pré établis et une liste de naissances. Les modèles sont dans le répertoire modeles et la liste des naissances est dans le fichier naissances.txt. Les modèles de faire-part de naissance sont formés de texte dans lequel des balises spéciales ont été insérées, par exemple, <PRENOM> ou <DATE>, qui représentent les emplacements dans le texte où devront se situer le prénom et la date de naissance du bébé.

Dans le fichier naissances.txt, il y a une liste de naissance regroupant les informations nécessaires psur générer les faire-part.

Q1. Examinez le contenu et la structure du fichier naissances.txt


Réponse :

    less naissances.txt

C'est un fichier au format CSV (Comma separated values). Le contenu de chaque colonne est dans l'ordre: le genre, le prénom, la taille, le poids et la date de naissance du bébé.

Q2. Créez un répertoire faire-part qui contiendra les faire-part générés.


Réponse :

    mkdir faire-part

Q3. Ecrire un script creerFairePart.sh qui prend comme paramètre un nom de modèle de faire-part, le prenom, la taille, le poids et la date de naissance du bébé et remplace dans le modèle du faire part les informations nécessaires. Il affichera le résultat sur la sortie standard

Indice: on pourra utiliser la commande sed et sa capacité à substituer un texte par un autre.

Exemple: Un exemple d'exécution de ce script est

$ cat modeles/faire-part-fille-3.txt
Coucou c’est moi !
Je m'appelle <PRENOM>
Et de mes <TAILLE>cm et <POIDS>kg
Je ravis déjà mes parents !
Ils m’ont découverte le <DATE>
Et depuis sont chaque jour enchantés !
$
$ bash creerFairePart.sh modeles/faire-part-fille-3.txt Louise 45 3.3 2019-1-02
Coucou c’est moi !
Je m'appelle Louise
Et de mes 45cm et 3.3kg
Je ravis déjà mes parents !
Ils m’ont découverte le 2019-1-02
Et depuis sont chaque jour enchantés !

Réponse :
    #! /bin/bash

    MODELE=$1
    PRENOM=$2
    TAILLE=$3
    POIDS=$4
    DATE=$5

    sed -e "s/<PRENOM>/$PRENOM/" -e "s/<TAILLE>/$TAILLE/" -e "s/<POIDS>/$POIDS/"  -e "s/<DATE>/$DATE/" $MODELE

Note: les double guillements dans les actions de la commande sed sont importants pour permettre l'expansion des variables.

Q4. Les dates dans le fichier naissances.txt sont sous le formats anglo-saxon (année-mois-jour) et avec un chiffre pour le mois. Modifiez votre script précédent pour transformer la date au format européen et avec un nom de mois au lieu d'un chiffre. Indice: man date


Réponse :

La commande à utiliser est
   date -d ma_date "+%d %B %Y"

d'où la modification suivante dans le script

     #! /bin/bash

     MODELE=$1
     PRENOM=$2
     TAILLE=$3
     POIDS=$4
     DATE=`date -d $5 "+%d %B %Y"`


     sed -e "s/<PRENOM>/$PRENOM/" -e "s/<TAILLE>/$TAILLE/" -e "s/<POIDS>/$POIDS/"  -e "s/<DATE>/$DATE/" $MODELE

Note: les double guillements dans les actions de la commande sed sont toujours importants pour permettre l'expansion des variables.

Note: 2 sous Mac Os la commande date ne fonctionne pas de la même manière que sous Linux (la solution donnée ici ne fonctionne pas)

Q5. Ecrire une commande qui permet de calculer le nombre de modèles de faire-part pour les filles.


Réponse :

    ls modeles/*fille* | wc -l

Q6. Ecrire une commande qui permet de calculer le nombre de modèles de faire-part pour les garçons.


Réponse :

    ls modeles/*garcon* | wc -l

Q7. Donnez un script choisirModeleFille.sh qui permet de choisir alétoirement un modèle de faire part pour les filles. L'adresse du fichier du modèle choisi sera affiché sur la sortie standard.

Note : pour tirer un nombre aléatoire entre 1 et 10, il faut utiliser la commande $(( ( RANDOM % 10 ) + 1 )).


Réponse :

    #! /bin/bash

    NBMODELE=`ls modeles/*fille* | wc -l`
    CHOIX=$(( (RANDOM % $NBMODELE) + 1))
    echo `ls modeles/*fille-$CHOIX*`

Q8. Donnez un script choisirModeleGarcon.sh qui permet de choisir alétoirement un modèle de faire part pour les garçons. L'adresse du fichier du modèle choisi sera affiché sur la sortie standard.

Note : pour tirer un nombre aléatoire entre 1 et 10, il faut utiliser la commande $(( ( RANDOM % 10 ) + 1 )).


Réponse :

    #! /bin/bash

    NBMODELE=`ls modeles/*garcon* | wc -l`
    CHOIX=$(( (RANDOM % $NBMODELE) + 1))
    echo `ls modeles/*garcon-$CHOIX*`

Q9. A l'aide de la commande cut, écrivez une commande qui permet de récupérer le genre d'un bébé dans une ligne du fichier naissances.txt


Réponse :

    echo "Garcon;Hugo;50;3;2018-04-27" | cut -d";" -f 1

Q10. Créez un répetoire faire-part.


Réponse :

    mkdir faire-part

Q11. Ecrire un script genererFairePart.sh qui génère la liste des faire-part en lisant ligne par ligne le fichier naissances.txt et en utilisant les scirpts précédents. Chaque faire-part générer sera sauvegarder dans un fichier dont le nom suivra le format prenom-date.txt et placer dans le répertoir faire-part.

La construction en shell pour lire un fichier ligne par ligne est

cat fichier | while  read LIGNE ; do
  ...
  echo $LIGNE
  ...
done

Réponse :

#! /bin/bash

cat naissances.txt | while read LINE; do
    GENRE=`echo $LINE | cut -d";" -f 1`
    PRENOM=`echo $LINE | cut -d";" -f 2`
    TAILLE=`echo $LINE | cut -d";" -f 3`
    POIDS=`echo $LINE | cut -d";" -f 4`
    DATE=`echo $LINE | cut -d";" -f 5`

    if [ $GENRE == 'Fille' ]; then
    bash creerFairePart.sh `bash choisirModeleFille.sh` $PRENOM $TAILLE $POIDS $DATE > faire-part/$PRENOM-$DATE.txt
    else
    bash creerFairePart.sh `bash choisirModeleGarcon.sh` $PRENOM $TAILLE $POIDS $DATE > faire-part/$PRENOM-$DATE.txt
    fi
done

Q12. Faire une archive nommée faire-part.tar.gz de tous les faire-part générés.


Réponse :

    tar -cvzf faire-part.tar.gz faire-part

Partie 2 - La commande sed

La commande sed (pour Stream Editor) permet de transformer un flux de caractères en entrée pour le transformer en substituant du texte, en supprimant du texte ou encore en extrayant du texte.

On rappelle la forme générale des commandes de sed

Une expressions régulière ou expression rationnelle est une chaine de caractères qui décrit un ensemble de chaines de caractères. Elle utilise une syntaxe particulière pour décrire différentes classes de caractères qui composent l'ensemble de chaines de caractères. Il existe plusieurs types de langage d'expressions régulières, nous allons ici considérer un sous ensemble des BRE (Basic Regular Expression). La commande sed (Stream Editor) est capable d'utiliser des expressions rationnelles pour faire des traitements sur les chaines de caractères (cf "man sed").

Une expression rationnelle (BRE) est composée :

Exemples :

A noter que l'opérateur * est gourmand, c'est-à-dire qu'il essaiera toujours de reconnaitre la chaine de caractères la plus longue.

La principale différence entre les ERE et les BRE réside dans la notion de groupe capturant dans les BRE dénoté par \(\) les parenthèses. Il permet de mettre en mémoire une chaine de caractères qui correspond à une expression rationnelle entourée des parenthèses. On peut ensuite utiliser la chaine en mémoire pour autre chose. Les groupes capturant sont notés à partir de la parenthèse ouvrante et sont au nombre de 9 au maximum. Utiliser le premier groupe capturant est possible grâce à la commande \1, le second avec la commande \2, etc.

Q1. A l'aide de la commande sed, supprimez la première ligne du fichier naissances.txt.


Réponse :

    sed -e '1d' naissances.txt

Note: nous utilisons comme adresse le numéro de ligne du fichier.

Q2. A l'aide de la commande sed, supprimez la première ligne du fichier naissances.txt en faisant une copie de sauvegarde à l'aide de l'option -i.


Réponse :

    sed -i.save -e '1d' naissances.txt

Note: nous utilisons comme adresse le numéro de ligne du fichier. L'option -i permet de faire une modification en place (i.e., change le contenu du fichier __naissances.txt__) et fait une copie de sauvegarde dont le nom est __naissances.txt.save__

Q3. A l'aide de la commande sed, supprimez toutes les lignes du fichier naissances.txt qui contiennent le prénom Nathan


Réponse :

    sed -e '/Nathan/d' naissances.txt

Note: nous utilisons comme adresse l'expression rationnelle /Nathan/ donc sed détecte les lignes correspondant à cette expression rationnelle avant d'appliquer le traitement, ici une suppression.

Q4. A l'aide de la commande sed, affichez les lignes 15 à 28 du fichier naissances.txt.


Réponse :

    sed -n -e '18,25p' naissances.txt

Note: nous utilisons un "range" d'addresses décrites par les lignes du fichier. L'option -n permet de ne pas afficher
les lignes (comportement par défaut) du fichier afin de n'afficher que les lignes sélectionnées

Q5. A l'aide de la commande sed, supprimez les lignes 5 à 10 du fichiers naissances.txt.


Réponse :

    sed -e '5,10d' naissances.txt

Note: nous utilisons un "range" d'addresses décrites par les lignes du fichier. Nous n'utilisons pas ici l'option -n
afin que les lignes non supprimées du fichier soient affichées.

Q6. A l'aide de la commande sed, extraire la balise titre de la page HTML index.html


Réponse :

    sed -n -e '/<title>/,/<\/title>/p' index.html

Note: Nous utilisons un "range" d'addresses  décrites par expressions rationnelles. L'option -n permet de ne pas afficher
les lignes (comportement par défaut) du fichier afin de n'afficher que les lignes sélectionnées.

Note 2: Quand un "range" d'adresses utilisent des expressions rationnelles, la seconde expressions rationnelles n'est évaluée qu'après la fin de la première ligne qui correspond à la première expression rationnelle. En particulier si les balises `` et `` étaient sur la même ligne cela ne fonctionnerait pas.

Q7. A l'aide de la commande sed, extraire toutes les balises li de la page HTML index.html


Réponse :

    sed -n -e '/<li>/p' index.html

Note: Comme les balises `<li>` et `</li>` sont sur la même ligne, nous ne pouvons pas utiliser la solution précédente. Nous décrivons donc une adresse avec une seule expression rationnelle afin d'afficher les lignes qui nous intéressent.


Q8. Que font les commandes suivantes ?

sed -e 's/<\(.*\)>/<\U\1>/' index.html
sed -e 's/<\(.*\)>/<\u\1>/' index.html

Réponse :

L'utilisation des parenthèses \(\) permet de définir un bloc capturant, c'est-à-dire de garder en mémoire la chaîne de caractères qui correspond à l'expression    rationnelle entre parenthèse.
On peut ensuite utiliser cette chaine de caractères capturés, nommée ici \1, pour faire un traitement.
En particulier, la fonction \U permet de mettre en lettres majuscules la chaine de caractères qui suit immédiatement cette fonction.
La fonction \u permet de mettre seulement le premier caractère en lettre majuscule de la chaine de caractères qui la suit.

Dans ces exemples, les deux commandes permettent soit de mettre tous les noms des balises HTML en lettres majuscules soit uniquement la première lettre des noms des balises en majuscule.

A noter, qu'il existe également la fonction \L et \l qui permettent de mettre en lettres minuscules la chaine de caractères ou la première lettre de la chaine
de caractères qui suit la fonction.


Note: les fonctions \U et \u ne fonctionnent pas sous Mac OS.

Q9. En vous aidant de la question 5.; mettez en majuscule le titre (définit entre les balises <h1> et </h1>) de la page HTML.


Réponse :

    sed -e 's/<h1>\([^<]*\)/<h1>\U\1/' index.html

Note: L'expression rationnelle [^<]* permet de capturer des chaines de caractères formées de tous les caractères sauf le caractères <.
On l'utilise ici pour capturer la chaine entre les balises <h1> et surtout pour éviter de capturer la balise fermante </h1>.


Partie 3 - Test d'applications (Facultatif)

Dans cette partie nous allons écrire un script Bash qui permet de tester un programme. La fonction considérée est la fonction nth qui prend en argument une chaîne de caractères et une position et affiche le caractère se trouvant à cette position dans la chaîne.

Le script Python3 nth_print.py met en oeuvre cette fonction nth avec un ajout pour prendre les arguments de la fonction sur la ligne de commande.

Par exemple, dans le terminal on aura l'exécution suivante :

$ python3 nth_print.py "salut" 3
u

Dans le fichier dataset.txt, un jeu de données de test a été défini. L'objectif de cet exercice est d'exécuter les différents test défini dans le fichier dataset.txt pour vérifier que le programme nth_print.py fonctionne correctement.

Q1. Examinez le contenu et la structure du fichier dataset.txt


Réponse :

    less dataset.txt

C'est un fichier au format CSV (Comma separated values). Le contenu de chaque colonne est dans l'ordre: une chaine de caractère, une position et le résultat attendu de l'exécution du script **nth_print.py**

Q2. Ecrire un script executeTest.sh qui prend en argument une chaine de caractère, une position et un résultat attendu. Ce script exécute le programme nth_print.py et compare la sortie produite par ce programme au résultat attendu.

En exemple:

$ bash executerTest.sh chaine 3 i
Test nth("chaine", 3) => i == i (OK)
$ bash executerTest.sh chaine 3 j
Test nth("chaine", 3) => i != j (KO)

Réponse :

#! /bin/bash

CHAINE=$1
POSITION=$2
ATTENDU=$3

RESULTAT=`python3 nth_print.py "$1" $2`

echo -n "Test nth(\"$CHAINE\", $POSITION) => $RESULTAT"
if [ "$ATTENDU" == "$RESULTAT" ]; then
    echo " == $ATTENDU (OK)"
else
   echo " != $ATTENDU (KO)"
fi

Note: dans l'appel au programme **nth_print.py** il faut protéger le premier argument avec des guillemets car potentiellement cette chaîne peut contenir des espaces.

Note: Dans la condition du **if** il faut mettre les variables entre guillemets car potentiellement leur valeur peut contenir des espaces.

Q3. Ecrire un script toutTester.sh qui exécute tous les tests définis dans le fichier dataset.txt et affiche le résultat sur la sortie standard.

On rappelle que la construction en Shell pour lire un fichier ligne par ligne est

cat fichier | while  read LIGNE ; do
  ...
  echo $LIGNE
  ...
done

Réponse :

#! /bin/bash

cat dataset.txt | while read LIGNE; do
    CHAINE=`echo $LIGNE | cut -d"," -f 1`
    POSITION=`echo $LIGNE | cut -d"," -f 2`
    ATTENDU=`echo $LIGNE | cut -d"," -f 3`

    bash executerTest.sh "$CHAINE" $POSITION "$ATTENDU"
done

Note: Dans l'appel au script executerTest.sh on protège les arguments par des guillemets car potentiellement ceux-ci ont des valeurs qui peuvent
contenir des espaces.

Q4. Est-ce que l'exécution des tests vous semble normal ?


Réponse :

Il y a un problème avec le troisième test avec la position 0.

Q5. Corrigez le programme nth_print.py


Réponse :

il faut mettre une comparaison strict dans le première conditionnelle qui détecte si on donne une position négative.

Q6. Ajoutez des compteurs permettant de calculer le nombre de tests réussis et échoués afin de faire un rapport en fin d'exécution du script toutTester.sh


Réponse :

Une première solution qui ne fonctionne pas (Pourquoi ?)

#! /bin/bash

SUCCES=0
ECHECS=0

cat dataset.txt | while read LIGNE; do
    CHAINE=`echo $LIGNE | cut -d"," -f 1`
    POSITION=`echo $LIGNE | cut -d"," -f 2`
    ATTENDU=`echo $LIGNE | cut -d"," -f 3`

    RESULTAT=`bash executerTest.sh "$CHAINE" $POSITION "$ATTENDU"`
    echo $RESULTAT

    if [ `echo "$RESULTAT" | grep -c '(OK)$'` -eq 1 ]; then
    SUCCES=$((SUCCES + 1))
    echo "SUCCES $SUCCES"
    else
    ECHECS=$((ECHECS + 1))
    echo "Echecs $ECHECS"
    fi
done

echo "$SUCCES test(s) reussi(s) et $ECHECS test(s) echoue(s)"

Car l'utilisation d'un pipe (cat dataset | while ...) crée un sous-processus qui ne permet pas de partager les variables SUCCES et ECHEC. La solution est d'utiliser un autre structure de lecture de fichier ligne par ligne qui utilise une redirection du flux d'entrée de la forme

while read LINE; do
   echo -e "$LINE\n"
done < file.txt

En conséquence nous avons la solution suivante

#! /bin/bash

SUCCES=0
ECHECS=0

while read LIGNE; do
    CHAINE=`echo $LIGNE | cut -d"," -f 1`
    POSITION=`echo $LIGNE | cut -d"," -f 2`
    ATTENDU=`echo $LIGNE | cut -d"," -f 3`

    RESULTAT=`bash executerTest.sh "$CHAINE" $POSITION "$ATTENDU"`
    echo $RESULTAT

    if [ `echo "$RESULTAT" | grep -c '(OK)$'` -eq 1 ]; then
    SUCCES=$((SUCCES + 1))
    echo "SUCCES $SUCCES"
    else
    ECHECS=$((ECHECS + 1))
    echo "Echecs $ECHECS"
    fi
done < dataset.txt

echo "$SUCCES test(s) reussi(s) et $ECHECS test(s) echoue(s)"