PROFDINFO.COM

Votre enseignant d'informatique en ligne

Section 5 - Lecture et écriture dans des fichiers

Retour à la page du cours

Nous apprendrons ici comment lire et écrire dans des fichiers à même notre scripts. Ce sont des opérations fort courantes dans le monde Unix, où toutes les configurations sont généralement stockées dans des fichiers textes. Plusieurs applications utilisent également des fichiers texte à la manière de bases de données, avec un enregistrement par ligne et plusieurs champs par enregistrement.

5.1 - Lire dans un fichier avec read

5.2 - Lire dans un fichier avec read et une boucle

5.3 - Décomposer les lignes en mots

5.4 - Internal Field Separator

5.5 - Écrire dans un fichier

5.6 - Exercices

5.1 - Lire dans un fichier avec read

La commande read, qui nous permet de lire une ligne tapée au clavier, peut également aller lire dans un fichier. Le fonctionnement est assez simple: on utilise la redirection (<) directement dans le script (notez que l'effet est alors exactement le même que si on ne l'utilisait pas dans le script mais que l'usager l'utilisait en appelant notre script).

Par exemple, supposons un fichier file.txt qui contient les lignes suivantes:

Bonjour les petits amis
Comment ça va
Je m'appelle
Georges

Écrivons ensuite le script tout simple myread:

read LIGNE < file.txt
echo $LIGNE

Lorsque l'on exécute ce script, le fichier file.txt est passé à la commande read, qui en lit la première ligne et la place dans la variable LIGNE.

Remarquez que l'effet est le même que si on avait créé myread ainsi:

read LIGNE 
echo $LIGNE

et que l'on l'appelait de cette façon:

myread < file.txt

À ce moment, le fichier file.txt est utilisé au lieu de l'entrée standard (le clavier). C'est comme si on tapait le contenu du fichier au clavier.

Tout ceci est bien joli, mais qu'arrive-t-il si on veut lire plusieurs lignes dans notre fichier? Si on modifie myread pour y ajouter plusieurs reads, comme ceci:

read LIGNE1 
echo $LIGNE1
read LIGNE2 
echo $LIGNE2
read LIGNE3 
echo $LIGNE3

on peut ensuite l'appeler avec la redirection:

myread < file.txt

À ce moment, comme notre input est remplacé par le contenu du fichier, chaque read lira une ligne du fichier, comme si on les avaient tapées dans le même ordre, ce qui est fort logique.

Est-ce qu'on peut faire la même chose en utilisant la redirection à l'intérieur du script? Essayons-le. Modifions myread de la façon suivante:

read LIGNE < file.txt
echo $LIGNE
read LIGNE < file.txt 
echo $LIGNE
read LIGNE < file.txt
echo $LIGNE

Remarquez que j'utilise toujours la même variable - en effet, pourquoi en utiliser d'autres puisqu'une fois qu'une ligne est écrite on ne l'utilisera plus? Maintenant, en théorie, on pourrait appeler notre script simplement comme ceci, puisque que notre redirection est à l'intérieur du fichier:

myread

Malheureusement le résultat n'est pas celui escompté. Pourquoi? Parce que la ligne read LIGNE < file.txt lit la première ligne du fichier, peu importe combien de fois on l'appelle dans un même script. D'un read à l'autre, la commande read ne sait pas qu'il y en a eu d'autres avant elle... Comment régler ce problème, autrement que de forcer l'utilisateur à appeler notre script avec une redirection (solution fort peu élégante dans le cas d'un fichier de configuration)? La solution dans la section suivante...

Retour à la table des matières de la section

5.2 - Lire dans un fichier avec read et une boucle

Eh oui, il suffit d'utiliser une boucle, qui utilise une commande comme condition. Il faut savoir que la commande read retourne 0 (qui sera interprété comme "vrai" dans une boucle) si elle arrive à lire une ligne du fichier et autre chose sinon (entre autres si elle ne peut plus lire de lignes parce qu'elle est arrivée à la fin du fichier).

Pour lire le contenu entier d'un fichier dans une boucle, on redirigera donc le fichier à la boucle plutôt qu'au read. Ça se fait (assez curieusement) en redirigeant au done qui ferme la boucle:

while read LIGNE
do
   echo $LIGNE
done < file.txt

Notez bien la particularité de la redirection au done. Si on tentait plutôt de la mettre au read, on aurait des résultats bien différents. Lesquels et pourquoi, selon vous?

