Trotzdem meine neue Website perlmeister.com nur ein paar Seiten hat,
zeigen sich schon typische Probleme: Jede Seite führt oben einen
Navigations-Balken und unten eine Fußnote mit dem Hinweis,
wohin man sich wenden kann, falls etwas nicht funktioniert.
Ändert sich irgendwas, ist der Teufel los: Soll nur ein neues Datum in die Fußzeile, muß man sämtliche Seiten editieren. Das treibt mich nicht nur zum Wahnsinn, sondern ist zudem auch sehr fehleranfällig. Hier kommt die Lösung: Da bestimmte HTML-Elemente auf vielen Seiten wiederkehren, liegt der Ansatz nahe, in den eigentlichen Seiten nur jeweils einen Tag (Täg!) im Format
<!-- include /german/foot.ger --> <!-- /include -->
abzulegen, den sich
ein Spezialprogramm (bevor die Seiten 'live' gehen) schnappt, die referenzierte
Fußzeile, die angeblich in der Datei /german/foot.ger liegt,
holt und sie, wie z.B. in
<!-- include /german/foot.ger --> Hier ist die
Fußzeile! <!-- /include -->
zwischen die Tags preßt.
Der Browser zeigt die <!-- include ... --> Spezial-Tags
nicht an, da es in einen HTML-Kommentar verpackt ist. Ändert sich die
Fußzeile ein weiteres Mal, wiederholt Tag-Ersetzer einfach seine
Tätigkeit -- schließlich ist die Pfad-Information trotz ersetzten
Inhalts immer noch da.
Das Skript aus Listing includer.pl durchstöbert ein Verzeichnis
bis in beliebige Untiefen und ersetzt in allen gefundenen HTML-Dateien
die include-Tags. Damit man die HTML-Stückchen schön hierarchisch
abspeichern kann, liegt jedes von Ihnen in einer eigenen Datei in einem
Verzeichnis unterhalb eines include-Verzeichnisses, in das der
includer vor der Massen-Ersetzung eintaucht, alle Dateien ausliest
und deren Inhalte in einem Hash %INCLUDE_MAP unter den Pfadnamen ablegt.
Für meine Website sieht das include-Verzeichnis folgendermaßen aus:
include.production/
english/
foot.eng head.eng
german/
foot.ger head.ger
Es gibt also Navigations-Balken (head) und Fußnoten (foot)
für deutsche und englische
Seiten. Steht in einer deutschen Seite im Verzeichnis HTML/index.html also
<!-- include /german/head.ger --> <!-- /include -->
<H1>Hier ist der Seitentext</H1>
<!-- include /german/foot.ger --> <!-- /include -->
stopft includer.pl mit dem Aufruf
includer.pl -i include.production HTML
die Navigationsbalken und Fußnoten in alle Seiten unterhalb des
HTML-Verzeichnisses. Wandern die Seiten danach nicht auf den
endgültigen Web-Server, sondern zunächst auf eine Testmaschine,
sehen die Links im Navigationsbalken unter Umständen anders aus -- kein
Problem: Einfach ein zweites Include-Verzeichnis, beispielsweise
include.test anlegen, die HTML-Stückchen darunter entsprechend
modifizieren und
includer.pl -i include.test HTML
aufrufen, schon generiert includer.pl die Seiten für eine andere
Konfiguration, denn die Dateien unterhalb von HTML referenzieren
die HTML-Stückchen relativ zum include-Verzeichnis, so bezieht
sich beispielsweise ein Tag, das /english/foot.eng enthält, auf
include.test/english/foot.eng, falls die Option
-i include.test des Includers gesetzt ist.
Der Includer zeigt für jede Seite an, wieviele Ersetzungen er durchführen konnte:
HTML/index.html: 2 subs
HTML/resume.html: 2 subs
HTML/german/index.html: 2 subs
HTML/german/perl/index.html: 2 subs
HTML/german/perl/gotoperl/index.html: 2 subs
Vorsichtige Naturen starten den Includer zunächst mit der
Option -r, die bewirkt, daß er zwar alle Dateien analysiert, bei
eventuell nicht gefundenen Referenzen meckert, aber keine
Ersetzungen durchführt.
Der Includer arbeitet natürlich Offline, entweder erzeugt man den
HTML-Seiten-Baum auf einer anderen Maschine, um Ihn nach Vollendung
auf den Webserver zu spielen, oder aber man installiert includer.pl
und das include-Verzeichnis mit den HTML-Stückchen
der Einfachheit halber auf dem Webserver
selbst, in einem Verzeichnis oberhalb der Baumwurzel und läßt ihn nach
jeder Änderung einmal durch die Original-Seiten rattern, die Ausfallzeit ist
gering.
Listing includer.pl zieht in Zeile 6 das Getopt::Std-Modul, dessen
Funktion getopts in Zeile 14 die Kommandozeilen-Parameter -r
und -i setzt und, falls vorhanden, die Einträge in $opt{r} und
$opt_i entsprechend setzt.
Bei fehlender -i-Option nutzt includer.pl das Verzeichis include
im gegenwärtigen Verzeichnis. Um aus einer absoluten Angabe wie
mydir/include eine relative zu formen, springt das Skript in
den Zeilen 20-24 einfach schnell ins fragliche Verzeichnis, ermittelt
mit cwd() aus dem Cwd-Modul den relativen Namen und
springt wieder zurück.
In den Zeilen 31 und 32 folgen dann zwei
Aufrufe der find-Funktion aus dem File::Find-Modul. Erst
bekommt scan_include die Dateinamen aus dem Include-Verzeichnis zu
fressen, wobei laut File::Find-Konvention der angesprungene Callback
immer im gerade abgearbeiteten Verzeichnis steht, man also einfach
mit $_ auf die aktuell angesprungene Datei zugreifen kann. Ändert man
absichtlich oder unabsichtlich den Wert von $_ besteht File::Find
ärgerlicherweise darauf, daß $_ seinen Wert am Ende des Callbacks wieder
zurück erhält, sonst kracht's.
scan_include liest also die einzelnen Dateien unterhalb des
Include-Verzeichnisses aus und speichert deren Inhalt als Strings
unter dem Pfadnamen im Hash %INCLUDE_MAP ab.
Schickt sich dann Zeile 32 an, die zu korrigierenden HTML-Seiten abzuklappern,
öffnet der Callback process_file jeweils die Datei, liest sie in einen
String $lines ein, führt in einer gewaltigen Anweisung zum Suchen
und Ersetzen die ganze Transformation durch, und überschreibt, falls
nicht gerade das Read-Only Flag -r gesetzt ist, die jeweilige
Datei mit dem neuen Inhalt.
Die Anweisung aus den Zeilen 71-77 ersetzt alles zwischen den beiden
gesuchten Spezialtags durch den Rückgabewert der Funktion
include_replace() -- der Modifikator e für evaluate macht's
möglich. Die anderen Modifikatoren der Substitutionsanweisung
(die statt "/" das Zeichen "@" als Trenner benutzt)
sind g, i, x und s die für globale Bearbeitung (alle
vorkommenden Tags
werden ersetzt), ignore case (Groß-/Kleinschreibung ignorieren),
eXtended (erlaubt Kommentare und Leerzeichen zur besseren Strukturierung)
und single line (.* paßt über mehrere Zeilen hinweg) stehen.
include_replace kriegt für jeden Treffer den Namen der aktuell
bearbeiteten HTML-Datei und den Namen der gesuchten Include-Datei mit
-- und prüft mit dem Hash %INCLUDE_MAP, ob diese vorher gefunden
wurde. Falls nicht, bricht das Programm mit einer Fehlermeldung ab,
falls ja, liefert include_replace einfach den im Hash ge-cache-ten
Inhalt der Include-Datei zurück, mit dem die Substitutions-Anweisung in
process_file dann endlich den Tag ersetzt. So einfach und doch so
kompliziert!
Zurück zum Alltag: Ändert sich nun ein Objekt, das in mehreren HTML-Seiten
vertreten ist (z.B. Navigationsbalken), wird es einfach im Include-Verzeichnis
einmal geändert und includer.pl aufgerufen -- ratz-fatz erscheint die
ganze Website in neuem Gewand. Die Webseiten selbst dürfen nach Herzenslust
editiert werden, nur die Bereiche zwischen <!-- include ... --> und
<!-- /include --> werden bekanntlich automatisch ersetzt.
Über die zahlreichen Zuschriften wegen meines September-Aufrufs zur Beifallsbekundung habe ich mich sehr gefreut, meine lieben Leser, vielen Dank dafür! Deswegen lass' ich mich auch nicht lange bitten und mache weiter ... see ya in Perl land!
##########################################################################
001 #!/usr/bin/perl -w
002 ##################################################
003 # Michael Schilli, 1998 (mschilli@perlmeister.com)
004 ##################################################
005 ##################################################
006 # Syntax: includer [-i includedir] directory
007 ##################################################
008
009 use Getopt::Std;
010 use File::Find;
011 use Cwd;
012 use strict;
013
014 my (%INCLUDE_MAP, $INCLUDE_ROOT); # Globals
015
016 my %opt;
017 getopts('ri:', \%opt) || usage("Argument Error");
018
019 print "READONLY MODE\n" if $opt{r};
020
021 my $include_dir = $opt{i} || "include";
022
023 my $now = cwd(); # Get absolute path
024 chdir($include_dir) ||
025 usage("Cannot include from $include_dir");
026 $INCLUDE_ROOT = cwd();
027 chdir($now);
028
029 usage("No start directory given") if $#ARGV < 0;
030
031 usage("Start directory doesn't exist: $ARGV[0]")
032 unless -d $ARGV[0];
033
034 File::Find::find(\&scan_include, $INCLUDE_ROOT);
035 File::Find::find(\&process_file, $ARGV[0]);
036
037 ##################################################
038 sub scan_include { # Scan include files
039 ##################################################
040 my $file = $_; # Save $_
041
042 return unless -f $_; # No directories
043
044 open(FILE, "<$file") ||
045 die "Cannot open $file (read)";
046
047 # relative path name
048 (my $rel = $File::Find::name) =~
049 s#^$INCLUDE_ROOT/*##g;
050
051 # read and store
052 my $data = join('', <FILE>);
053 chomp($data);
054 $INCLUDE_MAP{"/$rel"} = $data;
055
056 close(FILE);
057
058 $_ = $file; # reset $_
059 }
060
061 ##################################################
062 sub process_file {
063 ##################################################
064 my $file = $_;
065
066 return if -d $file;
067 return unless $file =~ /\.html$/;
068
069 open(FILE, "<$file") || # Read file
070 die "Cannot open $file (read)";
071 my $lines = join('', <FILE>);
072 close(FILE);
073
074 my $subs = ($lines =~ # Replace includes
075 s@<!-- \s* include # Intro tag
076 \s+ # Whitespace
077 ([^\s]+) # include file
078 .*?--> # end of tag
079 .*?<!--\s*/include\s*-->
080 @include_replace($file, $1)@gsexi);
081 # replace function
082
083 if($subs) {
084 print "$File::Find::name: $subs subs\n";
085
086 if(!$opt{r}) {
087 open(FILE, ">$file") ||
088 die "Cannot open $file (write)";
089 print FILE $lines;
090 close(FILE);
091 }
092 }
093
094 $_ = $file;
095 }
096
097 ##################################################
098 sub include_replace {
099 ##################################################
100 my ($file, $tag) = @_;
101
102 # Check if tag defined
103 if(exists $INCLUDE_MAP{$tag}) {
104 # ... and return replacement
105 return "<!-- include $tag -->" .
106 "$INCLUDE_MAP{$tag}" .
107 "<!-- /include -->";
108 } else {
109 die "Cannot resolve include '$tag' " .
110 "in file ", cwd(), "/$file";
111 }
112 }
113
114 ##################################################
115 sub usage {
116 ##################################################
117 $0 =~ s#.*/##g;
118 print "$0: @_.\n";
119 print "usage: $0 " .
120 "[-r] [-i includedir] directory\n" .
121 "-r: read only\n" .
122 "-i: include file directory\n";
123 exit 1;
124 }
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |