Licence CC BY-NC-SA

Les fichiers

Nos programmes actuels souffrent d'un grave inconvénient : tout le travail que nous pouvons effectué, tous les résultats obtenus, toutes les variables enregistrées en mémoire vive… ont une durée qui se limite à la durée d'utilisation de notre programme. Sitôt fermé, nous perdons toutes ces données, alors qu'elle pourraient s'avérer fort utiles pour une future utilisation.

Il serait donc utile de pouvoir les enregistrer, non pas en mémoire vive, mais sur le disque dur. Et pour cela, nous devons être capable de créer des fichiers, de les lire ou de les modifier. C'est d'ailleurs ainsi que procèdent la plupart des logiciels. Word ou OpenOffice ont besoin d'enregistrer le travail effectué par l'utilisateur dans un fichier .doc, .odt… De même, n'importe quel jeu a besoin de créer des fichiers de sauvegardes mais aussi de lire continuellement des fichiers (les images ou les vidéos notamment) s'il veut fonctionner correctement.

C'est donc aux fichiers que nous allons nous intéresser dans ce chapitre. Vous allez voir, cela ne sera pas très compliqué, car vous disposez déjà de la plupart des notions nécessaires. Il y a juste quelques points à comprendre avant de pouvoir nous amuser. ;)

Ouvrir / Fermer un fichier texte

Nous allons voir plusieurs types de fichiers utilisables en Ada. Le premier type est le fichier texte. La première chose à faire lorsque l'on veut manipuler un fichier, c'est de l'ouvrir ! :p Cette remarque peut paraître idiote et pourtant : toute modification d'un fichier existant ne peut se faire que si votre programme a préalablement ouvert le fichier concerné. Et bien entendu, la dernière chose à faire est de le fermer.

Package nécessaire

Le package à utiliser, vous le connaissez déjà : Ada.Text_IO ! Le même que nous utilisions pour afficher du texte ou en saisir sur l'écran de la console. En fait, la console peut être considérée comme une sorte de fichier qui s'ouvre et se ferme automatiquement.

Le type de l'objet

Comme toujours, nous devons déclarer nos variables ou nos objets avant de pouvoir les manipuler. Nous allons créer une variable composite de type fichier qui fera le lien avec le vrai fichier texte :

1
MonFichier : File_type ;

Fermer un fichier

Pour fermer un fichier, rien de plus simple, il suffit de taper l'instruction suivante (toujours entre BEGIN et END, bien sûr) :

1
close(MonFicher) ;

Euh… avant de le fermer, ne vaudrait-il pas mieux l'ouvrir ? :o

En effet ! :D Si vous fermez un fichier qui n'est pas ouvert, alors vous aurez droit à cette magnifique erreur :

1
raised ADA.IO_EXCEPTIONS.STATUS_ERROR : file not open

Il faut donc ouvrir avant de fermer ! Mais je commence par la fermeture car c'est ce qu'il y a de plus simple.

Ouvrir un fichier

Il y a une subtilité lorsque vous désirez ouvrir un fichier, je vous demande donc de lire toute cette partie et la suivante en me faisant confiance. Pas de questions, j'y répondrai par la suite. Il faut distinguer deux cas pour ouvrir un fichier :

  • Soit ce fichier existe déjà.
  • Soit ce fichier n'existe pas encore.

Ouvrir un fichier existant

Voici la spécification de l'instruction permettant d'ouvrir un fichier existant :

1
2
3
procedure open(File in out file_type ;
               Mode in File_Mode := Out_File ;
               Name in string) ;

File est le nom de la variable fichier : ici, nous l'avons appelé MonFichier. Nous reviendrons dans la partie suivante sur Mode, mais pour l'instant nous allons l'ignorer vu qu'il est déjà prédéfini. Name est le nom du fichier concerné, extension comprise. Par exemple, si notre fichier a l'extension .txt et s'appelle enregistrement, alors Name doit valoir "enregistrement.txt". Nous pouvons ainsi taper le code suivant :

1
2
3
4
5
6
7
8
9
WITH Ada.Text_IO ; 
USE Ada.Text_IO ; 

PROCEDURE TestFichier IS
   MonFichier : File_type ; 
BEGIN 
   open(MonFichier,Name => "enregistrement.txt") ; 
   close(MonFichier) ; 
END TestFichier ;

Seul souci, si le compilateur ne voit aucune erreur, la console nous affiche toutefois ceci :

1
raised ADA.IO_EXCEPTIONS.NAME_ERROR : enregistrement.txt: No error

Le fichier demandé n'existe pas. Deux possibilités :

  • Soit vous le créez «à la main» dans le même répertoire que votre programme.
  • Soit vous lisez la sous-sous-partie suivante. :p

Créer un fichier

Voici la spécification de l'instruction permettant la création d'un fichier :

1
2
3
procedure Create(File in out File_Type;
                 Mode in File_Mode := Out_File;
                 Name in String);

Elle ressemble beaucoup à la procédure open(). Et pour cause, elle fait la même chose (ouvrir un fichier) à la différence qu'elle commence par créer le fichier demandé. Nous pouvons donc écrire le code suivant :

1
2
3
4
5
6
7
8
9
WITH Ada.Text_IO ; 
USE Ada.Text_IO ; 

PROCEDURE TestFichier IS
   MonFichier : File_type ; 
BEGIN 
   create(MonFichier,Name => "enregistrement.txt") ; 
   close(MonFichier) ; 