Voici donc le fonctionnement de notre lecture en boucle: à l'entrée de la boucle, bash exécute la commande read LIGNE, qui lira une ligne du fichier file.txt puisqu'il est redirigé à la boucle. Si cette lecture fonctionne, read retourne 0, la condition est "vraie", donc on peut entrer dans la boucle (puisque le while roule tant que la condition est vraie).

Le echo sera donc exécuté, affichant la ligne que l'on vient de lire, puis on re-testera la condition en exécutant de nouveau le read. Lorsque la dernière ligne aura été affichée, l'exécution suivante du read retournera un code différent de 0 (erreur), puisqu'il n'y a plus rien à lire dans le fichier et que le read ne fonctionne plus. Dans ce cas, la condition n'est plus vraie, on n'entre donc plus dans la boucle et le tout se termine.

On a donc l'avantage de pouvoir lire plusieurs lignes dans un même fichier, mais en plus on peut y lire tout ce qui s'y trouve, sans savoir à l'avance combien de lignes contient le fichier!

Retour à la table des matières de la section

5.3 - Décomposer les lignes en mots

Il est possible de passer plus qu'une variable au read. Dans ce cas, la phrase qui est lue sera décomposée en différents mots, qui seront placés, dans l'ordre, dans chacune des variables. Par exemple, modifions notre script précédent pour obtenir:

while read MOT1 MOT2 MOT3
do
   echo "Mot 1:  $MOT1; Mot 2:  $MOT2; Mot 3:  $MOT3"
done < file.txt

Le script fonctionne de la même façon pour lire une ligne à la fois du fichier file.txt. La seule différence ici est qu'au lieu de placer toute la ligne dans la variable LIGNE, il placera le premier mot dans MOT1, le deuxième dans MOT2 et le reste de la ligne dans MOT3. Si jamais une ligne contient moins de trois mots, les dernières variables seront simplement vides.

La décomposition sera faite simplement en fonction des espaces. Notez qu'il peut y avoir plusieurs espaces entre deux mots, il peut même y avoir des tabulations, et la séparation se fera tout de même correctement. Remarquez également que cet usage du read est aussi valide pour la lecture au clavier.

Ce sont là les bases de la décomposition d'un fichier en ses lignes et mots, ce qui permet d'utiliser un fichier de configuration pour faire fonctionner un script.

Évidemment, il y a toujours moyen de faire plus compliqué... Par exemple: comment faire pour aller lire le fichier /etc/passwd afin d'aller décomposer en différents "mots" tout son contenu (c'est à dire, si je veux avoir le nom de l'usager, son groupe, son répertoire home, etc, dans des variables séparées?).

Testons avec notre script précédent en remplaçant la dernière ligne par:

done < /etc/passwd

Qu'est-ce qui se passe? Pourquoi? Comment remédier à la situation?

Retour à la table des matières de la section

5.4 - Internal Field Separator

Le problème avec le fichier /etc/passwd, c'est que les différents mots sont séparés non pas par des espaces mais par des deux-points (:). Lorsque le read lit une ligne, il ne voit pas d'espaces alors il place la ligne au complet dans la variable MOT1, et laisse les deux autres variables vides puisqu'il n'y a plus rien à y mettre.

La façon d'arranger le tout est toute simple: il existe une variable (appelée IFS pour Internal Field Separator), qui contient le caractère qui doit être utilisé pour séparer les mots dans une phrase. Par défaut, cette variable contient un espace, comme on peut le voir en tapant au prompt:

echo .$IFS.

