Par Warren MacEvoy wmacevoy@mesastate.edu
L'objet de cet article est de vous présenter un outil que j'ai appelé perlpp, le préprocesseur Perl. Depuis que je l'ai écrit, perlpp n'est disponible dans aucune distribution de Linux. Voyez les ressources pour savoir comment l'obtenir ainsi que les exemples ici décrits.
Perlpp est une version étoffée de cpp, le préprocesseur C. Il peut faire tout ce que fait cpp et plus encore. Il est, par exemple, facile d'introduire la notion de patrons de code dans quelque langage de programmation que ce soit avec perlpp.
L'utilisation de perlpp, le préprocesseur Perl, demande au moins une connaissance rudimentaire de la programmation en Perl, dont une version égale ou supérieure à la 5 doit être installée sur votre système.
C'est un langage tellement utile que presque chaque programmeur devrait en avoir quelques rudiments. Je commencerai par traiter des quelques notions de Perl employés dans les exemples. Si vous êtes déjà à votre aise dans ce domaine, vous pouvez passer directement à la section suivante.
Les variables. Les variables scalaires, qui peuvent recevoir
comme valeur des chaînes de caractères, des entiers ou des nombres en
double précision sont toujours préfixées par un $
, les
listes, de simples listes de scalaires en fait, par un
@
. Toutes les variables sont globales, à moins qu'elles ne
soient précédées d'un my
lors de leur première utilisation à
l'intérieur d'un bloc.
Désignation des chaînes de caractères. En Perl, elles peuvent
apparaître sous trois formes : presque littéralement, entre
apostrophes ('
), avec de l'interpolation, entre guillemets ("
) et de niveau système, entre
apostrophes inversées (`
). Nous reviendrons plus en détails
sur ce point plus tard, mais en gros :
'\n'
est un anti-slash suivi de la
lettre n."i=$i\n"
correspond aux caractères i=
, suivis de la valeur de la
variable $i
, suivis du caractère de nouvelle ligne. En
dialecte Perl, les chaînes entre guillemets sont dites
interpolées.`ls
$dir`
est la sortie correspondant à l'exécution de la commande
ls
avec la valeur de la variable $dir
comme
argument. csh
sous
la forme :
foreach $index (@LIST) { expression1; expression2; .... }
ainsi que celles du C :
for (do-once; check-first-each-time; do-last-each-time ) { expression1; expression2; .... }Ces deux variantes seront utilisées dans les exemples.
En fait, la syntaxe de base de Perl imite celle du C à bien des
égards, ce qui permet à ceux qui programment en C de lire des scripts
Perl assez facilement. Non, j'exagère : un programmeur C peut
écrire du Perl ressemblant à ce langage, et en obtiendra à peu près ce
qu'il en attend. Un programmeur Perl, lui, résoudrait le même problème
d'une manière totalement différente. Ce faisant, il peut arriver à
réaliser quelque chose de difficile à imaginer : du code plus
touffu que celui que l'on écrirait volontiers en C. Si vous ne me
croyez pas, regardez donc un peu les sources de perlpp
, qui
se trouvent être un script Perl.
Perl est beaucoup plus que ce maigre aperçu, mais ces quelques notions devraient vous suffire pour comprendre les exemples. Voyez les ressources pour en savoir davantage à son sujet.
Commençons par discuter de cpp
. Les programmeurs C
apprennent vite que leur code, au moins logiquement, passe par deux
étapes de traduction. La première, le pré-traitement, a recours à des
commandes telles que :
#include <stdio.h>et
#define FOO(x) bar(x)pour traduire le fichier d'entrée hybride C/cpp en un pur fichier C, lequel sera transmis au pur compilateur C. Illustrons :
fichier_d_entree -> cpp -> cc1 -> fichier_objetAlors que le but initial de
cpp
est de pré-traiter des
fichiers source à destination d'un compilateur C (ou C++), on peut
l'utiliser pour d'autres types de fichiers. Par exemple, xrdb
se sert de cpp
pour préparer des fichiers de ressources X11
avant de les charger. cpp est un outil très pratique, mais un
programmeur peut être rapidement confronté à des limitations,
essentiellement en raison de ses capacités réduites en tant que
pré-processeur pour les calculs et le traitement des chaînes de
caractères.
La raison pour laquelle j'ai conçu perlpp
était précisément
de remédier à ces carences pour un problème de calcul scientifique aux
Pacific Northwest National Laboratories, où j'ai rédigé la
partie sur l'équilibre chimique d'un modèle d'aqueduc. Dans le but de
rester compatible avec le reste du modèle, elle devait être écrite en
FORTRAN. Et pour le rester avec les environnements de développement
Linux, Sun et SGI, il fallait que ce soit FORTRAN 77. L'énoncé du
problème était le suivant : étant données les équations
d'équilibre chimique pour un ensemble donné d'espèces, générer
automatiquement un programme efficace et fiable pour résoudre ces équations.
Il fallait donc, à partir d'équations d'équilibre chimique sous une forme symbolique, passer à un fichier de traitement par lot Maple V (une application de mathématiques symboliques) en respectant un patron et en intégrant les résultats de ce dernier dans une bibliothèque de sous-programmes FORTRAN issue de patrons satisfaisant aux exigences du projet.
Cet environnement demandait la création automatique de plusieurs
sortes de programmes à partir de patrons et a tout naturellement
fourni matière à réflexion pour des pré-processeurs efficaces. Bien
qu'il m'ait fallu presque une semaine pour produire la version alpha
de perlpp
, cela a permis de gagner le même laps de temps sur
ce seul projet. Sans l'avoir, venir à bout de ce problème aurait pu
prendre quatre ou cinq semaines de plus. En outre, sans
perlpp
, le projet serait beaucoup plus difficile à
maintenir.
perlpp prend des fichiers en entrée et engendre des scripts perl lesquels, lors de leur exécution, créeront en mieux des fichiers de sortie semblables.
Crée un fichier hello.c.ppp qui contient les lignes
#include <stdio.h> int main() { printf("Hello World!\n"); return 0; }A présent, lancez la commande
perlpp
de la façon suivante :
perlpp -pl hello.c.pppNous reviendrons sur l'option
-pl
plus loin. Si vous allez
jeter un coup d'oeil, vous verrez que perlpp
a créé le
fichier hello.c.pl
, qui contient le script Perl
suivant :
#!/usr/bin/perl print '#include <stdio.h> '; print 'int main() '; print '{ '; print ' printf("Hello World!\\n"); '; print ' return 0; '; print '} ';La teneur de la première ligne pourra varier selon votre configuration. Voyez la section Dépannage si vous rencontrez des problèmes avec ce script.
Le lancement de hello.c.pl
produit le même texte que le
fichier d'entrée originel hello.c.ppp
. De ce fait, on peut
considérer perlpp
comme un moyen abscons et dispendieux du
point de vue des traitements pour copier des fichiers de texte.
L'option -pl
signifie "créer un programme perl". Si vous ne
la spécifiez pas, le programme est simplement exécuté et la sortie
écrite dans hello.c
.
En clair
perlpp hello.c.pppest équivalent à
perlpp -pl hello.c.ppp ./hello.c.pl > hello.c rm hello.c.plà part que le fichier
hello.c.pl
n'est jamais explicitement créé.
Donc, notre premier exemple, hello.c.ppp
, lorsqu'il est
normalement traité par perlpp
, crée une copie de lui-même,
hello.c
. Ce qui ne devrait ni vous enthousiasmer, ni vous
surprendre. Après tout, si vous soumettez à cpp
un fichier de
texte ne contenant aucune directive qui lui est propre, vous devriez
récupérer exactement ce que vous avez fourni au départ.
cpp n'a d'intérêt qu'avec des fichiers d'entrée incluant des
directives. Perlpp, lui, n'est que moyennement intéressant
avec un fichier d'entrée dépourvu de directives perlpp
, car
il engendre un script Perl qui régénère le fichier d'entrée grâce à
des expressions print
. Pour pouvoir en faire plus, il faut
utiliser les directives perlpp
.
Il n'y en a que quatre pour perlpp
, en plus d'une directive
par défaut. Chacune décrit comment une ligne d'entrée donnée doit être
transcrite dans le script Perl.
'
Impression littérale : si le premier caractère
d'une ligne est un '
(apostrophe), générer une expression
print
entre apostrophes (non interpolée). L'exécution de
cette expression produira le restant de la ligne littéralement.
"
Impression interpolée : si le premier
caractère d'une ligne est un "
(guillemet), générer une
expression print
entre apostrophes (interpolée). Pour en
savoir plus sur l'interpolation des chaînes, voyez la page de
man perlop
.
Si locale est utilisé, l'affectation de casse employée par \l, \L, \u et <\U> est tirée du fichier
locale courant. Cf. la page de man
perllocale
.
On notera que \\ (deux barres de fraction inverses) dans une chaîne interpolée sont
traduits en une seule barre de fraction inverse, par conséquent, \\n deviendra \n
dans la sortie. Ceci apparaîtra dans notre prochain exemple.
`
Impression système : si le premier caractère
d'une ligne est un `
(apostrophe inversée), générer une
expression print
encadrée d'apostrophes inverses. Son
exécution provoquera la sortie, d'abord de l'interpolation du restant
de la ligne, comme dans la règle 2 ci-dessus, puis l'exécution du
texte interpolé comme une commande de shell.
Si aucun des caractères !
, '
, "
ou
`
ne débute une ligne, c'est une traduction par défaut qui
intervient :
-qq
, perlpp
traite ces lignes
comme si elles commençaient par une apostrophe simple, c'est à dire la
règle 2, "impression littérale".-qq
, perlpp
les traite comme si
elles commençaient par un guillemet, la règle 3, "Impression
interpolée".Créons un fichier salutations.c.ppp
contenant les
lignes :
#include <stdio.h> int main() { !foreach $s ('Hello World!','Hola Mundo!', 'Ciao!') { " printf("$s\\n"); !} return 0; }Regardons d'abord le script Perl résultant en tapant :
perlpp -pl salutations.c.pppVous trouverez dans
salutations.c.pl
print '#include <stdio.h> '; print 'int main() '; print '{';foreach $s ('Hello World!','Hola Mundo!', 'Ciao!') { print " printf(\"$s\\n\"); ";} print ' return 0; '; print '} ';Regardez attentivement l'expression
print
créée par
l'expression printf
de salutations.c.ppp
:
print " printf(\"$s\\n\"); ";Perlpp se préoccupe d'ajouter des barres de fraction inverses là où c'est nécessaire afin que des apostrophes ne terminent pas prématurément la chaîne. De même pour les autres formes d'expressions encadrées de guillemets ou d'apostrophes engendrées par
perlpp
.
Exécutons ce script avec perlpp
perlpp salutations.c.pppCe qui produit le fichier
salutations.c
,
#include <stdio.h> int main() { printf("Hello World!\n"); printf("Hola Mundo!\n"); printf("Ciao!\n"); return 0; }
Ce dernier exemple utilise perlpp
pour générer un patron pour
des classes de vecteurs de longueur fixe en C++, dans lesquelles les
boucles sont développées. Développer une boucle signifie, par exemple,
remplacer le code
for (int i=0; i<3; ++i) a[i]=i;par
a[0]=0; a[1]=1; a[2]=2;Cela ne change rien quant à l'action du code, il devient simplement plus rapide. Ceci, car la variable d'index n'a pas besoin d'être incrémentée puis comparée entre chaque affectation.
Une telle classe de patron de longueur fixe serait utile, par exemple, dans une bibliothèque graphique où des vecteurs bi- et tri-dimensionnels de types fixés (float, int, double) seraient employés. Tous seraient semblables (d'où une occasion d'utiliser une classe de patron) mais le coût en performance dû au bouclage serait inacceptable.
Ici, perlpp peut aider. Il sera d'abord utilisé pour créer un
programme Perl (grâce à l'option -pl
) depuis un fichier
patron, Point.Template.ppp
. Le script
Point.Template.pl
est conçu pour engendrer différentes
classes de vecteurs de longueur fixe, en fonction des arguments qui
lui sont passés. Grâce à la directive d'impression système par
apostrophe inversée, ce script est ensuite utilisé dans le fichier
source originel, testPoint.cpp.ppp
, pour générer les classes
spécifiques demandées.
Le fichier Point.Template.ppp
est assez long et est
disponible en FTP anonyme comme indiqué dans les ressources. Par
conséquent, je ne m'attacherai qu'aux parties de ce fichier qui
illustrent de façon intéressante la manière d'utiliser
perlpp
.
La première ligne de Point.Template.ppp
digne d'intérêt est
! eval join(";",@ARGV);ce qui, bien sûr, se traduira en expression Perl par
eval join(";",@ARGV);Seul le
!
de tête est supprimé. L'exécution de cette ligne
réunit tous les arguments de ligne de commande du script, séparés par
des points-virgule, et évalue le tout comme une suite d'expressions
Perl. C'est une façon très fruste de traiter des arguments de ligne de
commande, mais cela convient à nos besoins.
Les quelques lignes qui suivent vérifient que la précédente évaluation de ligne de commande a bien défini trois variables essentielles :
$name
: le nom choisi pour la classe$dim
: la dimension du vecteur$type
: le type de vecteurSi elles n'ont pas été définies, le script vous le fera savoir via STDERR et se terminera avec 1 comme code de sortie.
Après ceci, le patron s'occupera de générer la classe désirée. Ce qui commence par
"class $name { "public: !# !# Declare internal array of desired type and size !# " $type a[$dim]; " static const int dim=$dim;Ici,
$name
, $type
et $dim
sont utilisées
pour créer du texte spécifique dans la définition de la classe. En
Perl, #
désigne un commentaire, par conséquent, !#
en est également un en perlpp
.
On peut trouver la première occurence de développement de boucle dans le constructeur de classe par défaut. Les lignes
! for ($i=0; $i<$dim; ++$i) { " a[$i]=0; ! }se traduisent en segment Perl
for ($i=0; $i<$dim; ++$i) { print(" a[$i]=0; "); }Cette boucle est exécutée dans le script Perl en tant que pré-processeur, au sein duquel elle sera développée en une suite d'affectations dans la source de la classe C++. Les boucles sont développées d'une manière identique dans les autres parties de la définition de classe.
Efficacité mise à part, le bloc de code source >perlpp
suivant fournit un constructeur de classe qui serait impossible à
déclarer en utilisant les facilités couramment offertes par les
patrons : un constructeur qui a autant d'arguments que la
dimension de la classe du vecteur devant être construite.
! @arg=(); for ($i=0; $i<$dim; ++$i) { $arg[$i]="$type a$i"; } ! $args=join(',',@arg); ! " $name($args)Si Perl est nouveau pour vous, vous pouvez avoir du mal à comprendre la première ligne. Elle commence par l'affectation de la liste
@arg
à une liste vide, puis elle crée via une boucle les
entrées de $dim
dans @arg
: "$type
a0"
, "$type a1"
, etc. La raison pour
laquelle les éléments de @arg
sont désignés par
$arg[$i]
dans la boucle for est que @arg
,
une fois affectée, fait référence à la variable scalaire en tant
qu'ième entrée de @arg
. Rappelez-vous, les variables
scalaires sont toujours préfixées par un $
--même celles qui
sont enfouies dans une liste.
Après cette déclaration, le constructeur est défini de façon à initialiser le vecteur avec ses arguments :
" { ! for ($i=0; $i<$dim; ++$i) { " a[$i]=a$i; ! } " }Ensuite vient la définition des opérateurs affectés, lesquels sont parfaitement standard. Puis, une autre caractéristique de
perlpp
est mise en évidence :
le code qui permet de définir toutes les assignations des opérateurs
est créé via une structure de boucle :
! foreach $op ("=","+=","-=","*=","/=") { . . # définition de l'assignation d'opérateur $op . ! }Dans la mesure où les assignations d'opérateurs sont en gros définies de la même façon, cette boucle rend l'écriture du patron plus compacte qu'avec les fonctionnalités habituelles. D'où, un développement, une maintenance et un débogage du patron plus rapides.
On trouve ensuite une boucle semblable destinée à définir les divers opérateurs binaires de la classe : addition, soustraction, etc. Toutes ces boucles, réduisent le côté redondant dans l'effort à fournir pour la définition du patron, ce qui est déjà, c'est amusant, un outil de réduction de redondance des tâches. Bon, d'accord, j'avoue, un rien m'amuse.
Le restant du patron déclare et définit trois opérateurs, des
fonctions d'E/S et une multiplication scalaire. Ils font ce pour quoi
ils sont conçus et on n'apprendra rien de plus sur perlpp
en
s'y attardant.
Poursuivons en utilisant Point.Template.ppp
. Il faut tout
d'abord le convertir en script Perl grâce à la commande :
perlpp -pl Point.Template.pppA présent, regardez le fichier source du programme de test,
testPoint.cpp.ppp
. La seule ligne qui nous intéresse est
` ./Point.Template.pl '\$name="FixVect"' '\$dim=2' '\$type="float"'Elle lance le script
Point.Template.pl
fraîchement créé avec les arguments :
$name="FixVect" $dim=2 $type="float"Grâce à eux, le script patron affiche une classe
FixVect
,
représentant des tableaux bi-dimensionnels de flottants, lesquels
seront inclus dans le fichier source testPoint.cpp
via la
directive perlpp
apostrophe inversée.
Engendrer des classes de patrons de cette façon n'est pas totalement
satisfaisant, car les notions de déclaration et de définition de
classe doivent être en principe distinctes. Cependant, on peut le
rectifier en modifiant le fichier patron. En gros, on pourrait
affecter une quatrième variable, $use
, lors de l'appel du
script, laquelle aurait la valeur soit de "declare"
soit de "define"
. Ainsi, en utilisant des clauses
if, le script fournirait la partie définition ou déclaration
de la classe. C'est encore une autre manière de réduire la redondance
d'un patron avec perlpp
.
Je ne veux pas vous laisser avec le sentiment que perlpp
n'est qu'une sorte "d'algorithme de compression". Rassembler les idées
dans un projet simplifie leur maintenance. Le but de perlpp
est d'éviter les "fuites de concept", dans lesquelles plusieurs
parties de fichiers source représentent d'une façon redondante une
idée, ceux-ci devant être maintenus séparément.
Perlpp remplace essentiellement le plutôt rigide (mais simple !) langage de "traitement" de texte cpp par l'équivalent souple (mais complexe) Perl. Beaucoup de programmeurs l'utilisent déjà pour un peu tout et n'importe quoi, de ce fait, en connaître la syntaxe est doublement gratifiant : en tant que tel, et comme puissant macro-langage pour tout langage de programmation.
Si vous ne connaissez pas Perl, alors perlpp
n'est qu'une
bonne raison de plus pour l'apprendre.
perlpp est disponible sous forme d'archive tarée en FTP anonyme sur perlpp-0.5.tar.gz. La distribution inclut la documentation propre à l'installation du paquetage.
Les exemples de cet article se trouvent sur lj-article.tar.gz.
Vous devez disposer d'une version de Perl égale ou supérieure à la 5,
pour pouvoir utiliser perlpp
. On trouve un paquetage de Perl
sous une forme ou sous une autre dans toutes les distributions de
Linux. La page Web
www.perl.org est un point de départ incontournable si vous
voulez en savoir plus sur ce langage.
perlpp est un script Perl qui en génère d'autres. Pour
pouvoir vous en servir, Perl doit être installé, et perlpp
capable de le trouver. Si ce dernier ne fonctionne pas, vérifiez que
ses deux premières lignes sont en accord avec l'emplacement des
exécutables de Perl.
Si elles sont justes, assurez-vous que les droits en exécution sont
bien positionnés pour le script (chmod 755 perlpp
), et que
perlpp
est accessible depuis votre PATH
.
Si vous venez d'installer perlpp
, il se peut que vous deviez
rafraîchir le cache des répertoires du PATH de votre shell avec
hash -r
(si vous utilisez bash) ou rehash
(pour csh).
Merci à la communauté des Linuxiens pour avoir fourni un si fantastique environnement permettant des calculs scientifiques fiables. J'ai beaucoup de mal à retenir mes railleries à chaque fois qu'un de mes collègues essaie de faire quelque chose d'utile sur une machine qui plante tellement souvent qu'il en est arrivé à s'y attendre.
Je remercie également Mike Littlejohn pour avoir testé perlpp
et relu cet article, de même que Karl Castleton, Steve Yabusaki et
Ashok Chilakapati pour m'avoir embauché sur le projet de modélisation
d'aqueduc.
Et enfin, merci aux Pacific Northwest National Laboratories, au
programme de regroupement des Associated Western Universities, et à la
faculté Mesa State College pour m'avoir accordé le temps, les moyens
et l'opportunité de développer perlpp
.
Alors que j'avais laissé tourner ma machine Red Hat 5.0 pour l'été
dans mon bureau de la faculté Mesa State College de Grand Junction,
Colorado, je suis parti pour les Pacific Northwest National
Laboratories de Richland, Washington, où j'ai rêvé perlpp
.
Je me suis servi à distance de ma Linuxette pendant tout l'été : furetage sur le web, e-mail, récupération de vieux fichiers source, utilisation de Emacs, Maple, TeX, Perl ou du compilateur FORTRAN. C'est vrai que j'ai eu recours à ces outils aussi sur les machines du PNNL, mais quelquefois, il manquait une licence ou l'équivalent Linux était meilleur pour ce que je voulais en faire au labo.
Pendant six semaines, j'ai utilisé cette machine distante au moins une fois par jour. Je n'ai eu des difficultés à m'y connecter qu'une seule fois. Passé l'été, j'ai appris que mon bureau du Colorado, qui se trouve dans un bâtiment réhabilité, avait connu plusieurs coupures de courant. Apparemment, à chaque fois, ma machine était repartie sans l'ombre d'un problème, et je ne m'en étais aperçu qu'à l'occasion d'une interrogation pendant une de ces coupures.
En matière de fiabilité et de disponibilité c'est beaucoup mieux que ce à quoi sont confrontés la plupart de mes collègues qui utilisent d'autres systèmes d'exploitation.
Copyright © 1999, Dr. Warren MacEvoy Publié dans le n° 44 de Linux Gazette, Août 1999.
Traduction française de Joël Sagnes.