END TestFichier ;

Et cette fois pas d'erreur après compilation ! Ça marche !

Euh… ça marche, ça marche… c'est vite dit. Il ne se passe absolument rien ! Je pensais que mon programme allait ouvrir le fichier texte et que je le verrais à l'écran?!

Non ! Ouvrir un fichier permet seulement au logiciel de le lire ou de le modifier (au logiciel seulement). Pour que l'utilisateur puisse voir le fichier, il faut qu'il ouvre un logiciel (le bloc-note par exemple) qui ouvrira à son tour et affichera le fichier demandé.

Dans l'exemple donné ci-dessus, notre programme ouvre un fichier, et ne fait rien de plus que de le fermer. Pas d'affichage, pas de modification… Nous verrons dans ce chapitre comment modifier le fichier, mais nous ne verrons que beaucoup plus tard comment faire un bel affichage à la manière des éditeurs de texte (dernière partie du cours).

Alors deuxième question : que se passe-t-il si le fichier existe déjà ?

Eh bien testez ! C'est la meilleure façon de le savoir. Si vous avez déjà lancé votre programme, alors vous disposez d'un fichier "enregistrement.txt". Ouvrez-le avec NotePad par exemple et tapez quelques mots dans ce fichier. Enregistrez, fermez et relancez votre programme. Ouvrez à nouveau votre fichier : il est vide !

La procédure open() se contente d'ouvrir le fichier, mais celui-ci doit déjà exister. La procédure create() crée le fichier, quoiqu'il arrive. Si un fichier porte déjà le même nom, il est supprimé et remplacé par un nouveau, vide.

Détruire un fichier

Puisqu'il est possible de créer un fichier, il doit être possible de le supprimer ?

Tout à fait. Voici le prototype de la procédure en question :

1
procedure Delete (File in out File_Type);

Attention ! Pour supprimer un fichier, vous devez auparavant l'avoir ouvert !

Le paramètre Mode

Nous avons laissé une difficulté de côté tout à l'heure : le paramètre Mode. Il existe trois modes possibles pour les fichiers en Ada (et dans la plupart des langages) :

  • Lecture seule : In_File
  • Écriture seule : Out_File
  • Ajout : Append_File

Ça veut dire quoi ?

Lecture seule

En mode Lecture seule, c'est-à-dire si vous écrivez ceci :

1
Open(MonFichier,In_File,"enregisrement.txt") ;

Vous ne pourrez que lire ce qui est écrit dans le fichier, impossible de le modifier. C'est un peu comme si notre fichier était un paramètre en mode IN.

Écriture seule

En mode Écriture seule, c'est-à-dire si vous écrivez ceci :

1
Open(MonFichier,Out_File,"enregisrement.txt") ;

Vous ne pourrez qu'écrire dans le fichier à partir du début, en écrasant éventuellement ce qui est déjà écrit, impossible de lire ce qui existe déjà. C'est un peu comme si notre fichier était un paramètre en mode OUT.

Ajout

Le mode Ajout, c'est-à-dire si vous écrivez ceci :

1
Open(MonFichier,Append_File,"enregisrement.txt") ;

Vous pourrez écrire dans le fichier, comme en mode Out_File. La différence, c'est que vous n'écrirez pas en partant du début, mais de la fin. Vous ajouterez des informations à la fin au lieu de supprimer celles existantes.

Et il n'y aurait pas un mode IN OUT des fois ?

Eh bien non. Il va falloir faire avec. D'ailleurs, même Word ne peut guère faire mieux : soit il écrit dans le fichier au moment de la sauvegarde, soit il le lit au moment de l'ouverture. Les autres opérations (mise en gras, changement de police, texte nouveau…) ne sont pas effectuées directement dans le fichier mais seulement en mémoire vive et ne seront inscrites dans le fichier que lorsque Word les enregistrera.

Opérations sur les fichiers textes

Une fois notre fichier ouvert, il serait bon de pouvoir effectuer quelques opérations avec. Attention, les opérations disponibles ne seront pas les mêmes selon que vous avez ouvert votre fichier en mode In_File ou en mode Out_File/Append_File !

Mode lecture seule : In_File

Saisir un caractère

Tout d'abord vous allez devoir tout au long de ce chapitre imaginez votre fichier et l'emplacement du curseur (voir la figure suivante):

À défaut de voir votre fichier texte et le curseur, vous devrez les imaginer

Dans l'exemple précédent, vous pouvez vous rendre compte que mon curseur se trouve à la seconde ligne, vers la fin du mot «situé». Ainsi, si je veux connaître le prochain caractère, je vais devoir utiliser la procédure get() que vous connaissez déjà, mais de manière un peu différente :

1
procedure Get (File in File_Type; Item out Character);

Comme vous pouvez vous en rendre compte en lisant ce prototype, il ne suffira pas d'indiquer entre les parenthèses dans quelle variable de type character vous voulez enregistrer le caractère saisi, il faudra aussi indiquer dans quel fichier ! Comme dans l'exemple ci-dessous :

1
Get(MonFichier,C) ;

Une fois cette procédure utilisée, la variable C vaudra 'é' (il y aura sûrement un problème avec le caractère latin) et le curseur sera déplacé à la fin du mot « situé », c'est-à-dire entre le 'é' et l'espace vide ' ' !

