À ce point de l'exposé et de ses exercices personnels le lecteur sera sans doute arrivé à la conclusion que la programmation est un art dont la difficulté réside moins dans l'impénétrabilité des concepts que dans l'accumulation des objets, le grouillement des processus et la complexité du texte qui les décrit. Dès lors qu'il est possible de diviser ce texte en petits chapitres, beaucoup d'algorithmes se révèlent finalement simples, ou s'ils restent abstrus, du moins leur difficulté se cantonne-t-elle à un passage concis.
Jusqu'à présent nous pouvons diviser les difficultés de nos programmes en les découpant en procédures. Abstraire une partie d'un traitement pour en faire une procédure indépendante (ou emboîtée) réduit la taille du texte que nous avons à garder en mémoire à un instant. Restreindre à cette procédure la portée de ses variables locales diminue le nombre d'objets dont nous avons à nous préoccuper simultanément.
Certaines procédures utilisées dans le programme peuvent être chargées par load. Ceci permet de ne pas avoir tout le texte dans le même fichier. Mais, que le programme soit interprété ou compilé, il est compris dans un environnement global unique, c'est à dire un espace de noms unique, même s'il est hiérarchisé par l'emboîtement des environnements. Toute modification ne serait-ce que d'une ligne du programme nécessite sa ré-interprétation ou sa re-compilation intégrale. On imagine que pour le développement de très gros programmes écrits par une équipe et non par une seule personne cela va poser un problème.
Scheme tel que défini par [CKR98] n'offre pas de solution à ce problème. Les méthodes de programmation que nous allons introduire maintenant sont donc des extensions à la norme, celles du compilateur Bigloo en l'occurrence.
Pour faire face au foisonnement de la complexité engendré par l'écriture de grands programmes, l'idée est de pouvoir découper le programme en modules qui seront compilés séparément.
Dans la plupart des systèmes de programmation, le compilateur traite les programmes source fichier par fichier. Les modules seront chacun dans un ou plusieurs fichiers et nous devrons avoir des moyens d'indiquer au compilateur comment assembler ces fichiers.
Les modules seront eux-mêmes constitués de procédures. Une procédure d'un module devra pouvoir invoquer une procédure d'un autre module.
Les modules ne mettront pas en commun toutes leurs informations, procédures ou données, mais uniquement celles qui seront explicitement définies comme publiques, ou exportées, rendues visibles aux modules extérieurs. Les autres procédures seront dites privées.
Inversement, un module ne tiendra pas compte de toutes les informations exportées par les autres modules, mais il n'importera que les procédures et les données qui sont nécessaires à son exécution.
Pour chaque module, la liste des informations qu'il exporte et de celles qu'il importe définit son interface avec le « monde extérieur ». Lors de la réunion des différents modules pour construire le grand programme (cette opération s'appelle l'édition de liens), les informations définies par les interfaces servent à établir la communication entre les modules.
Historiquement, trois philosophies de compilation séparée se sont développées parmi les langages informatiques :
C'est bien sûr ainsi que se font les choses avec les assembleurs, langages sans typage de données. Cette voie a été empruntée par Fortran et par C, qui est une sorte d'assembleur portable. C permet d'inclure dans le texte du programme-source des en-têtes qui décrivent des données, ce qui tient lieu d'interface.
Avec de tels langages, la cohérence du programme repose sur la vigilance du programmeur. On pourrait croire qu'un peu d'aide informatique serait la bienvenue pour assister cette vigilance, et que ce ne serait pas trop demander puisque justement tout ceci se fait avec des programmes d'ordinateur.
Nous allons reprendre le problème de lecture de banque de séquences de l'alinéa 9.3 page pour en imaginer une conception en modules (ou modulaire) du programme destiné à le résoudre.
Nous pouvons avoir envie de découper ce problème en trois parties :
Voici le premier module choix-fichier, contenu dans le fichier choix-fichier.scm, par où démarre le programme :
(module choix-fichier (main choisir-le-fichier) (import lire-swiss-seq)) ; \label{lisp:import-swiss} (define (choisir-le-fichier argv) ; \label{lisp:choix} (call-with-input-file (cadr argv) lire-des-sequences) ; \label{lisp:call} )
74
Le mot-clé main indique que la procédure choisir-le-fichier sera celle par où commencera l'exécution du programme lors de son lancement.
La procédure lire-des-sequences (ligne ) ne figure pas dans ce module. Elle sera importée depuis le module lire-swiss-seq, ce qu'indique la clause import.
À la ligne argv représente la liste d'arguments que le programme reçoit du shell Unix lors de son lancement. Par convention, Unix transmet sous forme de liste à tout programme lancé depuis la ligne de commande du shell le contenu de la ligne tapée par l'utilisateur pour le lancer, chaque mot tapé étant représenté par une chaîne de caractères qui sera un élément de la liste. Le premier élément est donc le nom du programme (plus précisément le nom du fichier qui contient l'exécutable), les éléments suivants sont les arguments éventuellement fournis par l'utilisateur. Dans le cas présent l'utilisateur devra fournir le nom du fichier contenant la banque de séquences qu'il souhaite examiner, nom qui sera donc récupéré comme (cadr argv), ainsi qu'indiqué en ligne . Le lancement du programme, nommé lire-swissprot, se fera par exemple ainsi :
./lire-swissprot ../Banques/swissprot.extrait
Le module lire-swiss-seq va importer des choses qui sont dans lire-outils et exporter la procédure lire-des-sequences, invoquée depuis le module précédent choix-fichier.
(module lire-swiss-seq (import lire-outils) (export (lire-des-sequences flux))) (define (lire-des-sequences flux) (let boucle ((sequence (lire-une-sequence flux))) ; \label{lisp:lireune} (if (string=? (car sequence) "*EOF*") #f (begin (imprime sequence) (boucle (lire-une-sequence flux)))))) (define (lire-une-sequence flux) (let boucle ((L '()) (ligne (lire-une-ligne flux))) (if (string=? ligne "*EOF*") (list ligne) (let ((type-ligne (substring ligne 0 2))) (if (string=? type-ligne "//") (reverse (cons ligne L)) ; \label{lisp:reverse} (boucle (cons ligne L) (lire-une-ligne flux)))))))
74
Les deux procédures de ce module ont été commentées en 9.3.1 page . On relèvera que lire-une-sequence est une procédure privée du module.
(module lire-outils (export (imprime seq) (lire-une-ligne flux))) (define (imprime seq) (if (null? seq) (print "sequence vide") (if (null? (cdr seq)) (print (car seq)) (begin (print (car seq)) (imprime (cdr seq)))))) (define (lire-une-ligne flux) (if (eof-object? (peek-char flux)) "*EOF*" (let boucle ((ligne "") (c (read-char flux))) ; \label{lisp:readchar} (if (char=? c #\newline) ligne (boucle (string-append ligne (string c)) (read-char flux)))))) ; \label{lisp:readchar}
74
Les procédures ont été commentées et la clause de la déclaration de module n'appelle guère de commentaire.
Un programme Bigloo est constitué de plusieurs modules. Un module peut s'étendre sur plusieurs fichiers (voir [Ser98]). Il convient de noter que chaque module constitue un espace de noms séparé, et qu'entre modules ne sont partagées que les informations explicitement exportées et importées et les données passées en arguments. Le découpage en modules introduit une divergence avec le modèle d'environnement global de Scheme tel que défini dans [CKR98].
Nos trois modules sont rédigés, il reste à les compiler et à construire le programme exécutable. Au chapitre 3 page nous avons vu comment construire un programme mono-module :
Ici c'est un peu plus compliqué, nous devons compiler chacun des trois modules, puis les relier ensemble pour construire l'exécutable. L'invocation de la commande bigloo montrée ligne enchaînait automatiquement compilation et construction. Ici il va falloir décomposer la construction en deux temps, compilation et édition de liens.
Cette séparation en deux étapes peut sembler compliquée, mais elle est de toute façon inévitable si l'on veut relier ensemble des modules écrits dans des langages différents, donc forcément compilés par des compilateurs différents et réunis in fine par l'édition de liens.
# bigloo -c <nom
de fichier source>
qui produira un fichier dit fichier objet, de même nom que le fichier source, mais suffixé par .o au lieu de .scm :
lire-swiss-seq.scm
lire-swiss-seq.o
access-file.scm
donne les fichiers contenant les modules :
((choix-fichier "choix-fichier.scm") (lire-swiss-seq "lire-swiss-seq.scm") (lire-outils "lire-outils.scm"))
74
#
bigloo -afile access-file.scm -c lire-outils.scm
# bigloo
lire-outils.o lire-swiss-seq.o choix-fichier.o -o lire-swissprot
L'ensemble du processus de construction se schématise ainsi :
Ce processus est d'une complexité non négligeable, il serait plus confortable de pouvoir le programmer. Ce sera l'objet de la section suivante.
Le système Unix possède un système de construction de programmes (on dit aussi un configurateur) nommé make. Au moyen de ce système on peut décrire les fichiers source et autres nécessaires à la construction d'un programme, les dépendances entre fichiers, les commandes nécessaires à la compilation des sources et à la construction des exécutables, ainsi que beaucoup d'autres choses.
L'usage habituel de make consiste à déterminer automatiquement quelles parties d'un grand programme doivent être recompilées après que certains fichiers source aient été modifiés, et à lancer les commandes appropriées pour ce faire.
Make est en fait un système plus général que seulement un configurateur de programmes. C'est un langage de programmation adapté à la résolution de systèmes de contraintes. Un programme make comporte deux types d'instructions : des règles et des commandes.
Une règle énonce le nom d'une cible à atteindre (par exemple le nom d'un fichier exécutable) suivi des noms des cibles préliminaires dont elle dépend et qu'il faudra avoir atteintes avant d'espérer atteindre la cible courante.
Une instruction de type règle est suivie d'instructions de type commandes qui formulent les actions à effectuer pour atteindre la cible de la règle.
Make peut être utilisé pour configurer d'autres objets que des programmes : toute situation où certains fichiers-cibles doivent être mis à jour automatiquement à partir de fichiers-origines à chaque modification de ces derniers et selon certaines règles est susceptible d'être automatisée au moyen de make. Une cible n'est d'ailleurs pas nécessairement un fichier, il peut s'agir plus généralement d'un ensemble de tâches à réaliser désigné par un nom symbolique.
Le langage de make est loin d'être simple et sa description complète excéderait les limites de ce cours10.1. Nous nous contenterons d'indiquer quelques moyens propres à rédiger un programme make simple.
Un programme make est contenu dans un fichier nommé par convention Makefile. En général on place dans un même répertoire les fichiers qui servent à la construction du programme et le Makefile qui indique comment le construire. Les exemples ci-dessous supposent cette condition remplie, et que ce répertoire est le répertoire courant quand on tape la commande make.
Nous prendrons comme exemple la construction du programme donné ci-dessus (section 10.4 page ). Notre Makefile va comporter des lignes de texte essentiellement de deux sortes :
Des lignes de règles, ou lignes de dépendance, énumèrent, pour une cible à produire, les cibles préalables (souvent des fichiers) dont elle dépend. Si un des fichiers dont dépend la cible est modifié depuis sa dernière construction10.2, il faudra la reconstruire. Ainsi, la règle de dépendance pour lire-outils.o est très simple :
lire-outils.o: lire-outils.scm
Le fichier-objet lire-outils.o dépend du fichier-source lire-outils.scm. Si ce dernier est modifié (la création est un cas particulier de modification) il faut reconstruire l'objet.
Une ligne de règle se reconnaît au fait qu'elle comporte le signe « : », qui sépare la cible à sa gauche des pré-requis à sa droite.
La règle pour le programme complet est plus longue, pour aller à la ligne il faut
placer un caractère « \
» à la fin, immédiatement suivi d'un
retour-chariot :
lire-swissprot: lire-outils.o lire-swiss-seq.o \
choix-fichier.o
La première règle du Makefile joue un rôle particulier, elle mentionne une cible toujours visée. Il convient de mettre en tête soit la règle qui déclenche la construction du programme résultant, soit une règle qui se contente d'énumérer les exécutables souhaités :
all: lire-swissprot
Des lignes de commandes énumèrent les actions à effectuer pour produire la cible. Dans le cas simple que nous allons décrire ce seront des compilations et des éditions de liens, mais toute commande Unix serait acceptable.
Attention, chaque ligne de commande s'exécute dans un environnement propre au sens Unix du terme ; si par exemple nous voulons visiter des sous-répertoires où des commandes make déclencheront d'autres actions, et ainsi de suite récursivement, placer la commande cd appropriée sur une ligne et passer à la ligne suivante pour décrire les actions ne produira pas le résultat escompté. Il faut utiliser la syntaxe du shell pour la composition séquentielle des processus :
(cd <mon répertoire> ; ${MAKE}
[<ma cible>])
${MAKE}
est une variable standard de make qui désigne make. À cet
endroit nous voulons exécuter make, mais si nous écrivons simplement make nous n'aurons
la garantie ni que nous invoquons bien la même version de make que celle qui a lancé le
Makefile, ni surtout qu'elle va s'exécuter avec les mêmes paramètres. ${MAKE}
nous donne ces garanties.10.3
Une ligne de commande commence par un caractère de tabulation. Comme make reconnaît les lignes de commande à ce caractère, il faut prendre garde à ne pas l'oublier pour les lignes de commande et à ne pas en mettre ailleurs. Et avoir remarqué que le « copier-coller » à la souris de votre interface graphique préférée transforme parfois les tabulations en espaces blancs.
La ligne de commande pour la cible lire-outils.o sera :
bigloo -afile access-file.scm -c lire-outils.scm
pour l'édition de liens qui construit le programme :
bigloo lire-outils.o lire-swiss-seq.o \ choix-fichier.o -o lire-swissprot
sans oublier la tabulation.
Notre Makefile complet peut donc s'écrire :
all: lire-swissprot choix-fichier.o : choix-fichier.scm bigloo -afile access-file.scm -c choix-fichier.scm lire-swiss-seq.o : lire-swiss-seq.scm bigloo -afile access-file.scm -c lire-swiss-seq.scm lire-outils.o : lire-outils.scm bigloo -afile access-file.scm -c lire-outils.scm lire-swissprot: lire-outils.o lire-swiss-seq.o choix-fichier.o bigloo lire-outils.o lire-swiss-seq.o choix-fichier.o \ -o lire-swissprot
74
Les trois règles de construction des objets sont extrêmement similaires. L'esprit de la programmation rechigne devant l'inélégance de cette répétition. Make nous offre la possibilité d'écrire des règles génériques :
%.o: %.scm
bigloo -afile access-file.scm -c $< -o
$@
ce qui signifie : pour tout fichier-cible avec un nom de la forme %.o, %
étant un préfixe quelconque, le fichier %.scm
s'il existe est un bon
prérequis, et alors l'action décrite par la ligne de commande suivante est exécutée
(si quelque-chose a été modifié dans le prérequis). $<
est une
variable dont la valeur est la liste des prérequis qui ont déclenché la règle (ici le
fichier %.scm
), $@
est une variable qui
désigne la cible courante. Nous obtenons donc le Makefile suivant :
all: lire-swissprot %.o: %.scm bigloo -afile access-file.scm -c $< -o $@ lire-swissprot: lire-outils.o lire-swiss-seq.o choix-fichier.o bigloo lire-outils.o lire-swiss-seq.o choix-fichier.o \ -o lire-swissprot
74
Ceci rédigé il nous suffit de nous placer dans un répertoire contenant les fichiers suivants :
Makefile | choix-fichier.scm | lire-swiss-seq.scm |
access-file.scm | lire-outils.scm |
et taper la commande make suffira à construire notre programme lire-swissprot.
Makefile avec des règles de suffixes
.SUFFIXES : .o .c .cc .scm BGL_SOURCES = peuple-iter.scm call-os.scm BGL_OBJECTS = peuple-iter.o call-os.o SOURCES = $(BGL_SOURCES) create_db.cc read_db.cc \ write_db.cc copy_seq.cc \ xalloc.c liste_db.cc query_db.cc stub.cc CXX_OBJECTS = read_db.o create_db.o write_db.o \ copy_seq.o liste_db.o query_db.o stub.o C_OBJECTS = xalloc.o OBJECTS = $(CXX_OBJECTS) $(C_OBJECTS) $(BGL_OBJECTS) EXECUTABLE = iter-os CCC=/usr/ucb/cxx BIGLOO = bigloo CFLAGS= -O3 CPPFLAGS= BGL_FLAGS= -Obench -farithmetic -rm BGL_LDFLAGS= -lstatic-bigloo CC=gcc all: $(EXECUTABLE) Makefile .cc.o: $(CCC) $(CPPFLAGS) -c $< -o $@ .c.o: $(CC) $(CFLAGS) -c $< -o $@ .scm.o: $(BIGLOO) -afile access-file.scm $(BGL_FLAGS) -c $< -o $@ $(EXECUTABLE): $(OBJECTS) $(BIGLOO) $(BGL_LDFLAGS) -o $@ $(OBJECTS) clean: rm -f $(EXECUTABLE) $(OBJECTS)
74
Une autre façon d'écrire pour make des règles plus générales (et donc moins nombreuses) est d'utiliser les suffixes des noms de fichiers pour déterminer les règles à leur appliquer. En général sous Unix les fichiers source sont suffixés par .scm pour les programmes Bigloo, .c pour les programmes C, .cc pour les programmes C++, .f pour les programmes Fortran, etc. Les compilateurs produisent des fichiers objet suffixés par .o. Ceci nous permet d'écrire des règles selon une syntaxe que nous allons illustrer par un exemple.
Supposons que nous voulions construire un programme nommé iter-os à partir de fichiers source Bigloo, C et C++ :
Ce Makefile commence à ressembler à du travail professionnel. La première
ligne définit les suffixes que nous allons utiliser (en fait cette ligne n'est
indispensable que pour définir le suffixe des fichiers Bigloo, les autres sont définis
par défaut). Les listes de fichiers objet sont définis par des variables pour éviter
les définitions multiples qui aboutissent à des incohérences. La variable définie par BGL_OBJECTS
=
... est invoquée ultérieurement sous le nom ${BGL_OBJECTS}
. Les
accolades {
et }
peuvent être remplacées (sauf cas
particulier) par des parenthèses ( et ).
Les drapeaux des compilateurs sont définis par des variables vides par méfiance à
l'égard de make, qui souvent prévoit des valeurs par défaut. Les noms des
compilateurs sont définis par des variables pour les mêmes raisons que nous utilisions ${MAKE}
ci-dessus pour désigner make.
La règle .cc.o décrit comment faire un fichier objet .o à partir
d'un fichier source .cc. La variable $<
désigne le fichier
prérequis à partir duquel on construit la cible. $@
est comme ci-dessus une
variable qui désigne la cible courante.
La règle clean permet de détruire les fichiers intermédiaires et finals pour repartir des sources quand on a fait des bêtises. make clean sera la façon de l'invoquer.
Ces quelques paragraphes sont loin d'épuiser le sujet make. Ils devraient vous permettre d'écrire des Makefiles simples et de lire ceux que vous trouverez dans les distributions de logiciels récupérées sur le réseau. Pour de plus amples détails, voir [OT91].
Lors de l'écriture d'un programme un peu complexe les étapes du développement et de la mise au point sont multiples. Il arrive que l'on explore une voie de développement qui se révèle peu fructueuse et que l'on souhaite abandonner, ou que ce qui était prévu pour être un logiciel se scinde en deux programmes. Bref, on souhaiterait revenir en arrière et retrouver son programme (constitué de ses nombreux modules) sous une de ses formes antérieures.
Déjà si on a découpé son programme en modules et rédigé des Makefiles on a de meilleures chances de parvenir à faire cette régression. Mais si on s'est contenté de modifier les fichiers avec son éditeur de texte il va falloir re-modifier les programmes source, ce qui est toujours une opération périlleuse. Il serait bien de disposer d'un outil d'archivage informatisé qui pourrait nous restituer des versions clairement identifiées de l'état ancien de nos programmes. Unix nous procure un tel outil, il s'appelle RCS (pour Revision Control System).
Nous allons introduire l'utilisation de RCS en prenant l'exemple du programme des paragraphes précédents.
RCS peut archiver les versions successives de fichiers dont le contenu est du texte, nous nous en servirons donc pour les fichiers-source, à l'exclusion des fichiers objet et exécutables. De toute façon, la forme à archiver d'un programme est la forme source, parce qu'à partir d'un source on peut reconstituer un exécutable, alors qu'on ne sait rien faire d'un exécutable.
RCS utilise pour gérer les fichiers qu'il archive le système de fichiers de Unix et note dans chacun les lignes modifiées par rapport au précédent archivage du même fichier, il ne s'agit pas de l'accumulation des versions successives.
Nous allons utiliser RCS pour gérer les programmes sources et les autres fichiers de texte nécessaires à la construction du programme lire-swissprot développé au cours de ce chapitre. Pour commencer il faut se placer dans le répertoire qui contient ces fichiers et y créer un sous-répertoire nommé RCS :
# mkdir RCS
Nous allons archiver dans la base fraîchement créée un programme qui nous semble dans un état suffisamment stable :
# ci -u choix-fichier.scm
#
92 97
RCS/choix-fichier.scm,v <- choix-fichier.scm
enter description, terminated with single '.' or end of file:
NOTE: This is NOT the log message!
>> Le module principal de lire-swissprot
>> .
initial revision: 1.1
done
La commande ci (pour check-in) de la ligne demande l'archivage du fichier choix-fichier.scm dans la base RCS. L'option -u précise que le fichier d'origine ne doit pas être détruit. Il ne sera pas détruit mais ses droits d'accès seront modifiés de telle sorte qu'il ne soit désormais accessible qu'en lecture, ce qui permet de le compiler mais pas de le modifier « à l'insu » de RCS.
On exécutera la commande ci de la même façon pour les autres fichiers, y compris Makefile et access-file.scm.
Si maintenant on désire introduire de nouvelles modifications dans le programme choix-fichier.scm, qui en constitueront une nouvelle version, la commande est la suivante :
# co -l choix-fichier.scm
RCS/choix-fichier.scm,v -> choix-fichier.scm
revision 1.1 (locked)
done
co (pour check-out) restaure les droits d'écriture sur le fichier. L'option -l précise que l'on souhaite verrouiller (lock) le fichier à son usage exclusif pour éviter que quelqu'un d'autre le modifie en même temps (indispensable pour le travail en équipe).
Une fois les modifications faites, une nouvelle application de la commande ci créera la version 1.2 du programme :
# ci -u choix-fichier.scm
RCS/choix-fichier.scm,v <- choix-fichier.scm
new revision: 1.2; previous revision: 1.1
enter log message, terminated with single '.' or end of file:
>> une modification
>> .
done
Si nous voulons travailler à nouveau sur cette version modifiée nous utiliserons la commande co -l comme ci-dessus, mais si notre précédente modification nous inspire des remords nous pouvons interroger RCS au moyen de la commande rlog pour savoir quel est l'historique des versions :
# rlog choix-fichier.scm RCS file: RCS/choix-fichier.scm,v Working file: choix-fichier.scm head: 1.2 branch: locks: strict access list: symbolic names: comment leader: "\# " keyword substitution: kv total revisions: 2; selected revisions: 2 description: Le module principal de lire-swissprot ---------------------------- revision 1.2 date: 1997/09/03 10:57:28; author: bloch; state: Exp; lines: +2 -0 une modification ---------------------------- revision 1.1 date: 1997/09/03 10:27:35; author: bloch; state: Exp; Initial revision
et décider par exemple de revenir à la version 1.1 :
# co -l1.1 choix-fichier.scm
RCS/choix-fichier.scm,v -> choix-fichier.scm
revision 1.1 (locked)
done
C'est la disponibilité sous Unix d'outils tels que RCS et make qui fait de ce système le préféré des développeurs de logiciels.
Pour utiliser les outils décrits ci-dessus et apprendre à cette occasion de nouvelles choses utiles, nous allons écrire en Scheme un « script CGI ».
Le langage HTML (Hypertext Markup Language) permet de confectionner des pages pour le World-Wide Web. Si on veut que le visiteur de la page puisse dialoguer avec un programme installé sur le serveur, il convient de construire une page d'un type particulier appelé formulaire, et que le programme obéisse à des conventions spéciales pour acquérir les données entrées par l'utilisateur dans le formulaire et pour afficher le résultat sur la page WWW. Un tel programme s'appelle une « CGI » (pour « Common Gateway Interface »). Une explication plus complète sur l'organisation des données et des programmes pour constituer un serveur WWW peut se trouver, par exemple, dans [LPJ$^$95] ou en http://www.univ-rennes1.fr/CRI/documentations/cgi-ismap/JRES.html.
Nous allons réutiliser comme exemple le programme 7.3.4 du chapitre 7 page de construction de triangles de Pascal, et construire un formulaire qui permettra au visiteur de demander la construction du triangle de Pascal pour une puissance n de son choix.
Il n'est pas question de donner ici un cours HTML . Ce langage a l'avantage considérable de s'apprendre par plagiat, puisque toute page HTML exhibe son code source sur simple demande d'un coup de souris. Le nôtre est inspiré d'un article de Pierre Ficheux pour « Linux Magazine » :
<HTML> <HEAD> <TITLE>Test Pascal CGI</TITLE> </HEAD> <BODY> <HR> <FORM ACTION="/cgi-bin/pascal.cgi" METHOD="POST"> Nombre :<INPUT NAME="Nombre" ><BR> <P> <INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Calculer"> <INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !"> </P> </FORM> </BODY> </HTML>
74
La ligne :
<FORM ACTION="/cgi-bin/pascal.cgi" METHOD="POST">
donne le nom du programme (pascal.cgi) associé au formulaire dont l'exécution sera déclenchée par le bouton déclaré par :
<INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Calculer">
La fenêtre de dialogue et le nom de la variable entrée par le visiteur sont déclarés par la ligne :
Nombre :<INPUT NAME="Nombre" ><BR>
Un formulaire tel que le programme 10.7.1 renvoie les données saisies par le visiteur sous une forme un peu particulière et il va nous falloir écrire quelques lignes de programme pour les analyser (les « parser »), en d'autres termes découper la chaîne renvoyée en champs et retourner au programme de calcul le champ désiré, en l'occurrence celui dénommé Nombre.
Un formulaire HTML tel que celui de la section précédente renvoie la chaîne suivante si on a entré 12 dans le champ Nombre :
Nombre=12&SUBMIT=Calculer
Les différents champs sont séparés par des &, chaque champ comporte son nom et sa valeur séparés par le signe =. Nous devons extraire la valeur saisie (ici 12) pour la donner au programme de calcul, ce qui est l'objet du module 10.7.2.1.
Construction du triangle de Pascal
(module pascal (main init) (import html-parser)) (define (init argv) (print "MIME-Version: 1.0") (print "Content-type: text/html") (newline) (pascal (string->number (le-champ "Nombre" (les-champs (read-line (current-input-port))))))) (define (pascal n) (do ((i 0 (+ i 1)) (L '(1) (ligne-suivante L))) ((> i n)) (print L "<BR>"))) (define (ligne-suivante Ligne) (let construct ((L Ligne) (L+1 '(1))) (if (null? (cdr L)) (reverse (cons 1 L+1)) (let ((L+1 (cons (+ (car L) (cadr L)) L+1))) (construct (cdr L) L+1)))))
74
Décoder les champs du formulaire HTML
(module html-parser (export (les-champs html-query) (le-champ nom a-liste-champs))) (define (index chaine caractere) (let ((longueur (string-length chaine))) (do ((i 0 (+ i 1))) ((or (= i longueur) (char=? caractere (string-ref chaine i))) (if (= i longueur) #f i))))) (define (get-champ html-query) (let ((longueur (string-length html-query)) (iperlu (index html-query #\&))) (cond ((number? iperlu) (cons (substring html-query 0 iperlu) (substring html-query (+ iperlu 1) longueur))) (else (cons html-query ""))))) (define (split champ separateur) (let ((longueur (string-length champ)) (ieq (index champ separateur))) (cons (substring champ 0 ieq) (substring champ (+ ieq 1) longueur)))) (define (unPlus chaine) ; ; Dans le message renvoyé au programme par ; un formulaire HTML, les espaces blancs sont ; remplacés par des signes +. La procédure qui ; suit remet des espaces blancs. ; (let ((longueur (string-length chaine)) (iplus (index chaine #\+))) (if (or (zero? longueur) (not iplus)) chaine (string-append (substring chaine 0 iplus) " " (unPlus (substring chaine (+ iplus 1) longueur)))))) (define (unQP chaine) ; ; Dans le message renvoyé au programme par ; un formulaire HTML, les caractères composés ; de code ASCII supérieur à 127 peuvent être ; représentés par le signe \% suivi de la valeur ; du code ASCII représentés sur deux chiffres ; hexadécimaux ("quoted printable"). La procédure ; suivante décode cette représentation. ; (let ((longueur (string-length chaine)) (ipercent (index chaine #\%))) (if (or (zero? longueur) (not ipercent)) chaine (string-append (substring chaine 0 ipercent) (string (integer->char (string->integer (substring chaine (+ ipercent 1) (+ ipercent 3)) 16))) ; Bigloo : base 16 (unQP (substring chaine (+ ipercent 3) longueur)))))) (define (les-champs html-query) (define (champs-aux html-query aliste-champs) (let* ((longueur (string-length html-query)) (champ-reste (get-champ html-query)) (champ-1 (car champ-reste)) (reste-query (cdr champ-reste))) (if (= longueur 0) (reverse aliste-champs) (champs-aux reste-query (cons (split champ-1 #\=) aliste-champs))))) (champs-aux (unQP (unPlus html-query)) '())) (define (le-champ son-nom a-liste-champs) (cdr (assoc son-nom a-liste-champs)))
74
Il est très similaire au programme 7.3.4 donné en page , agrémenté des quelques formules magiques nécessaires au bon fonctionnement de HTML. C'est le programme 10.7.2.1.
Makefile pour un programme CGI.
BIGLOO = bigloo AFILE = access-file.scm BGL_FLAGS = -afile $(AFILE) -Obench -farithmetic -static-bigloo CIBLE = pascal.cgi REPERT_CIBLE = ./bin REPERT_CGI = /usr/local/http/cgi-bin/ %.o: %.scm @ $(BIGLOO) $(BGL_FLAGS) -c $*.scm -o $*.o OBJECTS = Src/pascal.o Src/html-parser.o SOURCES = Src/pascal.scm Src/html-parser.scm all: $(REPERT_CIBLE)/$(CIBLE) $(REPERT_CIBLE)/$(CIBLE): $(OBJECTS) @ echo "Edition de liens..." @ $(BIGLOO) $(BGL_FLAGS) $(OBJECTS) \ -o $(REPERT_CIBLE)/$(CIBLE) @ echo "$(REPERT_CIBLE)/$(CIBLE) construit." @ echo "-------------------------------" $(REPERT_CIBLE): @ mkdir -p $(REPERT_CIBLE) install: cp $(REPERT_CIBLE)/$(CIBLE) $(REPERT_CGI) clean: -rm -f $(OBJECTS) $(SOURCES_C) -rm -f *~ Src/*~ Src/*.o Src/*.mco @ echo "nettoyage fait..." @ echo "-------------------------------" # destruction aussi des binaires : cleanall: clean -rm -f $(REPERT_CIBLE)/$(CIBLE)
74
Le caractère « @ » au début de certaines lignes de commande indique à make de ne pas les afficher lors de leur exécution. L'option de compilation -farithmetic demande à Bigloo d'utiliser les procédures de l'arithmétique entière plutôt que les procédures génériques, plus lentes (notre programme ne traite que des nombres entiers). L'option -static-bigloo stipule que les programmes de bibliothèque10.4seront incorporés physiquement au fichier exécutable, plutôt que liés dynamiquement, ce qui rendrait le programme exécutable dépendant de la présence de la bibliothèque pour son exécution.
Le secret du fonctionnement d'un serveur WWW est que les bons fichiers soient rangés dans les bons répertoires. En effet, pour des raisons de sécurité essentiellemment, les programmes CGI et les pages HTML sont placés en des endroits convenus. Ces endroits sont précisés, pour un serveur Apache par exemple, par le fichier /etc/apache/srm.conf.
Sur le serveur de l'auteur, la configuration, assez classique, est la suivante : l'arborescence des fichiers HTML a sa racine en /usr/local/http. Les programmes CGI seront en /usr/local/http/cgi-bin/, alias /cgi-bin/. Les pages HTML seront dans /usr/local/http/Public_html/ ou dans ses sous-répertoires.
Respecter les emplacements précisés par le fichier de configuration et ne pas oublier de conférer aux fichiers les permissions adéquates est la première condition pour un fonctionnement correct.
Le serveur de triangles de Pascal sera accessible en : http://hatchepsout.sis.pasteur.fr/Public_html/Scripts/pascal-post.html, service réservé à l'intérieur de l'Institut Pasteur, trop stratégique.
Notez que, par convention, /usr/local/http/Public_html/
se transforme en
http://hatchepsout.sis.pasteur.fr/Public_html/
/usr/local/http/cgi-bin/ en
http://hatchepsout.sis.pasteur.fr/cgi-bin/.
Juste pour illustrer ce qui précède avec un exemple un peu plus réel, nous allons mettre sous forme de programme CGI l'annuaire associatif du chapitre 8 page . Nous allons conserver deux fonctions : interroger l'annuaire par un nom de personne, ajouter à l'annuaire le doublet constitué du nom d'une personne et de son numéro de téléphone.
Entre deux utilisations, l'annuaire réside dans un fichier sur disque qui comporte une
ligne par abonné. Chaque ligne contient, sous forme de chaînes de caractères, le nom et
le numéro de téléphone de l'abonné, séparés par un caractère de tabulation (#\tab
).
Avant chaque usage il faut donc lire ce fichier et construire la table associative qui
constitue l'annuaire « actif ». Après chaque ajout il faut reconstituer le fichier à
partir de l'annuaire et le réécrire sur disque10.5.
<HTML> <HEAD> <TITLE>CGI Annuaire</TITLE> </HEAD> <BODY> <HR> <P> <FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST"> <PRE> Nom :<INPUT NAME="Nom" > <INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Interroger"> <INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !"> </PRE> </FORM> <FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST"> <PRE> Nom :<INPUT NAME="Nom" > Numéro :<INPUT NAME="Numero" ><BR> <INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Ajouter"> <INPUT NAME="RESET" TYPE=RESET VALUE="Effacer !"> </PRE> </P> </FORM> <FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST"> <PRE> <INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Afficher"> </PRE> </P> </FORM> <FORM ACTION="/cgi-bin/annuaire.cgi" METHOD="POST"> <PRE> <INPUT NAME="SUBMIT" TYPE=SUBMIT VALUE="Vider"> </PRE> </P> </FORM> </BODY> </HTML>
qui devra afficher ceci :
Le serveur d'annuaire sera accessible en :
http://hatchepsout.sis.pasteur.fr/Public_html/Scripts/annuaire.html
Voici les différents programmes mis en jeu (les procédures pour décoder HTML sont les mêmes qu'à la section précédente) :
((annuaire "Src/annuaire.scm") (mod-hash "Src/hash.scm") (manip-annuaire "Src/manip-annuaire.scm") (outils "Src/outils.scm") (html-parser "../../Sep-compile/Pascal/Src/html-parser.scm"))
74
BIGLOO = bigloo OBJETS_U = Src/outils.o Src/hash.o Src/manip-annuaire.o OBJET = Src/annuaire.o OBJET_H = ../../Sep-compile/Pascal/Src/html-parser.o AFILE = access-file.scm %.o: %.scm bigloo -afile $(AFILE) -c $< -o $@ annuaire: $(OBJETS_U) $(OBJET) $(OBJET_H) $(BIGLOO) $(OBJETS_U) $(OBJET) $(OBJET_H) -o ./bin/annuaire clean: rm -f Src/*.o Src/*~ *~ Html/*~ install: cp ./Html/annuaire-filtre.html \ /local/http/Public_html/Scripts/annuaire.html cp ./bin/annuaire /local/http/cgi-bin/annuaire.cgi chmod go+rx /local/http/Public_html/Scripts/annuaire.html chmod go+rx /local/http/cgi-bin/annuaire.cgi chown www:wwwmaint \ /local/http/Public_html/Scripts/annuaire.html chown www:wwwmaint /local/http/cgi-bin/annuaire.cgi
74
(module annuaire (main init) (import outils manip-annuaire html-parser)) (define *N-ELEM* 32) (define fichier (string-append "/home/bloch/Cours/Algo/Programmes/" "Memoire/Annuaire-cgi/Data/annuaire.dat")) (define (init argv) (print "MIME-Version: 1.0") (print "Content-type: text/html") (newline) (let ((query-list (les-champs (read-line (current-input-port)))) (cet-annuaire (make-annuaire fichier *N-ELEM*))) (cond ((not (file-exists? fichier)) (display (string-append "L'annuaire " (basename fichier) " n'est pas présent"))) (else (cet-annuaire 'charger query-list))))) (define (make-annuaire fichier n) (let ((un-annuaire (make-vector n '()))) (letrec ((self (lambda message (case (car message) ((charger) (charger fichier un-annuaire) (apply self (cons 'dispatch (cdr message)))) ((dispatch) (let* ((query-list (cadr message)) (action (le-champ "SUBMIT" query-list))) (cond ((string-ci=? action "Interroger") (let ((nom (le-champ "Nom" query-list))) (self 'interroger nom))) ((string-ci=? action "Ajouter") (let ((nom (le-champ "Nom" query-list)) (numero (le-champ "Numero" query-list))) (self 'ajouter nom numero))) ((string-ci=? action "Afficher") (self 'afficher)) ((string-ci=? action "Vider") (self 'dump)) ((string-ci=? action "Fin") (self 'exit)) (else (self 'ZZZ))))) ((interroger) (let ((nom (cadr message))) (interroger un-annuaire nom))) ((ajouter) (let ((nom (cadr message)) (numero (caddr message))) (ajouter un-annuaire fichier nom numero))) ((donner) un-annuaire) ((afficher) (afficher un-annuaire)) ((dump) (for-each-vector print un-annuaire)) ((exit) (print "Fin")) (else (print "Message inconnu")))))) self)))
74
(module outils (import mod-hash) (export (index chaine caractere) (split champ separateur) (ecrire a-liste flux) (afficher . argv) (vec!:ajouter-objet doublet vecteur) (for-each-vector proc V))) (define (index chaine caractere) (let ((longueur (string-length chaine))) (do ((i 0 (+ i 1))) ((or (= i longueur) (char=? caractere (string-ref chaine i))) (if (= i longueur) #f i))))) (define (split champ separateur) (let ((longueur (string-length champ)) (isep (index champ separateur))) (cons (substring champ 0 isep) (substring champ (+ isep 1) longueur)))) (define (ecrire a-liste flux) (if (not (null? a-liste)) (begin (display (string-append (caar a-liste) (string #\tab) (cdar a-liste)) flux) (newline flux) (ecrire (cdr a-liste) flux)))) (define (afficher . argv) (let ((annuaire (car argv)) (flux (if (null? (cdr argv)) (current-output-port) (open-output-file (cadr argv))))) (for-each-vector (lambda (L) (ecrire L flux)) annuaire) (if (not (null? (cdr argv))) (close-output-port flux)))) (define (vec!:ajouter-objet doublet vecteur) (let* ((n (vector-length vecteur)) (une-case (hash (car doublet) n)) (elem (vector-ref vecteur une-case))) (vector-set! vecteur une-case (cons doublet elem)))) (define (for-each-vector proc V) (let ((longueur (vector-length V))) (let boucle ((index 0)) (if (not (= index longueur)) (begin (proc (vector-ref V index)) (boucle (+ index 1)))))))
74
(module manip-annuaire (import outils mod-hash) (export (ajouter un-annuaire fichier nom numero) (interroger un-annuaire le-nom) (charger fichier un-annuaire) (stocker annuaire fichier))) (define (ajouter un-annuaire fichier nom numero) (vec!:ajouter-objet (cons nom numero) un-annuaire) (stocker un-annuaire fichier)) (define (interroger un-annuaire le-nom) (let* ((n (vector-length un-annuaire)) (la-case (hash le-nom n)) (reponse (assoc le-nom (vector-ref un-annuaire la-case)))) (print (if reponse (cdr reponse) (string-append "Pas d'abonné nommé " le-nom))))) (define (charger fichier un-annuaire) (let ((flux (open-input-file fichier))) (let boucle ((ligne (read-line flux))) (if (not (eof-object? ligne)) (begin (vec!:ajouter-objet (split ligne #\tab) un-annuaire) (boucle (read-line flux))) (close-input-port flux))))) (define (stocker annuaire fichier) (let ((flux (open-output-file fichier))) (for-each-vector (lambda (L) (ecrire L flux)) annuaire) (close-output-port flux)))
74
(module mod-hash (export (hash nom taille-table))) (define (hash nom taille-table) (remainder (apply + (map char->integer (string->list nom))) taille-table))
74