Nur lokal

Wenn in der Referenz kein Protokoll steckt, versuchen wir, die Datei zu öffnen:

open( DATEI, $datei );

DATEI ist ein sogenanntes Handle, mithin ein Stellvertreter für die Datei, deren Name in $datei steckt. Geht das in Ordnung, haben wir nichts zu berichtigen und können die Datei wieder schließen:

close DATEI;

Geht das Öffnen hingegen schief, …

if ( ! open( DATEI, $datei ) ){}

… müssen wir versuchen, den richtigen Dateinamen rauszubekommen. Wenn der in der Linkangabe mit einem / beginnt, bezieht er sich als absolute Pfadangabe nicht auf das Wurzelverzeichnis des Dateisystems, sondern auf die entsprechende Document Root auf dem Webserver. Halten wir also erst einmal das Verzeichnis in einer Variablen fest, das als Startverzeichnis für die Dateien der Website dient:

$rootDir = "/home/pjung/LU/LU1001/answergirl";

Dieser Umstand macht unsere Aufgabe etwas schwerer: Um eine im Link angegebene Datei, die mit / beginnt, zu finden, gilt es, nicht in /, sondern in $rootdir danach zu suchen. Gibt es etwas zu korrigieren, darf jedoch nicht $rootdir/korrigierter_Name zurück in den Link geschrieben werden, sondern lediglich /korrigierter_Name.

Was liegt also näher, als den korrigierten Namen und das Präfix, das wir zum Auffinden der Datei im Dateisystem brauchen, getrennt zu speichern? Setzt man die Inhalte beider Variablen, $praefix und $korrDat mit dem Punktoperator . zusammen, erhalten wir die Dateiangabe maßgeschneidert für's Dateisystem, nehmen wir nur $korrDat, haben wir die Angabe passend für den Link.

Schreiben wir also zunächst ein / in die Variable, die den korrigierten Link enthalten soll:

$korrDat = "/";

Außerdem merken wir uns das Root-Verzeichnis als Präfix:

$praefix = $rootDir;

Mit dieser Vorbereitung für's spätere Zusammensetzen im korrigierten Dateinamen können wir auf den Schrägstrich am Anfang (^) der in $datei gespeicherten Linkangabe verzichten. Um die Bedingung zu formulieren, unter der $korrDat und $praefix wie eben beschrieben gesetzt werden sollen, benutzen wir daher nicht den Match-, sondern den Substitute-Operator s. Wir sagen dem Skript einfach: "Wenn Du am Anfang von $datei einen / durch Nichts ersetzen kannst, setze $praefix und $korrDat wie eben besprochen."

Leider ist / mal wieder ein Fall für das Escape-Zeichen. Doch glücklicherweise gibt es eine Möglichkeit, das zu suchende und das ersetzende Muster nicht nur mit /, sondern auch mit anderen Sonderzeichen voneinander abzutrennen. Nehmen wir zum Beispiel das Dollarzeichen. Damit wird aus "Suche / am String-Anfang, und ersetze es durch Nichts" keine Escape-Orgie, sondern ein einfaches s$^/$$. Um diese Ersetzung gleich in der Variablen $datei vorzunehmen, sagen wir

$datei =~ s$^/$$;

Als Bedingung formuliert, sieht das dann so aus:

if ( $datei =~ s$^/$$ ){ }

War der Link kein absoluter, bleibt $korrDat noch leer, und im Präfix wird der Punkt als Stellvertreter des aktuellen Verzeichnisses samt Trenn-Slash gespeichert:

else {
   $praefix = "./";
 }

Häppchenweise

Wenn einmal der Link kaputt ist, kann dies am Dateinamen selbst oder an einem der im Pfad angegebenen Verzeichnisse liegen. Folglich müssen wir in den sauren Apfel beißen und jeden durch / getrennten Bestandteil von der Wurzel bis zur Spitze überprüfen. Dazu teilen wir den um einen eventuellen führenden / beraubten Inhalt von $datei an den Slash-Stellen in kleine Häppchen und speichern sie im Array @teile:

