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. |