Autre remarque d'importance : si vous êtes en fin de ligne, en fin de page ou en fin de fichier, vous aurez droit à un joli message d'erreur ! Le curseur ne retourne pas automatiquement à la ligne. Pour pallier à cela, deux possibilités. Première possibilité : vous pouvez utiliser préalablement les fonctions suivantes :

1
2
3
function End_Of_Line (File in File_Type) return Boolean;
function End_Of_Page (File in File_Type) return Boolean;
function End_Of_File (File in File_Type) return Boolean;

Elle renvoient TRUE si vous êtes, respectivement, en fin de ligne, en fin de page ou en fin de fichier. Selon les cas, vous devrez alors utiliser ensuite les procédures :

1
2
3
procedure Skip_Line (File in File_Type; Spacing in Positive_Count := 1);
procedure Skip_Page (File in File_Type);
procedure Reset  (File in out File_Type);

Skip_line(MonFichier) vous permettra de passer à la ligne suivante (nous l'utilisions jusque là pour vider le tampon). Il est même possible de lui demander de sauter plusieurs lignes grâce au paramètre Spacing en écrivant Skip_Line(MonFichier,5). Skip_Page(MonFichier) vous permettra de passer à la page suivante. Reset(MonFichier) aura pour effet de remettre votre curseur au tout début du fichier. D'où le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
loop
   if end_of_line(MonFichier) 
      then skip_line(MonFichier) ; 
   elsif end_of_page(MonFichier)
      then skip_page(MonFichier) ; 
   elsif end_of_file(MonFichier)
      then reset(MonFichier) ;
      else get(C) ; exit ;
   end if ;
end loop ;

Cette boucle permet de traiter les différents cas possibles et de recommencer tant que l'on n'a pas saisi un vrai caractère ! Attention, si le fichier est vide, vous risquez d'attendre un moment :p Passons à la deuxième possibilité :

1
2
3
procedure Look_Ahead(File        in File_Type;
                     Item        out Character;
                     End_Of_Line out Boolean);

Cette procédure ne fait pas avancer le curseur mais regarde ce qui se trouve après : si l'on se trouve en fin de ligne, de page ou de fichier, le paramètre End_Of_Line vaudra true et le paramètre Item ne vaudra rien du tout. Dans le cas contraire, End_Of_Line vaudra false et Item aura la valeur du caractère suivant. Je le répète, le curseur n'est pas avancé dans ce cas là ! Voici un exemple avec une variable fin de type boolean et une variable C de type Character.

1
2
3
4
5
Look_Ahead(MonFichier,C,fin);
if not fin
   then put("Le caractère vaut :") ; put(C) ; 
   else put("C'est la fin des haricots !") ; 
end if ;

Saisir un string

Pour saisir directement une ligne et pas un seul caractère, vous pourrez utiliser l'une des procédures dont voici les prototypes :

1
2
3
procedure Get (File in File_Type; Item out String);
procedure Get_Line(File in File_Type ; Item out String ; Last out Natural);
function Get_Line(File in File_Type) return string ;

La procédure Get() saisit tout le texte présent dans l'objet-fichier File et l'enregistre dans le string Item (le curseur se trouve alors à la fin du fichier). La procédure get_line() saisit tout le texte situé entre le curseur et la fin de la ligne et l'affecte dans le string Item ; elle renvoie également la longueur de la chaîne de caractère dans la variable Last (le curseur sera alors placé au début de la ligne suivante). Enfin, la fonction Get_line se contente de renvoyer le texte entre le curseur et la fin de ligne (comme la procédure, seule l'utilisation et l'absence de paramètre last diffèrent).

Mais nous nous retrouvons face à un inconvénient de taille : nous ne connaissons pas à l'avance la longueur des lignes et donc la longueur des strings qui seront retournés. À cela, deux solutions : soit nous utilisons des strings suffisamment grands en priant pour que les lignes ne soient pas plus longues (et il faudra prendre quelques précautions à l'affichage), soit nous faisons appel aux unbounded_strings :

  • Première méthode :
1
2
3
4
5
6
7
8
9
...
   S : string(1..100) ;
   MonFichier : file_type ;
   long : natural ;
BEGIN
   open(MonFichier, In_File, "enregistrement.txt") ; 
   get_line(MonFichier, S, long) ; 
   put_line(S(1..long)) ; 
   ...
  • Seconde méthode:
1
2
3
4
5
6
7
8
...
   txt : unbounded_string ;
   MonFichier : file_type ;
BEGIN
   open(MonFichier, In_File, "enregistrement.txt") ; 
   txt:=To_Unbounded_String(get_line(MonFichier)) ; 
   put_line(To_String(txt)) ; 
   ...

Mode écriture : Out_File / Append_File

Rappel : en mode Append_File, toute modification se fera à la fin du fichier ! En mode Out_File, les modifications se feront à partir du début, au besoin en écrasant ce qui était déjà écrit.

Vous devriez commencer à vous douter des opérations d'écriture applicables aux fichiers textes, car , à-dire-vrai, vous les avez déjà vues. Voici quelques prototypes :

1
2
3
4
5
procedure New_Line (File in File_Type; Spacing in Positive_Count := 1);
procedure New_Page (File in File_Type);
procedure Put (File in File_Type; Item in Character);
procedure Put (File in File_Type; Item in String);
procedure Put_Line(File in File_Type; Item in String);