(les points sont nécessaires si on veut voir l'espace...)

On pourra donc simplement modifier le contenu d'IFS avant de faire nos reads pour lui dire de séparer les mots avec des deux-points. On aurait donc alors le script suivant:

IFS=":"
echo "Liste des usagers enregistres " 
echo "----------------------------------------------"
while read USER PASSWORD USERID GROUPID NAME HOMEDIR USERSHELL
do
   echo "Usager:                 $USER"
 	echo "Mot de passe encrypte:  $PASSWORD"
 	echo "ID usager:              $USERID"
 	echo "ID groupe:              $GROUPID"
 	echo "Nom complet:            $NAME"
 	echo "Repertoire maison:      $HOMEDIR"
 	echo "Shell par defaut:       $USERSHELL"
 	echo "----------------------------------------------"
done < /etc/passwd

Exécutez ce script avec un "| less" puisqu'il y produira un résultat assez volumineux...

Maintenant on pourrait bien se demander: "Est-il possible de voir le nom du groupe pour chaque usager, plutôt que son numéro qui ne nous dit pas grand chose?". Évidemment, c'est possible. Il suffit de savoir que:

  • Le fichier /etc/group contient, dans l'ordre, les champs suivants:
    • Nom du groupe
    • Mot de passe encrypté (la plupart du temps ce champ est vide, sinon il y aura un x puisque les mots de passe sont dans /etc/gshadow par mesure de sécurité)
    • ID du groupe (c'est ce numéro qu'on retrouve dans /etc/passwd)
    • Liste d'usagers ayant ce groupe comme groupe secondaire (séparés par des virgules)
  • Les champs de /etc/group sont séparés par des deux-points, comme /etc/passwd.
  • On peut très facilement faire une boucle à l'intérieur d'une autre boucle...

Retour à la table des matières de la section

5.5 - Écrire dans un fichier

De la même façon qu'on peut rediriger un fichier à l'entrée d'un read, on peut rediriger la sortie d'un echo dans un fichier.

Il suffit d'utiliser le ">" ou le ">>", comme on pourrait le faire à la ligne de commandes. N'oubliez pas que le ">" envoie la ligne dans un fichier donné, détruisant ce fichier s'il existe déjà, tandis que le ">>" envoie la ligne dans un fichier donné, l'ajoutant à la fin du fichier s'il existe déjà.

On voudra donc souvent utiliser le ">" pour la première ligne puis des ">>" pour les autres. Par exemple, si on voulait que notre script de lecture de /etc/passwd crée un fichier avec les informations au lieu d'afficher le tout à l'écran, on n'aurait qu'à le modifier de la façon suivante:

IFS=":"
echo "Liste des usagers enregistres " > /etc/clear_passwd
echo "----------------------------------------------" >> /etc/clear_passwd

while read USER PASSWORD USERID GROUPID NAME HOMEDIR USERSHELL
do
   echo "Usager:                 $USER"      >> /etc/clear_passwd
   echo "Mot de passe encrypte:  $PASSWORD"  >> /etc/clear_passwd
   echo "ID usager:              $USERID"    >> /etc/clear_passwd
   echo "ID groupe:              $GROUPID"   >> /etc/clear_passwd
   echo "Nom complet:            $NAME"      >> /etc/clear_passwd
   echo "Repertoire maison:      $HOMEDIR"   >> /etc/clear_passwd
   echo "Shell par defaut:       $USERSHELL" >> /etc/clear_passwd
   echo "----------------------------------------------" >>  /etc/clear_passwd
done < /etc/passwd

À ce moment le script créerait un fichier appelé /etc/clear_passwd, détruisant toute copie qui existerait déjà, pour y inscrire la ligne de titre. Toutes les lignes suivantes seraient ajoutés à la suite du fichier, pour le remplir de tout notre output. Remarquez que cela revient au même que de laisser le script comme il était et de l'appeler en faisant par exemple:

readpasswd > /etc/clear_passwd

La seule différence est que dans notre nouvelle version, le script est conçu pour écrire automatiquement dans un fichier prédéfini et de ne rien afficher à l'écran – c'est quelque chose que l'on veut souvent faire lorsque l'on créé par exemple un log sur l'exécution de notre script.

Notez que dans le cas d'un script qui sera exécuté de façon automatisée à intervalles réguliers, on voudra ajouter constamment au même log et on évitera d'utiliser la redirection ">", même pour la première ligne.

Retour à la table des matières de la section

5.6 - Exercices

(voyez les solutions à ces exercices ici)

  1. Essayez maintenant de modifier le script de décomposition de /etc/passwd pour qu'il affiche le nom du groupe plutôt que le ID numérique pour chaque usager trouvé dans /etc/passwd. Faites une petite analyse avant de débuter vos modifications, question de réfléchir sur la façon de faire. Une fois cette réflexion faite, la modification en elle-même devient beaucoup plus facile.
     

    Dans le cas (improbable) où un usager ferait partie d'un groupe qui n'existerait pas, on affichera alors le ID numérique trouvé dans la ligne de l'usager.

    Question de se simplifier la vie, on ne tiendra pas compte des groupes secondaires et on ne traitera que le groupe primaire de chaque usager.

    .

  1. Vous devrez réaliser un script qui permet de consulter et de modifier aisément le fichier /etc/hosts.  Ce fichier contient des alias pour des adresses IP et permet d'utiliser ces alias au lieu de taper des adresses IP au long dans n'importe quelle tentative de connexion internet.

Retour à la table des matières de la section