Die Leiden eines Webmasters (Linux-Magazin, November 1998)

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.

Der Includer richtet's

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.

Wie funktioniert's?

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.

Substitutions-Monster

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!

Alltag

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.

Danke, danke!

Ü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!

##########################################################################

listing.pl

    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 Schilli

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