@teile = split( /\//, $datei );

Die split()-Funktion benötigt zwei Argumente: den String, den sie zerhacken soll, und das Trennzeichen. Statt nun einfach einen Delimiter anzugeben, kommt hier der Match-Operator zum Einsatz. Zwischen seine beiden /-Flügel setzen wir den die Verzeichnisse trennenden Slash /, und damit der nicht mit dem rechten Flügel-/ verwechselt werden kann, kommt ein \ davor.

Sukzessive nehmen wir die Teilchen nun unter die Lupe:

foreach $teil ( @teile ){ }

Wenn der Pfadbestandteil in $teil ein Punkt für das aktuelle oder () ein Doppelpunkt für das übergeordnete Verzeichnis ist, brauchen wir keine Schreibweise prüfen und fügen den Inhalt von $teil an den String an, der bereits in $korrdat steht:

if ( $teil eq "." || $teil eq ".." ){
   $korrDat .= $teil . "/" ;
 }

Perl kennt zwei Gleichheitsoperatoren: für numerische Werte und für Zeichenketten. Letzterer heißt eq ("equal"). $korrDat .= $teil; wiederum ist eine Kurzschreibweise für

$korrDat = $korrDat . $teil;

Mit dem Anhängeoperator für Zeichenketten, dem Punkt, fügen wir zudem einen Slash als Verzeichnistrenner ein.

Haben wir hingegen einen echten Datei- oder Verzeichnisnamen in $teil stehen, gibt es mehr zu tun. Versuchen wir zunächst, das, was bislang in $korrDat steht, zu verifizieren: Mit dem $praefix davor haben wir es mit einem Verzeichnis zu tun, dass es zu öffnen gilt:

opendir (VERZ, $praefix . $korrDat );

Schließen werden wir es später mit closedir( VERZ );. Sind wir beim Öffnen allerdings nicht erfolgreich, können wir gleich aufgeben und die Bearbeitung der @teile beenden:

opendir (VERZ, $praefix . $korrDat ) || last ;

last verlässt die aktive Schleife, sodass wir mit der Bearbeitung der nächsten $datei weitermachen können.

Wenn wir das Verzeichnis $praefix . $korrDat hingegen öffnen und den Handler VERZ "installieren" konnten, lesen wir am besten mit

readdir( VERZ );

alle darin vorhandenen Dateien aus. Gibt es dort eine Datei oder ein Verzeichnis mit dem Namen, der in $teil gespeichert ist? Auf der Shell würden wir dazu den grep-Befehl benutzen – und netterweise gibt es ihn auch in Perl:

grep ( /$teil/i , readdir( VERZ ) );

Das Muster wird dabei vom Match-Operator umgeben – und natürlich darf die i-Option nicht fehlen, denn die Groß-Kleinschreibung der tatsächlichen Datei kann von $teil durchaus verschieden sein.

Eins haben wir dabei jedoch nicht bedacht: grep findet in dieser Version auch dann Übereinstimmungen, wenn der Inhalt von $teil lediglich Bestandteil eines vorhandenen Datei- oder Verzeichnisnamens ist. Hier gilt es, das Muster zu präzisieren: Wir geben es inklusive Anfang (^) und Ende ($) an und speichern das Ergebnis in einem Hilfsarray:

@auswahl = grep ( /^$teil$/i , readdir( VERZ ) );

Enthält @auswahl nun nichts, also nicht mal ein nulltes Element, können wir keine korrigierte Fassung des Links erstellen und missbrauchen $korrDat für einen HTML-Kommentar, der sagt, dass $datei nicht auffindbar war. Weiter geht's dank last am Schleifenanfang mit einem eventuellen nächsten Element von @dateien:

if ( $#auswahl < 0 ){
   $korrDat = ("<!-- " . $datei . " nicht auffindbar! -->" );
   last;
 }

Mit komischen Sonderzeichenkombinationen, die für unfreiwilliges Gedächtnistraining sorgen, ist Perl reichlich gesegnet. Klaut man einem Array das @ und ersetzt es durch ein $#, erhält man ($) eine Skalarvariable, in der (#) die Anzahl der Array-Mitglieder gespeichert ist.

War die in @auswahl gespeicherte Ausbeute etwas zu erfolgreich (wir erinnern uns, verzeichnis und Verzeichnis können auf Unixsystemen problemlos nebeneinander existieren), setzen wir $korrDat auf einen Kommentar, der besagt, dass es mehrere Möglichkeiten gibt:

elsif ( $#auswahl > 0 ){
   $korrDat = ("<!-- " . $datei . " nicht eindeutig! -->" );
   last;
 }

Nur wenn wir genau eine Variante finden, können wir das nullte Element von @auswahl an $korrDat anhängen:

else {
   $korrDat .= $auswahl[0];
 }

Bleibt ein trailing slash anzuhängen, um $korrdat bereit für neue Unterverzeichnisebenen zu machen. Zu blöd – wenn $teil nun den Dateinamen enthielt, hat auch der einen Slash am Ende, obwohl es da nicht mehr weiter geht. Doch so weit, wie wir bislang mit unserem Skript gekommen sind – immerhin ist es nur ein Skript, um schnell ein paar Groß-Kleinschreibungsfehler auszubügeln – genehmigen wir uns einen üblen Hack: Wir ersetzen den Slash am Ende von $korrDat mit nichts. Und weil's so schön ist, nehmen wir dazu das Plus als Trennzeichen für den Substitute-Operator:

$korrDat =~ s+/$++;

Achja, jetzt hätten wir beinahe vergessen, die aus der Datei ausgelesene $zeile zu korrigieren, indem wir den alten Link $datei durch die korrigierte Ausgabe $korrDat ersetzen …

$zeile =~ s+$datei+$korrDat+;

… und zu guter Letzt natürlich auszugeben:

print $zeile;

Damit naht der große Augenblick: Ab ins Testverzeichnis und das Skript ohne weitere Parameter, aber vielleicht besser durch less "gepipet" auf die dortigen Dateien loslassen. Sieht gut aus? Dann nehmen wir noch schnell das Kommentarzeichen aus der Zeile

# $^I = ".bak";

heraus, und schon legt cgks die konvertierten Dateien unter ihren alten Namen ab, während die Ursprungsversion als Backupdatei mit der Endung .bak einen Vergleich ermöglicht.

Listing 2

cgks

als Ganzes

#!/usr/bin/perl -w
 $^I = ".bak";
 @ARGV = <*.html>;
 $rootDir = "/home/pjung/LU/LU1001/answergirl";
 while ( $zeile = <> ){
   $_ = $zeile;
   @dateien = m/<A HREF=\"(.*?)\">/ig;
   foreach $datei ( @dateien ){
     $korrDat = "";
     if ( ! /(ftp|http):\/\//i ){
       if ( ! open( DATEI, $datei ) ){
         if ( $datei =~ s$^/$$ ){
           $praefix = $rootDir;
           $korrDat = "/";
         } else {
           $praefix = "./";
         }
         @teile = split( /\//, $datei );
         foreach $teil ( @teile ){
           if ( $teil eq "." || $teil eq ".." ){
             $korrDat .= $teil . "/" ;
           } else {
             opendir (VERZ, $praefix . $korrDat ) || last ;
             @auswahl = grep ( /^$teil$/i , readdir( VERZ ) );
             closedir( VERZ );
             if ( $#auswahl < 0 ){
               $korrDat = ("<!-- " . $datei . " nicht auffindbar! -->" );
               last;
             } elsif ( $#auswahl > 0 ){
               $korrDat = ("<!-- " . $datei . " nicht eindeutig! -->" );
               last;
             } else {
               $korrDat .= $auswahl[0];
             }
             $korrDat .= "/";
           }
         }
         $korrDat =~ s+/$++;
         $zeile =~ s+$datei+$korrDat+;
       }
       close DATEI;
     }
   }
   print $zeile;
 }

Kasten 1: Manöverkritik

Sicher gibt es immer elegantere Möglichkeiten, ein Skript zu basteln. So ließe sich beispielsweise eine bei großen Datenmengen schnellere Variante schreiben, die sich jedes überprüfte Verzeichnis merkt, sodass nicht bei jedem Link jedes Verzeichnis neu betrachtet werden muss.

Viel kritischer für den Einsatz ist jedoch, dass das vorgestellte Programm dann versagt, wenn der HTML-Anker A HREF und die Linkquelle auf verschiedenen Zeilen stehen. Dieses Handicap in Kauf nehmen oder noch mehr Komplexität speziell bei den regulären Ausdrücken einzubauen, war die Alternative.

Für Gelegenheitsperlianer eher unbekannt ist jedoch die Tatsache, dass im "Comprehensive Perl Archiv Network" CPAN jede Menge Module lagern, die ähnlich Bibliotheken bei Sprachen wie C, C++ oder Java Unmengen Funktionen für alle möglichen und unmöglichen Einsatzgebiete enthalten.

Im Fall unseres Skripts hätten wir uns die Arbeit mit den regulären Ausdrücken, um mühselig Links zu erkennen, sparen können, wenn wir das bei vielen Distributionen bereits per Default installierte HTML::Parser-Modul (bei SuSE im Paket perl-HTML-Parser, bei Caldera in perl-modules) benutzt hätten.

Allerdings hat dieses Modul einen großen Nachteil: Wir bewegen uns damit in objektorientierten Perl-Gefilden und dürfen uns damit herumschlagen, was ein Parser ist. Für Perl-Noviz(inn)en und Programmmieranfänger(innen) vermutlich ein etwas zu ambitioniertes Projekt.

Glossar

A HREF

Um in einer Webseite mit einem Hyperlink auf eine andere Datei oder Internet-Ressource zu verweisen, schreibt man einen "Anker" in den Text. Dieser wird um die "Hyperreferenz" ergänzt, die angibt, worauf der Link verweist, z. B. A HREF="http://www.linux-user.de/". Damit diese Angabe für die Leser(innen) der Seite nicht im Text erscheint, klammert man sie in spitze Klammern (<A HREF="http://www.linux-user.de/">). Anschließend folgt der Text, auf den "geklickt" werden soll, um zum Verweis zu gelangen. Zu guter Letzt folgt das Ende-Tag </A>, mit dem der Anker abgeschlossen wird: <A HREF="http://www.linux-user.de/">LinuxUser</A>.

Pfad

Der von Verzeichnissen gepflasterte Weg zu einer Datei. Absolute Pfade beginnen an der durch / gekennzeichneten Wurzelspitze des Dateisystems, relative Pfade im aktuellen Verzeichnis.

Pipes

Eine "Rohrleitung", durch die die Ausgabe eines Kommandozeilenprogramms durchgeleitet wird und am "Leitungsende" als Eingabe für ein zweites Werkzeug dient. Sie wird durch einen senkrechten Strich | symbolisiert: kommando1 | kommando2.

regulären Ausdrücke

Von verschiedenen Standard-Unix-Tools verwendete Möglichkeit, Muster auszudrücken. So steht ein Punkt für ein beliebiges Zeichen oder ein Buchstabe für sich selbst. Folgt ein Sternchen, darf das, was vom vorangegangenen Muster abgedeckt wird, beliebige Male, auch keinmal auftreten. Ein Fragezeichen bedeutet hingegen, dass das, worauf es sich bezieht, genau null- oder einmal vorkommt.

Python

Eine objekt-orientierte Skript-Sprache. Programme, die mit Skript-Sprachen geschrieben werden, müssen nicht extra kompiliert werden, sondern können mit einem Interpreter direkt aus dem Source-Code ausgeführt werden. Oft (z. B. bei Perl) kompiliert zwar der Interpreter ein "internes" Binärprogramm, um die Ausführungsgeschwindigkeit zu erhöhen, doch davon merken Anwender(innen) im Normalfall nichts. Da der Aufruf eines Compilers entfällt, eignen sich interpretierte Sprachen besonders gut für kleine Programme, die "schnell hingeschrieben" eine Aufgabe lösen sollen und gar nicht für den Gebrauch durch Dritte bestimmt sind.

Tcl

Eine Skript-Sprache, die meist im Zusammenhang mit dem GUI-Toolkit Tk zum Schreiben grafischer Anwendungen verwendet wird. Sie lässt sich aber auch ohne Tk einsetzen.

Suchpfad

Gibt man ein Kommando ein, sucht die Shell die in der Umgebungsvariablen PATH gespeicherten Verzeichnisse der Reihe nach nach einer gleichnamigen ausführbaren Datei ab. Die erste Fundstelle wird benutzt; wird die Shell nicht fündig, so gibt sie die Fehlermeldung command not found aus, selbst wenn das Kommando irgendwo anders im Dateisystem existiert.

Entities

Hier: HTML-Kodierung für Zeichen, die im ASCII-Zeichensatz nicht auftauchen. Eine Entität beginnt in HTML immer mit einem kaufmännischen Und (&) und endet auf ein Semikolon, z. B. steht ü für ü, < für < und > für >.

Infos

[1] "Völlig link", "Answer Girl" im Linux-Magazin 10/1999: http://www.linux-magazin.de/ausgabe/1999/10/Answergirl/answergirl4.html

[2] Bjellis Perlwelt: http://perlwelt.horus.at/

[3] Viele Webseiten ändern: http://perlwelt.horus.at/beginning/MachSieAlle/home.html

LinuxCommunity kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • The Answer Girl
    Dass der Computeralltag auch unter Linux immer wieder für Überraschungen gut ist, ist eher eine Binsenweisheit: Immer wieder funktionieren Dinge nicht oder nicht so, wie eigentlich angenommen. Das Answer-Girl im Linux-Magazin zeigt, wie man mit solchen Problemchen elegant fertig wird.
  • Doppeltes Lottchen
    Batsh schlägt zwei Fliegen mit einer Klappe: In dieser Sprache geschriebene Programme lassen sich sowohl in Bash-Skripte als auch in die unter Windows genutzten Batch-Dateien übersetzen.
  • Bildverarbeitung mit den Skriptsprachen Perl und Python
    Mit nur wenigen Zahlen Code korrigieren Sie das Format digitaler Bilder, passen die Metadaten an oder beschriften die Fotos für den Upload in ein Online-Album.
  • LaTeX mit Daten aus externen Quellen anreichern
    Mit Skriptsprachen wie Perl oder Python verheiraten Sie Datenbanken und LaTeX zu einem Dream-Team beim Seriendruck.
  • Bilder verarbeiten mit der Magick Scripting Language
    Die Magic Srcipting Language hilft Ihnen, Bilder mit einem fest definierten Satz an Parametern zu bearbeiten. In Verbindung mit geschickter Shell-Programmierung nutzen Sie so die Ressourcen eines Systems optimal aus.
Kommentare

Infos zur Publikation

LU 12/2017: Perfekte Videos

Digitale Ausgabe: Preis € 5,95
(inkl. 19% MwSt.)

LinuxUser erscheint monatlich und kostet 5,95 Euro (mit DVD 8,50 Euro). Weitere Infos zum Heft finden Sie auf der Homepage.

Das Jahresabo kostet ab 86,70 Euro. Details dazu finden Sie im Computec-Shop. Im Probeabo erhalten Sie zudem drei Ausgaben zum reduzierten Preis.

Bei Google Play finden Sie digitale Ausgaben für Tablet & Smartphone.

HINWEIS ZU PAYPAL: Die Zahlung ist ohne eigenes Paypal-Konto ganz einfach per Kreditkarte oder Lastschrift möglich!

Stellenmarkt

Aktuelle Fragen

Huawei
Pit Hampelmann, 13.12.2017 11:35, 2 Antworten
Welches Smartphone ist für euch momentan das beste? Sehe ja die Huawei gerade ganz weit vorne. Bi...
Fernstudium Informatik
Joe Cole, 12.12.2017 10:36, 2 Antworten
Hallo! habe früher als ich 13 Jahre angefangen mit HTML und später Java zu programmieren. Weit...
Installation Linux mint auf stick
Reiner Schulz, 10.12.2017 17:34, 3 Antworten
Hallo, ich hab ein ISO-image mit Linux Mint auf einem Stick untergebracht Jetzt kann ich auch...
Canon Maxify 2750 oder ähnlicher Drucker
Hannes Richert, 05.12.2017 20:14, 4 Antworten
Hallo, leider hat Canon mich weiterverwiesen, weil sie Linux nicht supporten.. deshalb hier die...
Ubuntu Server
Steffen Seidler, 05.12.2017 12:10, 1 Antworten
Hallo! Hat jemand eine gute Anleitung für mich, wie ich Ubuntu Server einrichte? Habe bisher...