La pratique de Perl: jouons avec les formats

Par Hal Pomeranz traduction Philippe Bereski

Avant que Perl ne devienne un langage de programmation générique, il était PERL : "un Langague Pratique pour l'Extraction de données et l'écriture de Rapports". On trouve des vestiges de l'évolution de Perl depuis ses humbles débuts dans les recoins oubliés du langage. Les formats, par exemple, ont une syntaxe peu commune dans Perl et ils peuvent en général être émulés par d'autres routines (printf en particulier). C'est pourquoi la plus part des personnes commencent par passer les formats lors de leurs premiers contacts avec Perl. Mais si vous avez à écrire de nombreux scripts produisant des rapports d'analyse de gros volumes de de données, les formats peuvent se révéler être de bons outils.

Un simple rapport

Un des premiers programmes utiles que j'ai écrits en Perl était un petit utilitaire qui imprimait l'état de mon compte en banque. L'application lisait un fichier de données contenant tous les mouvements du compte avec leur date et imprimait un rapport proprement présenté ainsi que le solde du compte en banque. Je l'avais initialement écrit en utilisant des instructions printf(), mais quand le j'ai donné à Tom Limoncelli, il me l'a rendu avec des formats à la place. Ainsi modifié, il était plus agréable à lire (mais mon compte en banque était déjà géré).

