Introduction à la programmation en Perl

Troisième partie : tables de hachage et fichiers

© 2002 - Sylvain Lhullier
Paru dans LinuxMagazine-France

Chapeau de l'article

Nous poursuivons ici notre série d'articles sur la programmation en Perl. Après avoir passé en revue les principes de base du langage (scalaires, tableaux, structures de contrôle), nous allons aborder des notions qui constituent une partie de la puissance de Perl : les tables de hachage et les fichiers.

Introduction

Les tables de hachage de Perl ne se retrouvent pas dans beaucoup d'autres langages ; pour les avoir souvent utilisées en Perl, il est dur de repasser à des langages qui n'en sont pas pourvus. Les fichiers, quant à eux, se retrouvent dans tous les langages, mais la manière très simple et très puissante de les manipuler font des fichiers une facilité de Perl.

Tables de hachage

Une table de hachage (hash table en anglais) est un type de donnée en Perl permettant d'associer une valeur à une clef. On peut dire d'un tableau (notion abordée la dernière fois) qu'il associe une valeur scalaire à un entier : à la position i (pour i entier), une certaine valeur scalaire est présente. Une table de hachage va nous permettre d'aller au-delà : on pourra faire correspondre une valeur scalaire (comme pour un tableau) à toute chaîne de caractères (plutôt qu'à un entier).

Je peux, par exemple, avoir envie de gérer en Perl un index téléphonique simple : chacun de mes amis a un numéro de téléphone, je veux pouvoir retrouver leur numéro à partir de leur prénom. Je vais donc associer le numéro au prénom :

"Paul"     -> "01.23.45.67.89"
"Virginie" -> "06.06.06.06.06"
"Pierre"   -> "heu ..."

Les prénoms seront les clefs, c'est-à-dire le "point d'entrée" dans la table de hachage (comme les indices numéraux le sont pour les tableaux). Les numéros de téléphone seront les valeurs associées à ces clefs. Il s'agit bien d'une association chaîne de caractères -> scalaire.

Vous l'avez sans doute compris, dans une table de hachage, une clef n'est présente qu'une seule fois et ne peut donc avoir qu'une seule valeur (comme l'élément d'un indice donné d'un tableau). Par contre, une valeur peut être associée à plusieurs clefs.

Déclaration et initialisation

Une variable de type table de hachage se déclare de la sorte :

my %h;

On a alors une table de hachage vide (aucune clef). Il est possible de signaler explicitement que l'on déclare une table de hachage vide :

my %h = ();

Pour donner des valeurs initiales à notre table de hachage, on peut utiliser la syntaxe suivante :

my %h = ( "Paul"     => "01.23.45.67.89",
          "Virginie" => "06.06.06.06.06",
          "Pierre"   => "heu ..." );

Cette dernière table de hachage est déclarée et initialisée avec les clefs Paul, Virginie et Pierre ayant respectivement pour valeurs 01.23.45.67.89, 06.06.06.06.06 et heu ...

Accéder à un élément

Dans une table de hachage %h, on peut accéder à la valeur d'une clef au moyen de la syntaxe suivante : $h{clef} ; par exemple $h{Paul} vaut 01.23.45.67.89. Si la clef comporte d'autres caractères que des lettres, des chiffres et le souligné (underscore en anglais '_'), il faut la délimiter au moyen de simples ou de doubles quotes : $h{"Marie-Pierre"} ou $h{'Marie-Pierre'}

En fait, cette syntaxe force un contexte de chaîne de caractères entre les accolades, ce qui fait qu'un mot simple (bareword en anglais) sera converti silencieusement en chaîne de caractères (même en positionnant l'option -w).

De façon similaire aux tableaux avec l'arobase (@t), la totalité d'une table de hachage se représente au moyen du signe pourcentage (%h), alors qu'une valeur particulière est désignée à l'aide d'un dollar $h{clef}, cette dernière expression étant bien une variable de type scalaire.

Voici quelques exemples de manipulation d'éléments de la table de hachage %h :

$h{Jacques} = "02.02.02.02.02";
print "Tél : $h{Jacques}\n";
$h{'Jean-Paul'} = "03.03.03.03.03";
if( $h{"Jean-Paul"} ne "Heu ..." )
{ ... }

La clef utilisée pour cette syntaxe peut tout à fait être contenue dans une variable scalaire (qui sera évaluée en contexte de chaîne de caractères) :

my $k = "Jacques";
$h{$k} = "02.02.02.02.02";

Elle peut même être une expression plus complexe :

sub f { return "Jac"; }
$h{f().'ques'} = "02.02.02.02.02";

Parcours

Il existe trois fonctions permettant de parcourir une table de hachage. Dans les exemples fournis, nous considérerons que la table %h a été déclarée ainsi :

my %h = ( "Paul"     => "01.23.45.67.89",
          "Virginie" => "06.06.06.06.06",
          "Pierre"   => "heu ..." );

Libre à vous de parcourir vos tables de hachage avec la fonction qui vous convient le mieux.

Autovivification

Sous ce terme barbare se cache une idée simple : si vous tentez de modifier un élément d'une table de hachage qui n'existe pas, il sera créé. S'il est utilisé dans un contexte numérique, il prendra pour valeur initiale zéro. S'il est utilisé dans un contexte de chaîne de caractères, il prendra pour valeur la chaîne vide (depuis Perl 5.6).

Par exemple, considérons une table de hachage qui ne comporte pas la clef hello ; l'expression suivante

$h{hello} .= "après";

associe à la clef hello la valeur chaîne vide puis lui concatène la chaîne "après". De la même façon, l'expression

$h{bye}++;

crée un élément de valeur 1.

Cette propriété d'autovivification est bien pratique dans le cas où l'on ne connaît pas les clefs avant de devoir y accéder. Par exemple nous allons pouvoir compter le nombre d'occurrences des mots dans un texte de manière très simple. Supposons que les mots du texte soient déjà dans un tableau (par exemple en utilisant la fonction qw ; elle ne règle pas les problèmes des ponctuations, des majuscules et des lettres accentuées, mais elle suffira à notre exemple). Nous allons utiliser chaque mot comme une clef de la table et nous allons ajouter 1 à la valeur de cette clef :

my @texte = qw( bonjour vous bonjour );
my %comptage = ();
foreach my $mot ( @texte )
{
   $comptage{$mot}++;
}
while( my ($k,$v) = each(%comptage) )
{
   print "Le mot '$k' est présent $v fois\n";
}

Ce qui donne l'affichage suivant :

Le mot 'vous' est présent 1 fois
Le mot 'bonjour' est présent 2 fois

Existence et suppression d'une clef

À la lecture de ce qui précède, il peut sembler impossible de savoir si un élément d'une table de hachage existe ou non. Rassurez-vous, les auteurs de Perl ont tout prévu :-) L'opérateur exists renvoie vrai si l'élément de table de hachage qu'on lui donne en paramètre existe ; sinon il renvoie faux. Par exemple :

if( exists( $h{hello} ) )
{
   print "La clef 'hello' existe\n";
}

Il est important de noter qu'un test effectué au moyen de l'opérateur defined aurait été possible, mais dangereux. En effet, l'expression defined($h{hello}) est fausse dans deux cas très différents : soit si l'élément n'existe pas, soit si l'élément existe et vaut undef ; elle sera vraie si l'élément existe et ne vaut pas undef. Il est donc impossible de distinguer le cas d'un élément absent et celui d'un élément indéfini (valant undef) avec defined.

Cette distinction entre absent et indéfini peut paraître artificielle dans ce cas (elle peut tout de même être importante dans certaines situations !), mais dans le cas de la suppression d'une clef, il en est tout autrement.

Pour supprimer une clef dans une table de hachage, il faut utiliser l'opérateur delete. L'instruction

delete( $h{hello} );

supprime la clef hello de la table %h si elle existe (si elle n'existe pas, elle ne fait rien). De la même façon que exists est la bonne méthode pour tester l'existence d'un élément, delete est la bonne méthode pour en supprimer un. Le débutant pourrait être tenté d'écrire :

$h{hello} = undef;  # attention!

Ce qui est fort différent car, dans ce cas, la clef hello aura une valeur indéfinie, mais existera toujours ! On la retrouvera, par exemple, dans les parcours effectués au moyen des opérateurs keys, values ou each ; ce qui n'est sans doute pas le but recherché.

Pour résumer, on peut dire que pour tester l'existence d'une clef, il faut utiliser exists et que pour en supprimer une, il faut utiliser delete.

En marge de ces deux fonctions, voici une manière de savoir si une table de hachage est vide ou non (on qualifie de vide une table de hachage qui ne comporte aucune clef). Cette syntaxe utilise la table de hachage en contexte de chaîne de caractères, par exemple de cette façon :

if( %h eq 0 )
{
   print "%h est vide\n";
}

La valeur d'un hachage en contexte scalaire n'a pas d'autre utilisation que celle-ci. En effet, scalar(%A) renvoie une valeur du type 4/8 qui indique le nombre de places (buckets en anglais) utilisées par rapport au nombre total disponibles dans le hachage. Une table vide est une cas particulier, elle renverra 0.

Tables de hachage et listes

On peut facilement passer d'une liste (ou tableau) à une table de hachage et inversement. Voyez, par exemple, le petit programme suivant :

my @t = ("Paul", "01.23.45.67.89", "Virginie",
         "06.06.06.06.06", "Pierre", "heu ...");
my %h = @t;

La première instruction crée un tableau @t initialisé à une liste à 6 éléments. La seconde crée une table de hachage %h initialisée au moyen du précédent tableau. Les valeurs du tableau sont prises deux à deux : la première de chaque couple sera la clef dans la table de hachage, la seconde la valeur. Si le nombre d'éléments de la liste est impair, la dernière clef créée aura undef pour valeur. Si une clef venait à être présente plusieurs fois dans la liste, c'est la dernière valeur qui sera prise en compte dans la table de hachage. On aurait aussi pu écrire :

my %h = ("Paul", "01.23.45.67.89", "Virginie",
         "06.06.06.06.06", "Pierre", "heu ...");

Il est à noter que cette syntaxe rappelle étrangement l'un des premiers exemple de création de table de hachage qui utilisait => pour séparer clefs et valeurs. Cette similarité est en fait une quasi équivalence, car l'opérateur => peut être utilisé à la place de la virgule pour créer des listes ; il n'a été ajouté au langage Perl que pour faciliter la lecture des affectations de tables de hachage, car il force un contexte de chaîne à sa gauche, ce qui permet justement d'écrire %a = ( toto => 'titi' );

La conversion dans l'autre sens est aussi possible. L'évaluation d'une table de hachage dans un contexte de liste renvoie une liste des clefs et des valeurs, se suivant respectivement deux à deux, dans un ordre quelconque entre couples. La table de hachage %h de l'exemple précédent peut être affectée à un tableau :

my @t2 = %h;

Le tableau $t2 sera initialisé, par exemple, avec la liste suivante : ("Pierre", "heu ...", "Paul", "01.23.45.67.89", "Virginie", "06.06.06.06.06") ; chaque clef précède sa valeur, mais l'ordre des couples (clef,valeur) est quelconque (un peu comme pour la fonction each).

Une table de hachage se convertit en liste sans encombre dès qu'elle est en contexte de liste. Je vous laisse deviner ce que fait le code suivant :

foreach my $x (%h)
{
   print "$x\n";
}

La fonction reverse, qui nous a permis d'inverser les listes, peut être employée pour inverser une table de hachage :

%h = reverse(%h);

Les valeurs deviennent les clefs et inversement. Si plusieurs valeurs identiques sont présentes le comportement est imprévisible car, certes, lors de la transformation de liste en table de hachage la dernière valeur compte, mais lors de la transformation de table de hachage en liste l'ordre est quelconque ...

L'association individu - numéro de téléphone est idéale pour illustrer cela :

my %h = ("Paul", "01.23.45.67.89", "Virginie",
         "06.06.06.06.06", "Pierre", "heu ...");
my %quidonc = reverse %h;

On pourra alors retrouver la personne à partir de son numéro de téléphone. Si, par contre, Paul et Virginie avaient eu le même numéro, on ne aurait pas pu prédire quel serait la personne renvoyée.

Exemples

Voici quelques exemples d'utilisation des tables de hachage.

Le premier concerne la variable spéciale %ENV qui contient les variables d'environnement du programme. $ENV{PATH} contient le path, $ENV{HOME} vaut le nom du répertoire personnel de l'utilisateur qui exécute le programme, etc.

Deuxième exemple, les tables de hachage peuvent servir à constituer des tableaux à plusieurs dimensions ; on pourrait en effet imaginer avoir des clefs qui seraient la concaténation des coordonnées dans les n dimensions : dim1:dim2:dim3:...

my %h = ();
foreach my $i (0..4) {
   foreach my $j (-3..10) {
      foreach my $k (130..148) {
         $h{"$i:$j:$k"} = Calcul($i,$j,$k);
      }
   }
}

Nous verrons dans un prochain article qu'il est possible de bâtir de réels tableaux à plusieurs dimensions en utilisant des références.

L'exemple suivant concerne les ensembles ; nous allons utiliser les tables de hachage pour calculer l'union et l'intersection de deux ensembles.

# Voici mes deux ensembles
# Je mets les éléments dans des tableaux
my @ensA = (1, 3, 5, 6, 7, 8);
my @ensB = (2, 3, 5, 7, 9);

# Voici mon union et mon intersection,
# les éléments des ensembles en seront les clefs
my %union = ();
my %inter = ();

# Je mets tous les éléments de A dans l'union :
foreach my $e (@ensA) { $union{$e} = 1; }

# Pour tous les éléments de B :
foreach my $e (@ensB) {

    # S'il est déjà dans l'union, c'est qu'il est
    # dans A : je le mets donc dans l'intersection :
    $inter{$e} = 1 if ( exists( $union{$e} ) );

    # Je le mets dans l'union
    $union{$e} = 1;
}

# Tous les éléments présents dans A ou B
# sont des clefs de la table union.
# Tous les éléments présents dans A et B
# sont des clefs de la table inter.

# Je reconstitue des tableaux à partir
# des tables de hachage (en les triant
# pour l'affichage)
my @union = sort( {$a<=>$b} keys(%union) );
my @inter = sort( {$a<=>$b} keys(%inter) );

print("@union\n");
# affiche : 1 2 3 5 6 7 8 9
print("@inter\n");
# affiche : 3 5 7

Pour le même problème, voici une solution n'utilisant qu'une seule table de hachage, je vous laisse le soin d'en apprécier le principe :

my @ensA = (1, 3, 5, 6, 7, 8);
my @ensB = (2, 3, 5, 7, 9);

my %hash = ();  # Qu'une seule table ...

foreach my $e (@ensA) { $hash{$e}++; }
foreach my $e (@ensB) { $hash{$e}++; }

my @union = sort( {$a<=>$b} keys(%hash) );
my @inter = sort( {$a<=>$b}
                  ( grep { $hash{$_}==2 } keys(%hash) )
                );

print("@union\n");
# affiche : 1 2 3 5 6 7 8 9
print("@inter\n");
# affiche : 3 5 7

La compréhension de cet exemple demande d'avoir assimilé plusieurs notions importantes vues jusqu'ici.

Tranches de tableau et de table de hachage

En Perl, il est possible de manipuler plusieurs éléments d'un tableau ou d'une table de hachage à la fois. Cela s'appelle une tranche (slice en anglais).

Tranches de tableau

Une tranche de tableau est un sous-ensemble des éléments du tableau. Imaginons par exemple un tableau @t duquel nous souhaiterions manipuler les éléments d'indice 4 et 10 ; pour cela nous allons prendre la tranche correspondante de ce tableau : @t[4,10] est une liste à deux éléments qui est équivalente à ($t[4],$t[10]) Quelques explications sur la syntaxe. Tout d'abord, l'expression commence pas une arobase, car il s'agit d'une liste d'éléments ; le dollar est réservé aux scalaires, par exemple $t[4] est un scalaire. Ensuite, comme d'habitude pour les tableaux, les crochets permettent de spécifier les indices. Enfin, l'ensemble des indices est indiqué par une liste d'entiers : @t[2,10,4,3] @t[3..5] @t[fonction()] ...

Une telle tranche est utilisable comme valeur (passage de paramètres, etc) et comme l-value (expression à gauche du signe égal d'affectation) :

@t[4,10] = (4321,"age");

cette instruction affecte 4321 à l'indice 4 du tableau @t et la chaîne age à l'indice 10. On aurait pu écrire

($t[4],$t[10]) = (4321,"age");

Un autre utilisation des tranches de tableau apparaît avec les fonctions qui renvoient une liste. Par exemple la fonction stat prend en paramètre un nom de fichier et renvoie toute sorte d'informations sur le fichier : taille, dates, propriétaire etc. Il est courant d'écrire :

($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
 $atime,$mtime,$ctime,$blksize,$blocks) = stat($filename);

La fonction renvoie une liste qui est affectée aux variables de la liste de gauche. Les tranches peuvent intervenir si seules quelques informations vous intéressent et que vous ne voulez pas déclarer de variables inutiles. Par exemple, si seules les dates de modification (indice 9) et de création (indice 10) vous intéressent, vous pouvez écrire :

($mtime,$ctime) = ( stat($filename) )[9,10];

L'appel à la fonction est placé entre parenthèses et on ne prend que les éléments d'indice 9 et 10 de sa valeur de retour. On a alors une liste à deux éléments, celle-ci est affectée à la liste à gauche du signe égal et donc ces deux éléments sont affectés aux deux variables.

Tranches de table de hachage

De la même façon qu'il existe des tranches pour les tableaux et les listes, il en existe pour les tables de hachage. La sélection s'effectue bien sûr sur les clefs. Par exemple, si %h est une table de hachage, alors @h{'clef1','clef2'} est une liste équivalente à ($h{'clef1'},$h{'clef2'}) Il est ensuite possible d'utiliser cette liste comme bon vous semble (affectation, passage en paramètre, etc).

Une utilisation (assez complexe) des tranches serait indiquée lorsque l'on veut construire automatiquement une liste de valeurs uniques à partir d'un tableau dont on n'est pas sûr que ses valeurs le soient :

# Un tableau avec des valeurs dupliquées :
my @t = qw(hello toto hello vous);

# Déclaration d'une table de hachage :
my %h;

# On prend la tranche de %h dont les clefs
# sont les valeurs du tableau @t
# et on leur associe la valeur undef
@h{@t} = ();

# Les clefs de %h sont donc constituées des
# valeurs de @t, et on est sûr de ne les
# retrouver qu'une seule fois :
@t = keys %h;

Le tableau @t comporte alors une fois et une seule chacun de ses éléments.

Manipulation des fichiers

Pour le moment nous avons écrit des programmes dont les interactions avec leur environnement étaient faibles. Nous allons voir dans cette partie comment manipuler des fichiers en Perl.

Opérateurs sur les noms de fichier

Perl dispose d'opérateurs prenant en paramètre un nom de fichier ; ce nom de fichier doit être un scalaire (une variable ou une constante). Leur valeur de retour est souvent booléenne et quelquefois numérique. Les coutumiers du shell retrouveront de nombreuses options de la commande test.

Voici quelques exemple d'utilisation de ces opérateurs :

my $file = "/usr/doc/perl";
if( -f $file && -w $file ) { .... }
my $taille = -s $file;
my $age = -M $file;

La fonction glob

La fonction glob prend en argument une expression et renvoie une liste de noms de fichiers. Cette liste correspond à l'évaluation de l'expression selon les wildcards du shell.

Par exemple, glob( '*.o' ) renvoie la liste des fichiers du répertoire courant ayant l'extension .o

Notez bien qu'il ne s'agit pas d'une expression régulière (pour ceux qui connaissent ; pour les autres, nous en parlerons plus tard), mais bien d'une expression telle qu'on la donnerait à un shell : ls [A-Z]*.h liste tous les fichiers commençant par une majuscule ayant l'extension .h

Il existe une syntaxe plus concise et au comportement identique à cette fonction : l'expression peut être mise entre chevrons. Les deux lignes suivantes effectuent la même opération :

@l = glob('/usr/include/*.h');
@l = </usr/include/*.h>;

Après l'exécution d'une de ces deux lignes, le tableau @l contient la liste des noms absolus des fichiers d'include pour le C du répertoire /usr/include

Premiers exemples

Voici un premier exemple de manipulation de noms de fichiers :

#!/usr/bin/perl -w
use strict;
foreach my $name ( <*> )
{
   print "$name\n"  if( -l $name );
}

Il affiche les liens symboliques du répertoire courant.

Voici un second exemple :

#!/usr/bin/perl -w
use strict;
foreach my $name ( <.*>, <*> )
{
   next if( ! -d $name );
   next if( ! -w $name );
   print "$name : ". ( -s $name ) ."\n";
}

Ce programme affiche le nom et la taille des sous-répertoires du répertoire courant sur lesquels j'ai les droits d'écriture (y-compris ceux commençant par un point, donc . et ..).

Ouverture de fichier

Pour lire ou écrire dans un fichier, il est nécessaire de l'ouvrir préalablement. La fonction effectuant cette opération en Perl se nomme open et sa syntaxe est la suivante : open( HANDLEexpression )

Le paramètre HANDLE sera l'identifiant du fichier après ouverture (on pourrait parler de descripteur de fichier). C'est ce descripteur qui devra être fourni aux fonctions de lecture et d'écriture pour manipuler le fichier. Pour ne pas rentrer trop dans les détails, un descripteur de fichier se représente dans le code Perl par une simple chaîne de caractères (sans quotes pour le délimiter) ; la convention veut qu'on le mettre toujours en majuscules.

Le paramètre expression est un scalaire (chaîne de caractères) comportant le nom du fichier à ouvrir précédé de zéro, un ou deux caractères indiquant le mode d'ouverture :

Caractère(s) Mode d'ouverture
aucun lecture
< lecture
> écriture (écrasement)
>> écriture (ajout)
+> lecture et écriture (écrasement)
+< lecture et écriture (ajout)

Le habitués du shell retrouveront certaines notations connues.

Par exemple open(FIC1,"<index.html") ouvre le fichier index.html en lecture et open(FIC2,">index.html") l'ouvre en écriture-écrasement (c'est-à-dire que le fichier sera vidé avant que le curseur ne soit placé au début du fichier).

Cette fonction open renvoie une valeur booléenne vrai ou faux indiquant le bon déroulement ou non de l'opération. Il est très important de tester les valeurs de retour des fonctions manipulant les fichiers (cela est vrai quel que soit le langage), car on ne peux jamais être sûr de rien. Voici deux exemples de tests de la valeur de retour d'open :

if( ! open(FIC,">>data.txt") )
{
   exit(1);
}

Dans ce premier exemple, on tente d'ouvrir en ajout un fichier data.txt ; en cas d'impossibilité, on appelle la fonction exit qui met fin au programme (la valeur 1 signale une erreur ; la valeur 0 signalant la fin normal du programme, les autres valeurs sont utilisées pour signaler au shell appelant une erreur). Vous noterez que l'utilisateur qui exécute le programme n'est pas informé de la cause de l'échec ; le programme se termine, tout au plus sait-il qu'il y a eu un problème avec l'ouverture de ce fichier, mais il n'en connaît pas la cause. L'exemple suivant va nous permettre de lui afficher un joli message d'erreur :

open(FIC2,"</tmp/$a") or die("open: $!");

Nous cherchons ici à ouvrir en lecture le fichier dont le nom serait la concaténation de la chaîne /tmp/ et du contenu de la variable $a. La fonction die met fin au programme comme exit le ferait, mais affiche en plus le paramètre qu'on lui passe. En l'occurrence, le paramètre fourni est la chaîne de caractères comportant le nom de la fonction qui cause l'échec (on aurait pu ajouter le nom du fichier) ainsi que la variable $!. En contexte de chaîne de caractères, cette variable magique $! contient le message errno de la dernière erreur survenue, par exemple No such file or directory ou Permission denied etc. L'utilisateur est donc informé de la cause de la fin du programme.

Les descripteurs de fichier sont d'une espèce étrange en Perl, le débutant s'abstiendra de chercher à trop comprendre les mécanismes sous-jacents. Ce que l'on pourrait dire, c'est qu'il n'est pas nécessaire de déclarer un descripteur de fichier, la fonction open valant déclaration.

Lecture, écriture et fermeture de fichier

Une fois un fichier ouvert, il nous est possible d'écrire et/ou de lire dedans (selon le mode d'ouverture) et de le fermer.

La lecture des fichiers texte s'effectue typiquement au moyen de l'opérateur chevrons, cette lecture se faisant ligne par ligne.

$l = <FIC>;

Cette instruction lit la prochaine ligne disponible du fichier FIC. Vous noterez bien que l'opérateur chevrons (diamond operator en anglais) est ici en contexte scalaire. En contexte de liste, il renvoie la liste de toutes les lignes restant dans le fichier :

@t = <FIC>;

Cette instruction 'absorbe' toutes les lignes du fichier dans une liste qui est placée dans le tableau @t.

Pour itérer sur les lignes d'un fichier, il est courant de faire ainsi :

while( defined( $l = <FIC> ) )
{
   chomp $l;
   print "$. : $l\n";
}

La boucle while donne pour valeur à la variable $l une à une toutes les lignes du fichier. La fonction chomp supprime le dernier caractère s'il s'agit d'un retour à la ligne (voir premier article). La variable spéciale $. vaut le numéro de la ligne courant du dernier fichier lu (ici FIC).

Si vous utilisez la variable spéciale omni-présente $_, la construction

while( defined( $_ = <FIC> ) )

peut même s'abréger en :

while( <FIC> )

Pour écrire dans un fichier, nous allons utiliser les fonctions print et printf que nous avons déjà vues. Elles prennent en premier argument le descripteur de fichier :

print( FIC "toto\n" );
printf( FIC "%03d", $i );

Il faut noter qu'il n'y a pas de virgule après le descripteur de fichier (il ne faut pas en mettre !). Les parenthèses sont comme toujours optionnelles autour des arguments, mais permettent de lever certaines ambiguïtés. La fonction printf fonctionne comme printf ou fprintf du C et ne sera donc pas détaillée ici (voir man 3 printf).

Pour fermer un descripteur de fichier (et donc vider les buffers associés), il faut faire appel à la fonction close :

close( FIC );

À noter que si vous réutilisez un descripteur de fichier dans un open sans faire de close au préalable, Perl ne rouspétera pas et fermera consciencieusement le premier fichier avant d'ouvrir le deuxième.

Il existe plusieurs fichiers ouverts automatiquement par Perl dès le lancement du programme :

Discutons un peu de la manipulation de fichiers binaires. Les exemples de lecture de fichiers donnés jusqu'ici ne conviennent pas à de tels fichiers mais plutôt à des fichiers contenant du texte. Vous pouvez, pour cela, utiliser la fonction getc qui renvoie le prochain caractère disponible : $c = getc(FIC); Vous pouvez aussi faire usage de la fonction read qui lit un nombre déterminé de caractères : $tailleLue = read( FIC, $tampon, $tailleALire ); Les données seront placées dans la variable $tampon À la fin du fichier, ou s'il y a un problème, le tampon n'est pas complètement rempli. C'est pour cela que l'on récupère la valeur de retour de read.

Pour écrire des données non textuelles dans un fichier, vous pouvez tout à fait utiliser les fonctions print et printf car les chaînes de caractères de Perl peuvent contenir le caractère de code ASCII zéro. On notera que la fonction write existe, mais n'est pas l'inverse de read.

S'il vous est nécessaire d'utiliser les fonctions d'entrée/sortie bas niveau, voici leurs noms en Perl : sysopen, sysread, syswrite et close.

Deuxième exemple

Voici le prolongement de l'exemple donné pour les tables de hachage. Nous n'allons plus considérer que les mots sont contenus dans un tableau, mais nous allons les extraire d'un fichier.

#!/usr/bin/perl -w
use strict;
open(FILE,"<$filename.txt") or die"open: $!";
my($line,@words,$word,%total); 
while( defined( $line = <FILE> ) )
{
   @words = split( /\W+/, $line );
   foreach $word (@words)
   {
      $mot =~ tr/A-Z/a-z/;
      $total{$word}++;
   }
}
close(FILE);
foreach $word (sort keys %total)
{
   print "$word a été rencontré $total{$word} fois.\n";
}

On effectue une boucle while sur les lignes du fichier. Chaque ligne est alors découpée en mots par la fonction split (\W+ correspond aux suites de caractères non-alphanumériques, nous verrons cela lors d'un prochain article sur les expressions régulières). Chaque mot est mis en minuscules au moyen de l'opérateur tr (que nous expliquerons avec les expressions régulières).

Exécution de commandes avec open

Il est facile en Perl de lancer une commande shell et de récupérer sa sortie standard ou de fournir son entrée standard.

Pour lire la sortie standard d'un programme, il suffit d'utiliser la syntaxe suivante : open(HANDLE,"commande|") par exemple :

open(FIC1,"ls|")
open(FIC2,"df -HT $device|")

Les lignes lues via le descripteur de fichier ainsi créé seront celles que la commande aurait affichées à l'écran si on l'avait lancée depuis un terminal.

La syntaxe open(HANDLE,"|commande") permet de lancer une commande. Les lignes écrites dans le descripteur de fichier constitueront son entrée standard, par exemple :

open(FIC3,"|gzip > $a.gz")
open(FIC4,"|mail robert\@bidochon.org")

Écrire une table de hachage sur disque avec les fichiers DBM

Le format DBM est un format de fichier de hachage (clef/valeur) standardisé. Il existe en dehors de Perl.

En Perl, il nous est possible de manipuler directement une table de hachage en la liant avec un tel fichier : les valeurs de la table de hachage et du fichier sont synchronisées. Pour y accéder nous utiliserons les fonctions dbmopen et dbmclose

Nous allons directement passer à un exemple. Il comporte deux phases : dans la première nous allons créer un fichier DBM comportant un couple clef/valeur, dans la seconde nous utiliserons ce fichier pour retrouver ce couple. Premier script :

my %h;
dbmopen(%h,"data",0644) or die($!);
$h{'prenom'} = 'Larry';
dbmclose(%h) or die($!);

Un ensemble de fichiers data* est alors créé. Ces fichiers sont ensuite exploitables de la sorte :

my %h;
dbmopen(%h,"data",0644) or die($!);
print "$h{'prenom'}\n";
dbmclose(%h) or die($!);

Lors de cet appel à dbmopen la table %h retrouve la valeur qu'elle avait lors du dbmclose du premier script.

Conclusion

N'oubliez pas que la documentation de Perl est très bien faite et est disponible sur votre ordinateur au moyen de la commande perldoc : perldoc perl vous donne accès à la liste des thèmes consultables avec perldoc. Par exemple perldoc perldata vous explique les structures de données en Perl. Pour une fonction particulière, utilisez l'option -f : perldoc -f chomp

Vous avez maintenant en main beaucoup de notions importantes de Perl. À vous de les mettre en oeuvre pour vos propres problèmes. Il n'est pas forcement facile d'exprimer son besoin en termes directement exploitable dans un langage particulier, mais en ayant un peu d'expérience de Perl, vous trouverez vite votre manière de résoudre un problème. Vous allez alors commencer à faire connaissance avec Tim Towtdi (There is more than one way to do it). ;-)