Vous devriez dors et déjà vous doutez de ce que font ces instructions. New_line() insère un saut de ligne dans notre fichier à l'emplacement du curseur (par défaut, mais le paramètre spacing permet d'insérer 2, 3, 4… sauts de lignes). New_Page() insère un saut de page. Les procédures Put() permettent d'insérer un caractère ou toute une chaîne de caractères. La procédure put_line() insère une chaîne de caractères et un saut de ligne.

Autres opérations

Il existe également quelques fonctions ou procédures utilisables quels que soient les modes ! En voici quelques-unes pour se renseigner sur notre fichier :

  • Pour connaître le mode dans lequel un fichier est ouvert (In_File, Out_File, Append_File). Permet d'éviter des erreurs de manipulation.
1
function Mode (File in File_Type) return File_Mode;
  • Pour connaître le nom du fichier ouvert.
1
function Name (File in File_Type) return String;
  • Pour savoir si un fichier est ouvert ou non. Pour éviter de fermer un fichier déjà fermé, ou d'ouvrir un fichier déjà ouvert, etc.
1
function Is_Open (File in File_Type) return Boolean;

Et en voici quelques autres pour déplacer le curseur ou se renseigner sur sa position. Pour information, le type Positive_Count sont en fait des natural mais 0 en est exclu (les prototypes ci-dessous sont écrits tels qu'ils le sont dans le package Ada.Text_IO).

  • Pour placer le curseur à une certaine colonne ou à une certaine ligne.
1
2
procedure Set_Col (File in File_Type;  To in Positive_Count);
procedure Set_Line (File in File_Type; To in Positive_Count);
  • Pour connaître la colonne, la ligne ou la page où se situe le curseur.
1
2
3
function Col (File in File_Type) return Positive_Count;
function Line (File in File_Type) return Positive_Count;
function Page (File in File_Type) return Positive_Count;

Les fichiers binaires séquentiels

On ne peut manipuler que des fichiers contenant du texte ? Comment je fais si je veux enregistrer les valeurs contenues dans un tableau par exemple ? Je dois absolument utiliser les attributs 'image et 'value ?

Non, le fichier texte n'est pas obligatoire, mais il a l'avantage d'être modifiable par l'utilisateur grâce à un simple éditeur de texte, type Notepad par exemple. Mais si vous ne souhaitez qu'enregistrer des integer (par exemple), il est possible d'enregistrer vos données dans des fichiers binaires prévus à cet effet.

C'est quoi un fichier binaire ?

C'est un fichier constitué d'une suite de bits (les chiffres 0 et 1, seuls chiffres connus de l'ordinateur). Ça ne vous aide pas beaucoup ? :( Ça tombe bien car ces fichiers contiennent une suite d'informations illisibles par l'utilisateur. Pour pouvoir lire ces données, il faut faire appel à un logiciel qui connaitra le type d'informations contenues dans le fichier et qui saura comment les traiter. Par exemple :

  • Les fichiers MP3, sont illisibles par le commun des mortels. En revanche, en utilisant un logiciel tel VLC ou amarok, vous pourrez écouter votre morceau de musique.
  • Les fichiers images (.bmp, .tiff, .png, . jpg, .gif…) ne seront lisibles qu'à l'aide d'un logiciel comme photofiltre par exemple.
  • On pourrait encore multiplier les exemples, la plupart des fichiers utilisés aujourd'hui étant des fichiers binaires.

Parmi ces fichiers binaires, il en existe deux sortes : les fichiers séquentiels et les fichiers à accès direct. La différence fondamentale réside dans la façon d'accéder aux données qu'ils contiennent. Les premiers sont utilisables comme les fichiers textes (qui sont eux-même des fichiers séquentiels), c'est pourquoi nous allons les aborder de ce pas. Les seconds sont utilisés un peu différemment, et nous les verrons dans la sous-partie suivante.

Nous allons créer un programme Enregistrer qui, comme son nom l'indique, enregistrera des données de type integer dans un fichier binaire à accès séquentiel. Nous devons d'abord régler le problème des packages. Celui que nous allons utiliser ici est Ada.sequential_IO. Le souci, c'est qu'il est fait pour tous les types de données (float, natural, array, string, integer, character…), on dit qu'il est générique (nous aborderons cette notion dans la quatrième partie de ce cours). La première chose à faire est donc de créer un package plus spécifique :

1
2
3
4
5
6
7
WITH Ada.Integer_Text_IO ;         USE Ada.Integer_Text_IO ; 
WITH Ada.Sequential_IO ; 

PROCEDURE Enregistrer IS
   PACKAGE P_integer_file IS NEW Ada.Sequential_IO(integer) ; 
   USE P_integer_file ; 
   ...

Comme vous pouvez le remarquer, Ada.Sequential_IO ne bénéficie pas d'une clause USE (du fait de sa généricité) et doit être «redéclaré» plus loin : nous créeons un package P_integer_file qui est en fait le package Ada.Sequential_IO mais réservé aux données de type integer. Nous pouvons ensuite écrire notre clause de contexte « USE P_integer_file ». Nous avons déjà vu ce genre de chose durant notre premier TP pour créer des nombres aléatoires. Je ne vais pas m'étendre davantage sur cette notion un peu compliquée car nous y reviendrons plus tard. Notez tout de même la nomenclature : je commence mon nom de package par un P_ pour mieux le distinguer des types ou des variables.

Nous allons ensuite déclarer notre variable composite fichier, notre type T_Tableau et notre variable T de type T_Tableau. Je vous laisse le choix dans la manière de saisir les valeurs de T. Puis, nous allons devoir créer notre fichier, l'ouvrir et le fermer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
   MonFichier : File_Type ;
   type T_Tableau is array(1..5,1..5) of integer ;
   T : T_Tableau ; 
BEGIN
   -----------------------------------------
   -- Saisie libre de T (débrouillez-vous)--
   -----------------------------------------

   create(MonFichier,Out_File,"sauvegarde.inf") ; 

   -------------------------------
   -- enregistrement (vu après) --
   -------------------------------

   close(MonFichier) ; 
END Enregistrer ;

Comme je vous l'ai dit, l'encart « Saisie libre de T » est à votre charge (affectation au clavier, aléatoire ou prédéterminée, à vous de voir). La procédure create() peut être remplacée par la procédure open(), comme nous en avions l'habitude avec les fichiers textes. Les modes d'ouverture (In_File, Out_File, Append_File) sont également les mêmes. Concentrons-nous plutôt sur le deuxième encart : comment enregistrer nos données ? Les procédures put_line() et put() ne sont plus utilisables ici ! En revanche, elles ont une «sœur jumelle» : write() ! Traduction pour les anglophobes : «écrire». Notre second encart va donc pouvoir être remplacé par ceci :

1
2
3
4
5
6
7
...
   for i in T'range(1) loop
      for j in T'range(2) loop
         write(MonFichier,T(i,j)) ; 
      end loop ; 
   end loop ; 
...

Compiler votre code et exécutez-le. Un fichier « sauvegarde.inf » doit avoir été créé. Ouvrez-le avec le bloc-notes : il affiche des symboles bizarres, mais rien de ce que vous avez enregistré ! Je vous rappelle qu'il s'agit d'un fichier binaire ! Donc il est logique qu'il ne soit pas lisible avec un simple éditeur de texte. Pour le lire, nous allons créer, dans le même répertoire, un second programme : Lire. Il faudra donc «redéclarer» notre package P_integer_file et tout et tout. ;)

Pour la lecture, nous ne pouvons, bien entendu, pas non plus utilisé get() ou get_line(). Nous utiliserons la procédure read() (traduction : lire) pour obtenir les informations voulues et la fonction Enf_Of_File() pour être certains de ne pas saisir de valeur une fois rendu à la fin du fichier (à noter que les fins de ligne ou fins de page n'existe pas dans un fichier binaire).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
WITH Ada.Integer_Text_IO ;         USE Ada.Integer_Text_IO ; 
WITH Ada.Sequential_IO ;  

PROCEDURE Lire IS
   PACKAGE P_integer_file IS NEW Ada.Sequential_IO(integer) ; 
   USE P_integer_file ; 
   MonFichier : File_Type ;          
   type T_Tableau is array(1..5,1..5) of integer ;
   T : T_Tableau ; 
BEGIN
   open(MonFichier,In_File,"sauvegarde.inf") ; 
   for i in T'range(1) loop
      for j in T'range(2) loop
         exit when Enf_Of_File(MonFichier) ; 
         read(MonFichier,T(i,j)) ; 
         put(T(i,j)) ; 
      end loop ; 
   end loop ;
   close(MonFichier) ; 
END Lire ;

Les procédures et fonctions delete(), reset(), Name(), Mode() ou Is_Open() existent également pour les fichiers séquentiels.

Voilà, nous avons dors et déjà fait le tour des fichiers binaires à accès séquentiel. Le principe n'est guère différent des fichiers textes (qui sont, je le rappelle, eux aussi à accès séquentiels), retenez simplement que put() est remplacé par write(), que get() est remplacé par read() et qu'il faut préalablement spécifier le type de données enregistrées dans vos fichiers. Nous allons maintenant attaquer les fichiers binaires à accès direct. :pirate:

Les fichiers binaires directs

Les fichiers binaires à accès directs fonctionnent à la manière d'un tableau. Chaque élément enregistré dans le fichier est doté d'un numéro, d'un indice, comme dans un tableau. Tout accès à une valeur, que ce soit en lecture ou en écriture, se fait en indiquant cet indice. Première conséquence et première grande différence avec les fichiers séquentiels, le mode Append_File n'est plus disponible. En revanche, un mode InOut_File est enfin disponible !

Nous allons enregistrer cette fois les valeurs contenues dans un tableau de float unidimensionnel. Nous utiliserons donc le package Ada.Direct_IO qui, comme Ada.Sequential_IO, est un package générique (fait pour tous types de données). Il nous faudra donc créer un package comme précédemment avec P_integer_file (mais à l'aide de Ada.Direct_IO cette fois). Voici le code de ce programme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
WITH Ada.Float_Text_IO ;      USE Ada.Float_Text_IO ; 
WITH Ada.Direct_IO ;

PROCEDURE FichierDirect IS
   PACKAGE P_float_file IS NEW Ada.Direct_IO(Float) ; 
   USE P_float_file ; 
   MonFichier : File_Type ; 

   TYPE T_Vecteur IS ARRAY(1..8) OF Float ; 
   T : T_Vecteur := (1.0,2.0,4.2,6.5,10.0,65.2,5.3,101.01); 
   x : float ; 
BEGIN
   create(MonFichier,InOut_File,"vecteur.inf") ; 

   for i in 1..8 loop
      write(MonFichier,T(i),count(i)) ; 
      read(MonFichier,x,count(i)) ; 
      put(x) ; 
   end loop ; 

   close(MonFichier) ; 
END FichierDirect ;

Le troisième paramètre des procédures write() et read() est bien entendu l'indice de la valeur lue ou écrite. Toutefois, cet indice est de type Count (une sorte de type natural) incompatible malheureusement avec le type de la variable i (integer). C'est pourquoi nous sommes obligés d'écrire count(i) pour la convertir en variable de type Count.

Là encore, si vous tentez d'ouvrir votre fichier « vecteur.inf », vous aurez droit à un joli texte incompréhensible. Remarquez que l'instruction « write(MonFichier,T(i),count(i)) » pourrait être remplacée par les deux lignes suivantes :

1
2
set_index(MonFichier,count(i)) ; 
write(MonFichier,T(i)) ;

En effet, l'instruction set_index() place le curseur à l'endroit voulu, et il existe une deuxième procédure write() qui ne spécifie pas l'indice où vous souhaitez écrire. Cette procédure écrit donc là où se trouve le curseur. Et il en est de même pour la procédure Read().

Comme pour un tableau, vous ne pouvez pas lire un emplacement vide, cela générerait une erreur ! C'est là l'inconvénient des fichiers binaires à accès directs.

Les répertoires

Peut-être avez-vous remarqué que, comme pour les packages, les fichiers que vous créez ou que vous pouvez lire se situent tous dans le même répertoire que votre programme. Si jamais vous les déplacez, votre programme ne les retrouvera plus. Pourtant, lorsque vous mènerez des projets plus conséquents (comme les TP de ce tutoriel ou bien vos propres projets), il sera préférable de rassembler les fichiers de même types dans d'autres répertoires.

Chemins absolus et relatifs

Vous savez que les répertoires et fichiers de votre ordinateur sont organisés en arborescence. Chaque répertoire est ainsi doté d'une adresse, c'est à dire d'un chemin d'accès. Si votre programme est situé dans le répertoire C:\Rep1\Rep2\Rep3 (j'utilise un chemin windows mais le principe est le même sous les autres systèmes d'exploitation), vous pouvez fournir deux adresses à l'ordinateur : soit cette adresse complète, appelée chemin absolu, soit le chemin à suivre pour retrouver les fichiers à partir de son répertoire courant, appelé chemin relatif.

Arborescence des répertoires

Si votre programme est situé dans le même répertoire que vos fichiers, le chemin relatif sera noté par un point ( . ), que vous soyez sous Windows, Mac ou Linux. Ce point signifie « dans le répertoire courant ». Si vos fichiers sont situés dans le répertoire C:\Rep1\Rep2 (adresse absolue), le chemin relatif sera noté par deux points (..) qui signifient « dans le répertoire du dessous ».

Compliquons la tâche : notre programme est toujours situé dans le répertoire C:\Rep1\Rep2\Rep3 mais les fichiers sont situés dans le répertoire C:\Rep1\Rep2\Rep3\Rep4. L'adresse relative des fichiers est donc .\Rep4. Enfin, si nous déplaçons nos fichiers dans un répertoire C:\Rep1\Rep2\AutreRep, leur chemin relatif sera ..\AutreRep (le programme doit revenir au répertoire du dessous avant d'ouvrir le répertoire AutreRep)

Indiquer le chemin d'accès

Maintenant que ces rappels ont été faits, créons donc un répertoire « files » dans le même répertoire que notre programme et plaçons-y un fichier appelé "Sauvegarde.txt". Comment le lire désormais ? Au lieu que votre programme n'utilise l'instruction suivante :

1
Open(F,In_File,"Sauvegarde.txt") ;

Nous allons lui indiquer le nom du fichier à ouvrir ainsi que le chemin à suivre :

1
2
3
4
5
Open(F,In_File,"./files/Sauvegarde.txt") ; --Avec adresse relative

         --   OU

Open(F,In_File,"C:/Rep1/Rep2/Rep3/files/Sauvegarde.txt") ; --Avec adresse absolue

Le paramètre pour le nom du fichier contient ainsi non seulement son nom mais également son adresse complète.

Gérer fichiers et répertoires

Le programme ne pourrait pas copier ou supprimer les répertoire lui-même ?

Bien sûr que si ! Le langage Ada a tout prévu. Pour cela, vous aurez besoin du package Ada.Directories. Le nom du fichier correspondant est a-direct.ads et je vous invite à le lire par vous-même pour découvrir les fonctionnalités que je ne détaillerai pas ici. Voici quelques instructions pour gérer vos répertoires :

1
2
3
4
5
6
7
8
function Current_Directory return String;
procedure Set_Directory    (Directory : String);

procedure Create_Directory (New_Directory : String);
procedure Delete_Directory (Directory : String);

procedure Create_Path      (New_Directory : String);
procedure Delete_Tree      (Directory : String);

Current_Directory renvoie une chaîne de caractères correspondant à l'adresse du répertoire courant. La procédure Set_Directory permet quant à elle de définir un répertoire par défaut, si par exemple l'ensemble de vos ressources se trouvait dans un même dossier. Create_Directory() et Delete_Directory() vous permettrons de créer ou supprimer un répertoire unique, tandis que Create_Path() et Delete_Tree() vous permettrons de créer toute une chaîne de répertoire ou de supprimer un dossier et l'ensemble de ses sous-dossiers. Des programmes similaires sont disponibles pour les fichiers :

1
2
3
4
procedure Delete_File (Name : String);
procedure Rename      (Old_Name, New_Name : String);
procedure Copy_File   (Source_Name   : String;
                       Target_Name   : String);

Ces procédures vous permettront respectivement de supprimer un fichier, de le renommer ou de le copier. Pour les procédures Rename() et Copy_File(), le premier paramètre correspond au nom actuel du fichier, le second paramètre correspond quant à lui au nouveau nom ou au nom du fichier cible. Voici enfin trois dernières fonctions parmi les plus utiles :

1
2
3
function Size   (Name : String) return File_Size;
function Exists (Name : String) return Boolean;
function Kind   (Name : String) return File_Kind;

Size() renverra la taille d'un fichier ; Exists() vous permettra de savoir si un fichier ou un dossier existe ; Kind() enfin, renverra le type de fichier visé : un dossier (le résultat vaudra alors « Directory »), un fichier ordinaire (le résultat vaudra alors « Ordinary_File ») ou un fichier spécial (le résultat vaudra alors « Special_File »).

Quelques exercices

Exercice 1

Énoncé

Créer un fichier texte appelé "poeme.txt" et dans lequel vous copierez le texte suivant :

Que j'aime à faire apprendre un nombre utile aux sages ! Immortel Archimède, artiste, ingénieur, Qui de ton jugement peut priser la valeur ? Pour moi ton problème eut de pareils avantages.

Jadis, mysterieux, un problème bloquait Tout l'admirable procède, l'oeuvre grandiose Que Pythagore decouvrit aux anciens Grecs. Ô quadrature ! Vieux tourment du philosophe

Insoluble rondeur, trop longtemps vous avez Defie Pythagore et ses imitateurs. Comment intégrer l'espace plan circulaire ? Former un triangle auquel il équivaudra ?

Nouvelle invention : Archimède inscrira Dedans un hexagone ; appréciera son aire Fonction du rayon. Pas trop ne s'y tiendra : Dédoublera chaque élément antérieur ;

Toujours de l'orbe calculée approchera ; Définira limite ; enfin, l'arc, le limiteur De cet inquiétant cercle, ennemi trop rebelle Professeur, enseignez son problème avec zèle

Ce poème est un moyen mnémotechnique permettant de réciter les chiffres du nombre $\pi$ ($\approx 3,14159$), il suffit pour cela de compter le nombre de lettres de chacun des mots (10 équivalant au chiffre 0).

Puis, créer un programme poeme.exe qui lira ce fichier et l'affichera dans la console.

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
with ada.Text_IO, ada.Strings.Unbounded ;
use ada.Text_IO, ada.Strings.Unbounded ;

procedure poeme is
   F : File_Type ;
   txt : unbounded_string ; 
begin
   
   open(F,in_file,"poeme.txt") ;
   
   while not end_of_file(F) loop 
      txt :=to_unbounded_string(get_line(F)) ;
      put_line(to_string(txt)) ; 
   end loop ;
   
   close(F) ; 

end poeme ;

Exercice 2

Énoncé

Plus compliqué, reprendre le problème précédent et modifier le code source de sorte que le programme affiche également les chiffres du nombre $\pi$. Attention ! La ponctuation ne doit pas être comptée et je rappelle que la seule façon d'obtenir 0 est d'avoir un nombre à 10 chiffres !

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
WITH Ada.Text_IO, Ada.Integer_Text_IO, Ada.Strings.Unbounded ;
USE Ada.Text_IO, Ada.Integer_Text_IO, Ada.Strings.Unbounded ;

PROCEDURE Poeme IS
   F   : File_Type;
   Txt : Unbounded_String;

   PROCEDURE Comptage (Txt String) IS
      N : Natural := 0;
   BEGIN
      FOR I IN Txt'RANGE LOOP
         CASE Txt(I) IS
            WHEN ' '                                     => IF N/=0 THEN Put(N mod 10,3) ; N:= 0 ; END IF ;
            WHEN '.' | '?' | ',' | ';' | '!' | ':' | ''' => IF N/=0 THEN Put(N mod 10,3) ; N:= 0 ; END IF ;
            WHEN others                                  => n := n+1 ; 
         END CASE ; 
      END LOOP ;
   END Comptage ;

BEGIN

   Open(F,In_File,"poeme.txt") ;

   WHILE NOT End_Of_File(F) LOOP
      Txt :=To_Unbounded_String(Get_Line(F)) ;
      Put_Line(To_String(Txt)) ;
      Comptage(To_String(Txt)) ; 
      New_line ; 
   END LOOP ;

   Close(F) ;

END Poeme ;

Exercice 3

Énoncé

Nous allons créer un fichier "Highest_Score.txt" dans lequel seront enregistrés les informations concernant le meilleur score obtenu à un jeu (ce code pourra être mis en package pour être réutilisé plus tard). Seront demandés : le nom du gagnant, le score obtenu, le jour, le mois et l'année d'obtention de ce résultat.

Voici un exemple de la présentation de ce fichier texte. Pas d'espaces, les intitulés sont en majuscule suivis d'un signe =. Il y aura 4 valeurs entières à enregistrer et un string.

Résultat attendu

Le programme donnera le choix entre lire le meilleur score et en enregistrer un nouveau. Vous aurez besoin pour cela du package Ada.Calendar et de quelques fonctions :

  • clock : renvoie la date du moment. Résultat de type Time.
  • year(clock) : extrait l'année de la date du moment. Résultat : integer.
  • month(clock) : extrait le mois de la date du moment. Résultat : integer.
  • day(clock) : extrait le jour de la date du moment. Résultat : integer.

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
WITH Ada.Text_IO ;             USE Ada.Text_IO ;
WITH Ada.Integer_Text_IO ;     USE Ada.Integer_Text_IO ; 
WITH Ada.Calendar;             USE Ada.Calendar ; 
WITH Ada.Characters.Handling ; USE Ada.Characters.Handling ; 
WITH Ada.Strings.Unbounded ;   USE Ada.Strings.Unbounded ;

procedure BestScore is

         --------------------
         --Lecture du score--
         --------------------

   procedure read_score(F file_type) is
      txt : unbounded_string ; 
      long : natural ;
      Score, Jour, Mois, Annee : natural ;
      Nom : unbounded_string ; 
   begin
      while not end_of_file(F) loop
         txt:=To_Unbounded_String(get_line(F)) ; 
         long := length(txt) ; 
         if long>=6 
            then if To_String(txt)(1..6)="SCORE=" 
                    then Score := natural'value(To_String(txt)(7..long)) ; 
                 end if ;
                 if To_String(txt)(1..6)="MONTH=" 
                    then Mois := natural'value(To_String(txt)(7..long)) ; 
                 end if ;
         end if ; 
         if long >= 5
            then if To_String(txt)(1..5)="YEAR=" 
                    then Annee := natural'value(To_String(txt)(6..long)) ; 
                 end if ;
                 if To_String(txt)(1..5)="NAME=" 
                    then Nom := to_unbounded_string(To_string(txt)(6..long)) ; 
                 end if ;
         end if ; 
         if long >= 4 and then To_String(txt)(1..4)="DAY=" 
            then Jour := natural'value(To_String(txt)(5..long)) ; 
         end if ; 
      end loop ; 
      Put_line("   " & to_string(Nom)) ; 
      Put("a obtenu le score de ") ; Put(Score) ; New_line ; 
      Put("le " & integer'image(jour) & "/" & integer'image(mois) & "/" & integer'image(annee) & ".") ; 
   end read_score ; 

         ---------------------
         --Écriture du score--
         ---------------------

   procedure Write_Score(F file_type) is
      txt : unbounded_string ; 
      score : integer ; 
   begin
      Put("Quel est votre nom ? ") ; txt := to_unbounded_string(get_line) ; 
      Put_line(F,"NAME=" & to_string(txt)) ; 
      Put("Quel est votre score ? ") ; get(score) ; skip_line ; 
      Put_line(F,"SCORE=" & integer'image(score)) ; 
      Put_line(F,"YEAR=" & integer'image(year(clock))) ; 
      Put_line(F,"MONTH=" & integer'image(month(clock))) ; 
      Put_line(F,"DAY=" & integer'image(day(clock))) ; 
   end Write_Score ; 

         ------------------------------
         --   PROCÉDURE PRINCIPALE   --
         ------------------------------
   c : character ; 
   NomFichier : constant string := "Highest_Score.txt" ; 
   F : file_type ; 
begin
   loop
      Put_line("Que voulez-vous faire ? Lire (L) le meilleur score obtenu,");  
      Put("ou en enregistrer un nouveau (N) ? ") ; 
      get_immediate(c) ; new_line ; new_line ; 
      c := to_upper(c) ; 
      exit when c = 'L' or c = 'N' ; 
   end loop ; 
         
   if c = 'L'
      then open(F,In_File,NomFichier) ; 
           read_score(F) ;
           close(F) ; 
      else create(F,Out_File,NomFichier) ; 
           write_score(F) ;
           close(F) ; 
           open(F,In_File,NomFichier) ; 
           read_score(F) ;
           close(F) ; 
   end if ; 
end BestScore ;

Ce chapitre sur les fichiers vous permettra enfin de garder une trace des actions de vos programmes ; vous voilà désormais armés pour créer des programmes plus complets. Vous pouvez dors et déjà réinvestir ce que vous avez vu et conçu dans ces derniers chapitres pour améliorer votre jeu de craps (vous vous souvenez, le TP) en permettant au joueur de sauvegarder son (ses) score(s), par exemple. Pour le prochain chapitre, nous allons mettre de côté les tableaux (vous les retrouverez vite, rassurez vous :-° ) pour créer nos propres types.


En résumé :

  • Avant d'effectuer la moindre opération sur un fichier, ouvrez-le et surtout, n'oubliez pas de le fermer avant de clore votre programme.
  • Les fichiers séquentiels (que ce soient des fichiers textes ou binaires) ne sont accessibles qu'en lecture ou en écriture, mais jamais les deux en même temps.
  • Il est préférable de ne pas garder un fichier ouvert trop longtemps, on préfère en général copier son contenu dans une ou plusieurs variables et travailler sur ces variables.
  • Avant de saisir une valeur en provenance d'un fichier, assurez-vous que vous ne risquez rien : vous pouvez être en fin de ligne, en fin de page ou en fin de fichier; dans un fichier à accès direct, l'emplacement suivant n'est pas nécessairement occupé !
  • N'hésitez pas à classer vos fichiers dans un sous-dossier par soucis d'organisation. Mais pensez alors à indiquer le chemin d'accès à votre programme, de préférence un chemin relatif.