Je voulais que le fichier d'entrée soit le plus facile possible à saisir, son formatage est donc très simple. La première ligne reprend l'état initial du compte, en centièmes (je n'ai ainsi pas besoin de saisir la virgule des décimales ni à travailler en flottant). Chaque ligne suivante représente un mouvement : quatre champs, séparés par une tabulation, contiennent le numéro du chèque ou le code du mouvement, la date, une description et une valeur (toujours en centièmes). Les dépôts et les versements sont représentés par des nombres négatifs (en général je prélève plus souvent sur mon compte que je ne le crédite). Voici un programme simple qui lit ce fichier et génère le compte rendu de l'état du compte :

     format STDOUT = 
     @<<<<< @>>>> @<<<<<<<<<<<<<<<<<<< $@######.## $@######.## 
     $code, $date,$descript, 	       $amt,       $balance
     .

     open(INP, "transactions") || die "Can't read transactions file\n"; 
     chop($penny_balance = <INP>); 
     while (<INP>) {
          chop; 
          ($code, $date, $descript, $penny_amt) = split(/\t/); 
          $penny_balance -= $penny_amt; 
          $amt = $penny_amt / 100; 
          $balance = $penny_balance / 100; 
          write; 
     } 
     close(INP);
     format top =
     .
     Trans: Date: Description: Montant: Solde:
     ====== ===== ============ ======= ========
     .
Les quatres premières lignes de cet exemple sont la déclaration du format. La première définit son nom. Quand la fonction write() est appelée pour écrire une ligne de données formatées, elle utilise le format dont le nom correspond à celui de la "handle" utilisée pour manipuler le fichier. Dans notre exemple, le programme écrit les données sur la sortie standard. Notez que si aucun format n'est spécifié, Perl utilise STDOUT par défaut, mais il est toujours préférable de nommer explicitement les formats, même pour écrire sur STDOUT.

La seconde ligne donne l'aspect d'une ligne à écrire. Chaque groupe de lettres commençant par un @ spécifie un champ à écrire - tout le reste n'est que du texte (ex: les symboles $ au début de deux montants). Le signe inférieur (<) signifie que le champ doit être justifié à gauche, le signe supérieur(>) que le champ doit être justifié à droite; le signe tube (|) qu'il doit être centré. Les champs numériques sont indiqués par un (#) et un point décimal optionnel. La taille du champ est déterminée par le nombre de ces caractères spéciaux y compris le @ (dans cet exemple, le premier champ fait 6 caractères, le second 5, etc...). De ce fait le format représente de façon synthétique l'exact alignement de la sortie.

La troisième ligne associe une variable à chaque champ. Lorsque la fonction write() est appelée, la valeur courante de la variable est imprimée en utilisant le format spécifié. Les choses sont plus claires à lire si vous écrivez le nom des variables sous leur champ dans dans le format.

La dernière ligne du format est toujours un point isolé sur la ligne. Il termine le format.

La déclaration d'un format peut intervenir n'importe où dans le code. L'exemple ci-dessous en contient deux, l'un avant le code l'autre après. Ceci pour exposer le principe; dans votre propre code, je vous recommande de toujours grouper vos formats au début du script. Si plusieurs formats portant le même nom sont utilisés, seul celui défini en dernier sera pris en compte.

Le format du nom de "top" est systématiquement imprimé en début de chaque page. La variable spéciale $= contient le nombre de lignes d'une page; elle vaut 60 par défaut mais vous pouvez la redéfinir à une valeur plus petite si vous le souhaitez. La variable spéciales $- contient le nombre lignes restantes dans la page. Vous pouvez forcer un saut de page en assignant 0 a $-. Cependant ne mélangez pas l'utilisation des fonctions print(), print() et write() sinon $- ne sera pas correctement décrémentée.

Quelques trucs pas très glorieux

Alors qu'il est possible de définir une en-tête de page, il n'est pas possible de définir un pied de page. Il y a cependant une astuce pour remédier à cette situation. write() utilise le nom de la "handle" pour sélectionner le format qu'elle utilise mais il est possible d'en utiliser un autre en assignant son nom à la variable $~. L'astuce consiste donc à tester le nombre de lignes restant avant la fin de la page et à utiliser un format spécial en lieu et place du pied de page. Voici l'algorithme pour mettre en oeuvre cette astuce :
     format top = 
     Trans: Date: Description: Amount: Balance: 
     ====== ===== ============ ======= ======== 
     . 
     format STDOUT = 
     @<<<<< @>>>> @<<<<<<<<<<<<<<<<<<< $@######.## $@######.## 
     $code, $date,$descript,           $amt,       $balance 

     format footer =
	
     Page @### 
          $% 
     .

     $footer_depth = 2;

     open(INP, "transactions") || die "Can't read transactions file\n"; 
     chop($penny_balance = <INP>); 
     while (<INP>) { 
          chop; 
          ($code, $date, $descript, $penny_amt) = split(/\t/); 
          $penny_balance -= $penny_amt; 
          $amt = $penny_amt / 100; 
          $balance = $penny_balance / 100; 
          write; 
          if ($- == $footer_depth) { 
               $~ = "footer"; 
               write; 
               $~ = "STDOUT"; 
          } 
     } 
     close(INP);
D'abord nous définissons un nouveau format pour le pied de page ainsi qu'une constante globale contenant le nombre de lignes occupées par un pied de page. Le format du pied de page de notre exemple utilise une autre variable spéciale, $%, qui contient le numéro de la page courante (en commençant par 1).

Chaque fois que nous écrivons une nouvelle ligne, nous testons le nombre de lignes restantes dans la page ($-). Lorsqu'il nous reste exactement $footer_depth lignes il est temps d'afficher le pied de page. Nous affectons simplement le nom du format du pied de page (footer) à la variable $~, nous appelons write() puis nous re-affectons le nom du format normal (STDOUT) avant lewrite suivant. Cette ligne sera écrite sur la page suivante après l'en-tête.

Alors que cette méthode fonctionner parfaitement aussi longtemps que write() n'écrit qu'une seule ligne à la fois, l'anticipation de l'arrivée de la fin de la page est plus compliquée quand on utilise des formats de plusieurs lignes. Il est nécessaire d'écrire un peu plus de code dans la boucle while() pour écrire des lignes blanches et le pied de page. Cette question est laissée en exercice au lecteur.

Si vous souhaitez changer l'en-tête - pare exemple pour obtenir une grande entête pour la première page et une plus petite pour les suivantes - vous pouvez utiliser la variable $^. Elle se comporte pour les entêtes de la même façon que se comporte $~ pour les formats. Ne définissez jamais $- ou $^ à un format inexistant ou votre programme sortira en erreur fatale. Si vous ne souhaitez pas d'en tête, de définissez pas du tout le format top, ou définissez le à un format vide.

Les formats sur plusieurs lignes

Examinez deux points importants dans le format l'en-tête des deux exemples. Premièrement il n'y a pas de champ dans la déclaration du format. Il est parfaitement légal de ne pas avoir de variable dans un format bien que ceci n'a certainement de sens que pour une entête.

Deuxièmement, la déclaration du format porte sur plusieurs lignes. Ceci est également légal et chaque ligne peut contenir zéro, un ou plusieurs champs. En général un format multi-ligne alterne la description des champs avec leur valeur.

Le code suivant est un exemple intéressant de formatage multiligne. Je présuppose l'existence d'une fonction mailparse() qui lit sur le standard input une message à la fois. Pour chaque message, mailparse() stocke chaque information de l'en-tête dans un tableau associatif global, %header, indexée par les drapeaux du message (From, To ... ) et toutes les lignes du messages dans une variable scalaire nommée $body. La sortie du message est donnée dans l'exemple. A propos, mon éditeur ne m'a jamais envoyé un tel message: Je l'ai forgé de toutes pièces. Comme tous les auteurs, je suis toujours en avance sur mes dates de livraison. Hum, cette dernière partie est mensongère, mais j'ai bien forgé moi même le message.

     format message = 
     Date: @<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
           $header{`Date'},           $body 
     From: @<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
           $header{`From'},           $body 
     To  : @<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
           $header{`To'},             $body 
     Subj: ^<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
           $header{`Subject'},        $body 
     ~~ ^<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
        $header{`Subject'},        $body 
     . 

     $~ = "message"; 
     while (<STDIN>) { 
          &mailparse(); 
          write; 
     } 


     Date: Tue, 11 Apr 1995 16:39:06    Hal-- What's the status of your Perl
     From: tmd@iwi.com (Tina M. Darmo   article for the upcoming issue of ;login;?
     To  : hal@netmarket.com (Hal Pom   Rob needs to review the article before
     Subj: Your ;login: article is      giving it to Carolyn for typesetting.
           *OVERDUE* 	 	        Please send email soon-- the fate of the
	 	 	 	 	universe is at stake. --Tina
Il y a un certain nombre de nouveaux constructeurs dans le format du message de cet exemple. Tout d'abord nous trouvons des champs qui commencent par ^ au lieu de @. Pour ce type de champ, Perl consommera dans la variable autant de lettres que nécessaire pour le remplir. En en utilisant plusieurs de ce type associé à une variable contenant une longue chaîne, il est possible de l'afficher dans un texte justifié à gauche, comme elle apparaît dans notre exemple avec d'un coté l'en-tête du message et de l'autre le message. Le variable spéciale $: (je promets que c'est la dernière fois que je mentionne une variable spéciale dans cet article) contient l'ensemble des caractères où il est licite de couper un mot; les valeurs par défaut de $: sont \n - (newline, espace et tiret).

Le marqueur spécial ~~ sur la dernière ligne signifie "Continuer d'écrire jusqu'à épuisement de toute les variables" ($body et $header{Subject} dans ce cas). Ceci est utile dans le cas ou vous ne connaissez pas exactement la longueur du texte, mais que vous voulez être certain d'imprimer la totalité de l'information. Vous pouvez placer ~~ n'importe où dans le format, mais il vaut mieux le placer à un endroit bien visible (le début d'une ligne est presque toujours la meilleure place).

Conclusion

J'ai eu affaire à de nombreux programmes Perl comportant de complexes séquences de printf() qui auraient été plus facile à écrire et à relire si l'auteur avait utilisé des formats. Si vous avez à produire rapidement des rapports contenant un nombre important de données tabulées, les formats sont un outil extrêmement puissant.

Traduction de ;login: Vol. 20 No. 3, June 1995.



Dernière édition: 21 mars 1998 phb
Original 11/27/96ah
Back to the original
Retour à l'index