Web-Programmierung mit Perl .
 9783827320537, 3827320534 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Webprogrammierung mit Perl

programmer’s

choice

Die Wahl für professionelle Programmierer und Softwareentwickler. Anerkannte Experten wie z. B. Bjarne Stroustrup, der Erfinder von C++, liefern umfassendes Fachwissen zu allen wichtigen Programmiersprachen und den neuesten Technologien, aber auch Tipps aus der Praxis. Die Reihe von Profis für Profis!

Hier eine Auswahl:

Perl

PostgreSQL

MySQL

Farid Hajji 1184 Seiten, 2. Auflage € 49,95 [D]/sFr 88,00 ISBN 3-8273-1553-2

Bruce Momjian 540 Seiten € 44,95 [D]/sFr 78,00 ISBN 3-8273-1859-9

Heinz-Gerd Raymans 624 Seiten € 44,95 [D]/sFr 78,00 ISBN 3-8273-1887-4

Das Buch bietet eine fundierte Einführung in die Grundlagen von Perl. Das für Programmierer geschriebene Lehr- und Arbeitsbuch zeichnet sich durch viele Beispiele aus der Praxis aus (objektorientierte Programmierung, Tcl/Tk). Zahlreiche Übungsaufgaben vertiefen den Stoff oder geben weiterführende Tipps.

Dieses Buch bietet die lange gesuchte Einführung in PostgreSQL, ein komplexes, aber leistungsstarkes Datenbanksystem. Das Buch führt schrittweise von einfachen zu komplexen Datenbankabfragen und demonstriert die Anwendung von PostgreSQL anhand praktischer Beispiele.

MySQL ist die zurzeit populärste Datenbank im OpenSource-Bereich. Dieses Buch erklärt, wie Sie MySQL als Datenbank im WWW oder im lokalen Netz einsetzen können. Es konzentriert sich dabei besonders auf die wesentlichen Features und Befehle der MySQL-Datenbank und Möglichkeiten für den Datenzugriff.

Helmut Patay

Webprogrammierung mit Perl

eBook Die nicht autorisierte Weitergabe dieses eBooks an Dritte ist eine Verletzung des Urheberrechts!

An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam

Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Produktbezeichnungen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.

5

4 3

2

1

05

04

03

02

ISBN 3-8273-2053-4 © 2002 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Christine Rechl, München Titelbild: Liriodendron tulpifera, Tulpenbaum © Karl Blossfeldt Archiv Ann und Jürgen Wilde, Zülpich/ VG Bild-Kunst Bonn, 2002 Lektorat: Frank Eller, [email protected] Korrektorat: G + U, Technische Dokumentation, Flensburg Herstellung: Monika Weiher, [email protected] CD-Mastering: Gregor Kopietz, [email protected] Satz: reemers publishing services gmbh, Krefeld, www.reemers.de Druck und Verarbeitung: Bercker Graphischer Betrieb, Kevelaer Printed in Germany

Inhalt

Über dieses Buch

13

1

Einführung

15

1.1 1.2 1.3 1.4 1.5 1.6 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7 1.8 1.9 1.10 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.10.6 1.10.7 1.10.8 1.10.9

Was ist Perl? Wie installiert man Perl? Wie installiert man Zusatzmodule für Perl? Die Online Hilfe von Perl Perl-Homepage Wie sieht ein Perl-Skript aus? Was sind Statements? Was sind Direktiven? Die Hashbang-Zeile Exit-Status von Skripts Kommentare in Perl Wie sieht ein Perl-Modul aus? Wie sieht die Skriptumgebung in Perl aus? Wie findet Perl Module? Wie werden Skripts ausgeführt? Was ist ein Skriptargument? Skripts in UNIX ausführen Skripts in Windows ausführen Ausführen kurzer Programme Prüfen von Perl-Skripts Inline-Dokumentation im Quellcode Namenskonventionen für Perl-Skripts und Perl-Module Verzeichnistrenner BEGIN und END

15 16 19 20 21 21 23 23 24 27 27 27 29 30 31 32 32 32 33 34 34 35 36 37

2

Grundlagen

41

2.1 2.2 2.3 2.3.1 2.3.2 2.3.3

Grundbegriffe Datentypen Skalare Behandlung von Zahlen Darstellbarer Zahlenbereich Kennzeichnung von Strings (Quoting)

41 43 43 44 44 45

6

Inhalt

2.3.4 2.3.5 2.3.6 2.4 2.4.1 2.4.2 2.5 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.6.6 2.6.7 2.7 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.10 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.10 2.10.1 2.10.2 2.10.3 2.11 2.11.1 2.11.2

Der skalare Wert »undef« Boolesche Werte Referenzen Listen Arrays Hashes Konstanten Variablen Variablennamen (Identifier) Reservierte Wörter in Perl Geltungsbereich von Variablen Skalare Variablen Array-Variablen Hash-Variablen Referenzvariablen Operatoren Was sind Operatoren? Arithmetische Operatoren String-Operatoren Zuweisungsoperatoren Autoincrement- und Autodecrement-Operatoren Logische Operatoren Vergleichsoperatoren Vergleichsoperatoren für Zahlen Vergleichsoperatoren für Strings Bit-Operatoren Statements Statement if Statement unless Schleifen Statement return Funktionen Funktionsdefinition Funktionsaufruf Datenübergabe an Funktionen Datenübergabe an den Aufrufer einer Funktion Funktionskontext Module Die package-Direktive Die require-Direktive Die use-Direktive Ein-/Ausgabe (File I/O) FileHandles DirHandles

47 48 49 49 49 53 56 58 59 60 61 66 69 76 80 96 96 99 102 103 104 105 108 108 112 117 132 133 136 136 144 146 147 152 154 159 162 164 165 166 168 174 175 191

3

Pattern Matching

197

3.1 3.1.1 3.1.2

Matching-Optionen Option i Option m

205 205 206

Inhalt

7

3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.2 3.2.1 3.3 3.4 3.4.1 3.4.2 3.4.3 3.5 3.5.1 3.5.2 3.5.3 3.5.4

Option s Option ms Option g Speichern von Treffern Die Positionsvariablen @- und @+ Reguläre Ausdrücke Metazeichen Ersetzen von Zeichenketten Erweiterte Ausdrücke (?imsx-imsx) (?:pattern) und (?imsx-imsx:pattern) (?!pattern) Besondere Matchingvariablen $1, $2 ... @-, @+ $` und $' $+

208 211 211 213 219 221 221 241 251 251 253 254 255 255 256 256 259

4

Komplexe Datentypen

261

4.1 4.2 4.3

Mehrdimensionale Arrays Mehrdimensionale Hashes Hash-Arrays

261 266 276

5

Objektorientierte Programmierung

5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.2 5.2.1 5.2.2 5.2.3 5.3

Klassen Klassenattribute und Klassenmethoden Konstruktor Instanzattribute und Instanzmethoden Fehlermeldungen von Klassen Vererbung Die Variable @ISA Overloading Overriding Factories

6

Die File-Module

325

6.1 6.1.1 6.1.2 6.2 6.2.1 6.3

File::Path File::Path::mkpath() File::Path::rmtree() File::Find File::Find::find() File::Copy

325 325 328 328 329 337

7

Anwendungsbeispiele

7.1 7.2 7.3

dos2Unix.pl unix2Dos.pl Hexdump von Dateien

279 282 282 283 289 298 301 302 307 309 321

339 339 345 350

8

Inhalt

7.4 7.4.1 7.4.2 7.5 7.6 7.7 7.8 7.9 7.10

Lesen von Properties-Dateien Prozedurale Implementierung Objektorientierte Implementierung Ausgabe aller Hypertext-Links dirname.pl basename.pl Pfadnamen mit Sonderzeichen finden Automatische Dateien erzeugen Dateibäume verwalten

8

CGI

8.1 8.1.1 8.1.2 8.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3 8.3.1 8.4 8.4.1 8.4.2 8.5 8.5.1 8.5.2 8.5.3

Das HTTP-Protokoll Der Request Die Response Cookies Notwendigkeit von Cookies Arbeitsweise von Cookies Netscape-Cookies Cookies gemäß Internet-Draft-Spezifikation Cookie-Beschränkungen CGI-Umgebung CGI-Kommunikation Templates Templatevariablen Template-Engine Das PERL-Modul CGI.pm Verarbeiten von HTML-Formularen Dynamische HTML-Formulare Arbeiten mit Cookies

9

Das Datenbank-Interface DBI

9.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.1.7 9.1.8 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.2.6

Kurzeinführung in SQL SQL-Clientprogramme Tabellen (Tables) Das INSERT-Statement Das DELETE-Statement Das UPDATE-Statement Das SELECT-Statement Joins Commit und Rollback Arbeiten mit dem Modul DBI.pm Voraussetzungen Abfrage verfügbarer Datenbanktreiber Abfrage verfügbarer Datenquellen Aufbauen der Datenbankverbindung Ausführen von SQL-Statements Benutzung von RaiseError

357 358 359 362 366 367 368 370 400

411 416 418 422 427 427 428 430 434 436 437 441 446 446 452 482 484 486 497

507 510 511 515 526 527 529 530 532 535 536 536 537 537 538 543 557

Inhalt

9

9.3 9.3.1 9.3.2 9.4 9.5

Sessions mit CGI und DBI Beispiel eines Workflows Implementierung einer Sessionverwaltung Rekursive Strukturen mit DBI Mehrsprachige Datensätze

558 559 574 606 669

10

Perl/Apache-Integration

10.1 10.2 10.2.1 10.3 10.4 10.5 10.6 10.7

Standard-CGI mod_perl Installation von mod_perl Apache-Module in Perl Authentifizierungs-Modul Web-Authentifizierung mit DBI Persistente Datenbankverbindungen AuthCookie – ein Beispiel

A

Style Guide

721

B

Vordefinierte Variablen

723

B.1 B.2 B.3 B.4 B.5 B.6 B.7 B.8 B.9 B.10 B.11 B.12 B.13 B.14 B.15 B.16 B.17 B.18 B.19 B.20 B.21

@_ @ARGV %ENV $0 @INC %INC $$, $PID, $PROCESS_ID $@, $EVAL_ERROR $_, $ARG $1, $2 ... @+, @LAST_MATCH_END @-, @LAST_MATCH_START $&, $MATCH $`, $PREMATCH $', $POSTMATCH $|, $OUTPUT_AUTOFLUSH $,, $OFS, $OUTPUT_FIELD_SEPARATOR $\, $ORS, $OUTPUT_RECORD_SEPARATOR $?, $CHILD_ERROR $!, $ERRNO, $OS_ERROR %SIG

C

Vordefinierte Funktionen

C.1 C.2 C.3

abs() atan2() binmode()

673 674 677 677 680 695 704 707 708

723 724 724 725 726 729 729 730 730 731 733 733 733 734 734 735 735 736 737 737 738

743 743 744 744

10 C.4 C.5 C.6 C.7 C.8 C.9 C.10 C.11 C.12 C.13 C.14 C.15 C.16 C.17 C.18 C.19 C.20 C.21 C.22 C.23 C.24 C.25 C.26 C.27 C.28 C.29 C.30 C.31 C.32 C.33 C.34 C.35 C.36 C.37 C.38 C.39 C.40 C.41 C.42 C.43 C.44 C.45 C.46 C.47 C.48

Inhalt bless() chdir() chmod() chomp() chop() chown() chr() cos() crypt() defined() delete() die() each() eof() eval() exists() exit() flock() getc() gmtime() grep() hex() int() join() keys() lc() lcfirst() length() localtime() log() lstat() mkdir() no() oct() ord() pack() pop() pos() print() printf() push() rand() read() ref() require()

746 747 747 748 749 750 750 751 751 752 754 755 757 757 758 762 764 765 771 771 772 774 774 776 777 778 778 778 779 780 780 780 781 782 783 783 785 785 786 788 789 789 790 791 792

Inhalt C.49 C.50 C.51 C.52 C.53 C.54 C.55 C.56 C.57 C.58 C.59 C.60 C.61 C.62 C.63 C.64 C.65 C.66 C.67 C.68 C.69 C.70 C.71 C.72 C.73 C.74 C.75 C.76 C.77 C.78

11 reverse() scalar() seek() select() shift() sin() sleep() sort() splice() split() sprintf() sqrt() srand() stat() substr() system() tell() time() truncate() uc() ucfirst() umask() undef() unlink() unpack() unshift() use() utime() values() wantarray()

Index

794 794 796 798 799 800 801 801 803 805 806 809 809 810 811 812 813 814 814 816 816 817 817 819 819 820 820 823 823 824

827

Über dieses Buch »Webprogrammierung mit Perl« soll Sie in die Lage versetzen, in kurzer Zeit effiziente Perl-Programme für praktische Problemlösungen zu erstellen. Wie der Titel des Buches bereits vermuten lässt, erhält das Thema »Webprogrammierung« eine besondere Gewichtung, ebenso habe ich viel Wert auf praxisorientierte Beispiele gelegt, die man in seinem Programmiererdasein häufig antrifft. Das Buch soll keine vollständige Referenz für die Programmiersprache sein, sondern vielmehr eine praktische Anleitung, wie man Probleme in möglichst kurzer Zeit löst. Es bietet nicht nur Informationen über die Programmiersprache Perl selbst, sondern vielfach auch übergreifendes Wissen über Datenbanken, Webtechnologien und Betriebssysteme, und entstand aus eben diesem Mangel an einer übergreifenden Dokumentation (meist muss man drei oder vier Bücher sowie etliche Manualpages gelesen haben, um eine Webanwendung performant und effizient zu programmieren). Wenn Sie die Konzepte und Beispiele dieses Buches verstanden haben, dann ist es ein Leichtes, mit Hilfe der ausgelieferten Dokumentation von Perl weiter in die Tiefen der Programmiersprache einzudringen. In jedem Fall können Sie sich nach dem erfolgreichen Durcharbeiten der Themengebiete dieses Buches als Perl-Experte bezeichnen, es ist aber auch möglich, dass Sie sich nur Basiswissen aneignen, ohne auf die tiefer gehenden Informationen näher einzugehen. Als Autor dieses Buches liegt mir besonders daran, Unterschiede zwischen den verschiedenen Betriebssystemen Unix und Windows herauszuschälen (als Entwickler neige ich natürlich mehr zu Unix-Systemen, weiß aber sehr wohl um die Erfordernis, auch Windows-Anwender zur Glückseligkeit zu führen). Wie oft schon sind Programmierer nahezu verzweifelt, nur weil sie vergessen haben, beim Dateitransfer den ASCII-Modus (oder auch den Binär-Modus) einzuschalten! Da ich nicht nur in Perl entwickle, sondern auch in JAVA und (sehr selten) in C oder C++, werden Sie in diesem Buch des Öfteren Vergleiche finden, was in welcher Sprache besser oder schlechter implementiert ist.

14

Über dieses Buch

Speziell bei Webanwendungen habe ich die Erfahrung gemacht, dass eine der häufigsten Aufgaben das Parsen (syntaktische beziehungsweise semantische Prüfung von Inhalten) von Formularen ist (so ist zum Beispiel die Prüfung einer E-Mail-Adresse eine echte Herausforderung für Programmierer). Für das Parsen von Text ist eine Unterstützung durch die Programmiersprache eine wirkliche Hilfe, und davon bietet Perl in Form von regulären Ausdrücken (Regular Expressions oder kurz auch »regex« genannt) reichlich. Zu dem Thema »reguläre Ausdrücke« sei betont, dass die Perl-Implementierung mittlerweile als Standard anerkannt wird, man findet sie sowohl in den neuesten JAVA-Versionen als auch im .NET Framework von Microsoft. Ich möchte hier anmerken, dass die Entwickler von JAVA ausdrücklich bekannt haben, reguläre Ausdrücke nicht zu verstehen (als Beispiel diente die Codierung von Strings in URL-Semantik, die in Perl eine einzeilige Anweisung darstellt); dies ist wohl der Grund dafür, dass reguläre Ausdrücke nicht von Anfang an in den Sprachwortschatz von JAVA mit aufgenommen wurden (was in Perl der Fall ist). Wenn man einmal die Leistungsfähigkeit der regulären Ausdrücke erkannt und verstanden hat, wird man ohne sie nicht mehr auskommen (zugegeben, es dauert, bis man mit der verwirrenden Syntax von regulären Ausdrücken vertraut ist).

Zielgruppe Dieses Buch wendet sich im Allgemeinen an alle, die effektiv programmieren wollen (oder müssen), im Speziellen möchte ich durch die Hervorhebung der Themen »Webprogrammierung« und »Datenbankprogrammierung« denen helfen, die Anwendungen im Umfeld des Internets erstellen, dazu gehören sowohl CGI-Skripts (das sind Programme, die vom Endbenutzer über einen Browser aufgerufen werden) als auch administrative Programme, die zur Verwaltung einer Website benötigt werden. Aber auch Einsteiger, die noch nicht über fundierte Kenntnisse des Programmierens verfügen, werden mit diesem Buch in die Lage versetzt, das notwendige Basiswissen für Problemlösungen zu erlernen, ohne sich in den Tiefen der Programmiersprache zu verlieren. Vorausgesetzt werden Kenntnisse in HTML und teilweise in JavaScript. Also: Sind Sie Anfänger: Lesen Sie alles durch, soweit Sie es verstehen. Sind Sie bereits in Perl tätig: Lesen Sie die Teile durch, die Neuigkeiten versprechen, es wird sich sicherlich etwas finden, das wertvoll für Sie ist!

1 Einführung Dieses Kapitel soll Ihnen einen Überblick verschaffen, worüber ich im Weiteren sprechen werde, nämlich »Perl«. Sie bekommen sozusagen ein Starterkit, mit dem Sie in der Lage sind, Perl auf dem Rechner zu installieren und die grundsätzlichen Dinge zu verstehen, die man braucht, um einfache Perl-Skripts auszuführen.

1.1 Was ist Perl? Perl ist die Abkürzung von »Practical Extraction and Report Language«, was frei übersetzt so viel heißt wie »sehr praktische Sprache für alles«. Sie wurde von Larry Wall ursprünglich für die Erstellung von Reports aus Rohdaten entwickelt und hieß anfangs »Pearl«, jedoch hat sich Larry schließlich für Perl entschieden, weil er keine fünf Buchstaben für den Namen der Programmiersprache vergeuden wollte. Zunächst galt Perl nur als gutes Tool für Administratoren, die Textdateien bearbeiten mussten. Viele C-Programmierer rümpften die Nase, wurden sie auf Perl angesprochen; für sie war die Programmiersprache C das einzig Wahre. Auch Java-Programmierer sehen Perl oft von oben herab an, ist doch Java die einzige Sprache, mit der sich beliebig skalierbare Anwendungen erstellen lassen. Manche wiederum finden die Syntax von Perl schlichtweg gewöhnungsbedürftig. Nun, auch ich brauchte eine gewisse Zeit, um mich mit dem Programmierstil von Perl anzufreunden. Sicherlich gibt es Anwendungen, bei denen man besser in Java programmiert. Jedoch hat sich Perl vom reinen Administratoren-Tool zu einer vielseitig einsetzbaren Programmiersprache gemausert, speziell im Umfeld der Webprogrammierung ist sie heute die Nummer eins. Nicht zu unterschätzen ist die Zeitersparnis bei der Implementierung von Anwendungen in Perl gegenüber anderen Programmiersprachen. Unschlagbar ist Perl in punkto Textbearbeitung, sind doch reguläre Ausdrücke vollständig im Sprachwortschatz von Perl integriert. Jeder, der schon einmal Formulardaten verarbeitet hat, weiß ein Loblied darauf zu singen.

16

1

Einführung

Oft wird bei Webanwendungen die Performance von CGI-Skripts mit Perl gegenüber Servlets in Java bemängelt, doch wir werden in diesem Buch sehen, dass diese Behauptung nicht richtig ist. Wenn Sie die Antwortgeschwindigkeit von CGI-Anwendungen mit »mod_perl« gesehen haben, werden Sie begeistert sein! Für mich stellt sich die Frage, welche Programmiersprache für welches Problem verwendet werden soll, nur noch sehr selten, da die Antwort fast immer Perl ist. So entwickelte ich vor einiger Zeit zum Beispiel ein Testprogramm, das mehrere LDAP-Server verschiedener Hersteller vergleichen sollte. Für die Erzeugung der Testdaten mussten Hashtabellen mit sehr vielen eindeutigen Schlüsseln erzeugt werden. Zunächst implementierte ich das Programm in Java, musste jedoch sehr schnell zu Perl übergehen, da die »Virtual Machine« (das ist sozusagen der Interpreter in Java) bereits nach etwa 60000 Tabelleneinträgen den Geist aufgab, sprich abstürzte, während für Perl auch die stattliche Anzahl von einer Million Einträgen kein Problem darstellte. Ein wesentliches Merkmal von Perl ist die Plattform-Unabhängigkeit, da Perl-Skripts in der Regel nicht als direkt vom Prozessor ausführbare Binärdateien vorliegen, sondern im Quelltext (englisch Sourcecode), der von einem Interpreter zeilenweise gelesen und nach einer Übersetzung in Maschinencode von diesem Interpreter ausgeführt wird. Dies hat Vor- und Nachteile. Der größte Vorteil ist, dass solche Skripts ohne Änderungen auf unterschiedlichen Rechnerarchitekturen laufen (obwohl man diesen Vorteil natürlich durch spezielle Programmierung wieder zunichte machen kann, indem man z.B. Betriebssystemkommandos vom Skript aus startet). Der größte Nachteil besteht darin, dass man keine Perl-Skripts ohne eine Installation des Perl-Interpreters ausführen kann (das gilt im übrigen nicht nur für Perl, sondern für alle Skript-basierten Sprachen, auch für Java). In der heutigen Zeit kann man jedoch davon ausgehen, dass auf Unix-Rechnern Perl fast immer installiert ist, und die Installation von Perl unter Windows ist ein Kinderspiel. Perl unterstützt sowohl prozedurale als auch objektorientierte Programmierung. Perl ist modulbasiert, d.h. der Funktionsumfang einer Perl Distribution lässt sich durch Einbinden weiterer Perl Module beliebig erweitern. Perl ist kostenlos, ebenso alle im Internet verfügbaren Zusatzmodule.

1.2 Wie installiert man Perl? Perl ist sowohl im Quellcode (Source Distribution) als auch für alle gängigen Betriebssysteme und Rechnervarianten als Binärpaket frei im Internet erhältlich. Auf LinuxSystemen ist Perl bereits fertig installiert mit in der Linux-Distribution enthalten.

Wie installiert man Perl?

17

Meist genügt es, die vorkompilierten Binärpakete von Perl zu installieren (unter Windows als selbstinstallierende exe-Datei, unter UNIX als RPM-, PKG-Datei oder als tarArchiv). In der Perl-Distribution enthalten sind Entwicklungs- sowie Laufzeitumgebung (das ist bei Perl dasselbe) sowie eine umfangreiche Dokumentation. Die derzeitig aktuellste Version von Perl ist 5.6.1. Vor allem für Windows-Anwender interessant: Auf der Website von ActiveSTATE findet man besonders zusammengestellte Binärdistributionen, deren Funktionsumfang über die Standardzusammenstellung von Perl hinausgeht. Es sind dort bereits Zusatzmodule eingebunden, die man sich normalerweise aus dem Internet besorgen müsste. Eine Übersicht der von ActiveSTATE frei erhältlichen binären Perl-Distributionen erhält man über folgenden URI: http://www.activestate.com/Products/Download/Download.plex?id=ActivePerl Man kann natürlich auch über den Menüpunkt DOWNLOADS der Perl-Homepage http://www.perl.com sowohl Binär- als auch Source-Distributionen herunterladen. Hinweis für Windows Benutzer Unter NT und Windows 98 benötigt man mindestens die Version 1.1+ (NT) bzw. 2.0+ (Windows 98) des Windows-Installationsprogramms (englisch »Windows Installer« oder auch »MSI Installer« genannt), bevor man die Perl-Distribution von ActiveSTATE installieren kann (bei Windows 2000 ist die benötigte Installer-Version bereits Bestandteil des Betriebssystems). Den Installer kann man über einen Hypertextlink ebenfalls von ActiveSTATE herunterladen und installieren (danach muss das System neu gebootet werden, wer hätte etwas anderes erwartet). Es ist zwar auch möglich, die Perl-Distribution als so genanntes AS-Package (das ist eine zipDatei, die man auspacken muss) zu installieren, diese Version enthält jedoch keinen Uninstaller. Merken Sie sich unbedingt das Verzeichnis auf der Festplatte, in dem Perl installiert wird. Nachdem die Perl-Distribution installiert ist, sollte man überprüfen, ob der Perl-Interpreter im Suchpfad der Kommandozeilen-Shell enthalten ist. Unter UNIX ist das die Bourne-Shell, die Kourne-Shell, die bash (frei verfügbare Shell) oder eine ähnliche Shell, unter Windows ist es die DOS-Box, die als Binärprogramm cmd.exe im Systempfad unter Windows abgelegt ist. Die Programmdatei des Perl-Interpreters liegt im Unterverzeichnis bin der Perl-Distribution und heißt unter UNIX einfach perl, unter Windows ist es die Datei perl.exe.

18

1

Einführung

Am einfachsten ruft man zu diesem Zweck den Perl-Interpreter mit dem Schalter -v auf, damit gibt das Programm nur einen informativen Text einschließlich der Version aus und beendet sich anschließend. Hier ein Beispiel auf meinem Windows-Rechner: D:\>perl -v This is perl, v5.6.1 built for MSWin32-x86-multi-thread (with 1 registered patch, see perl -V for more detail) Copyright 1987-2001, Larry Wall Binary build 630 provided by ActiveState Tool Corp. http://www.ActiveState.com Built 20:29:41 Oct 31 2001

Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit. Complete documentation for Perl, including FAQ lists, should be found on this system using `man perl' or `perldoc perl'. If you have access to the Internet, point your browser at http://www.perl.com/, the Perl Home Page.

D:\>

Wenn das System das Perl-Programm nicht finden kann und mit einer entsprechenden Fehlermeldung reagiert, dann sollte man die Umgebungsvariable PATH so erweitern, dass sie auch das Unterverzeichnis bin der Perl-Distribution enthält. In Windows-Systemen ist das Installationsverzeichnis meist C:\Perl oder C:\Programme\Perl: set PATH=%PATH%;C:\Perl\bin

Wenn die Umgebungsvariable PATH nur in der DOS-Box geändert wird, dann geht die Information nach dem Schließen der DOS-Box verloren, auch in anderen geöffneten DOS-Boxen ist sie nicht wirksam. Über die Systemeinstellungen im Menü »Umgebungsvariablen« kann man die Zuordnung permanent pro Benutzer oder auch für alle Windows Benutzer permanent speichern. In UNIX wird die Perl-Distribution oft nicht in einem einzelnen Wurzelverzeichnis installiert (das Wurzelverzeichnis wird im Kreise aller Programmierer auch »Root Directory« genannt), sondern an mehreren Orten. Die Binärprogramme landen meist im Verzeichnis /usr/local/bin oder /usr/bin, die Bibliotheken von Perl findet man häufig in /usr/local/lib/perlx oder /usr/lib/perlx, wobei das »x« für die Hauptversion der PerlDistribution steht (z.B. 5).

Wie installiert man Zusatzmodule für Perl?

19

Ausnahme: Distributionen, die sich unter /opt installieren, besitzen oft ein gemeinsames Wurzelverzeichnis. Um in UNIX das Unterverzeichnis bin der Perl-Distribution mit in den Suchpfad aufzunehmen, gibt es mehrere Möglichkeiten, normalerweise erweitert man jedoch die Umgebungsvariable PATH in der Datei .profile (Bourne-Shell, Kourne-Shell), .bashrc (bash) oder ähnlichen Startup-Dateien. Beispiel für Bourne- und Kourne-Shell: PATH=$PATH:/usr/local/bin; export PATH

Beispiel für bash: export PATH=$PATH:/usr/local/bin

Beispiel für csh: set PATH=$PATH:/usr/local/bin

1.3 Wie installiert man Zusatzmodule für Perl? Im Internet findet man tonnenweise Perl-Module, mit denen sich die Funktionalität der Basisinstallation beliebig erweitern lässt. Wollen Sie Zeiten mit einer Auflösung im Mikrosekundenbereich messen? Kein Problem, laden Sie sich das Modul Time::HiRes herunter. Brauchen Sie ein Tool, um effizient mit Datum einschließlich Zeitzone zu arbeiten? Holen Sie sich Date::DateManip. Ich könnte jetzt auf weiteren 100 Seiten mit der Liste von hilfreichen Modulen fortfahren. Die zentrale Sammelstelle für Perl-Zusatzmodule wird »CPAN« genannt. CPAN ist die Abkürzung von »Comprehensive Perl Archive Network« und bedeutet frei übersetzt »Alles Denkbare und Undenkbare für Perl«. Um einen Eindruck vom Umfang dieses Archivs zu gewinnen, verfolgen Sie doch einmal den CPAN-Hypertextlink auf der Homepage von Perl. Sie werden sich schier erschlagen fühlen ob der Unmenge an nützlichen Erweiterungen. Für die unterschiedlichen Varianten der Betriebssysteme stehen sowohl die automatische als auch die manuelle Installation von Zusatzmodulen zur Verfügung. Die automatische Installation hat den unschlagbaren Vorteil, dass andere Module, die wiederum vom zu installierenden Perl-Modul benötigt werden, automatisch mitinstalliert werden, man muss sich also nicht um zeitaufwändige und zermürbende Details kümmern. In diesem Buch möchte ich für Windows und UNIX jeweils nur die automatische Methode kurz erläutern, weitere Möglichkeiten der Installation sowie Detailinformationen finden Sie im Buch »Perl-Module«, das im gleichen Verlag erschienen ist.

20

1

Einführung

Windows-Installation von Zusatzmodulen: Wenn Sie eine Perl-Distribution von ActiveSTATE besitzen, können Sie Zusatzmodule sozusagen im Quick-and-Easy-Verfahren über ein Menü installieren, da im Unterverzeichnis bin der Perl-Distribution das Skript ppm.bat enthalten ist, das nahezu alle Arbeiten für Sie übernimmt. Informationen, wie das Skript zu handhaben ist, finden Sie ebenfalls im Buch »Perl Module«. Sie können in der Oberfläche des Skripts mit dem Befehl help aber auch eine Online Hilfe bekommen. An dieser Stelle sei nur angemerkt, dass Sie einen Proxyserver angeben müssen, falls Ihr Rechner hinter einer Firewall liegt. UNIX-Installation von Zusatzmodulen: In UNIX können Sie das in der Standard-Distribution enthaltene Perl-Modul CPAN benutzen, um weitere Zusatzmodule zu installieren. Geben Sie in der Shell das Kommando perl -MCPAN -e Shell

ein. Daraufhin werden Sie beim allerersten Aufruf einige Dinge gefragt, die meist mit dem Defaultwert beantwortet werden können. Die Anwendung führt Sie ähnlich wie das Skript ppm.bat in Windows durch die Installation. Weitere Informationen erhalten Sie in der Manualpage des Moduls CPAN. Ich werde weiter unten noch erklären, wie Sie die Perl-Dokumentation lesen.

1.4 Die Online Hilfe von Perl In der Perl-Distribution enthalten ist das Programm perldoc, mit dem man eine Online Hilfe über Perl selbst sowie über eingebaute Variablen, Funktionen, reguläre Ausdrücke, über objektorientierte Programmierung in Perl oder auch über installierte Perl Module erhält. Eine Beschreibung des Programms perldoc selbst kann man mit folgendem Aufruf in der Kommandozeile erhalten: perldoc perldoc

Hier weitere Beispiele für die Benutzung von perldoc: # Übersicht aller Hilfethemen perldoc perltoc # Hilfe für die integrierte Funktion print() perldoc -f print # Hilfe für das Perl-Modul CGI perldoc CGI

Perl-Homepage

21

# Hilfe für das Perl-Module IO::Handle perldoc IO::Handle # Hilfe für die vordefinierten Variablen in Perl perldoc perlvar # Hilfe für die integrierte Funktionen (Übersicht) perldoc perlfunc # Hilfe für die Operatoren perldoc perlop # Hilfe über Pattern Matching perldoc perlre # Hilfe über pod (Plain Old Documentation) perldoc perlpod

Das Programm perldoc besitzt keine grafische Oberfläche und wird aus der Shell über die Kommandozeile aufgerufen. Allen, die eine grafische Oberfläche bevorzugen, sei das Unterverzeichnis Docs der Perl-Distribution ans Herz gelegt. Dort sind alle Dateien der Dokumentation entweder als HTML-Seiten abgelegt, oder, falls man die Perl-Distribution von ActiveSTATE installiert hat, als kompilierte Hilfedatei, die man unter Windows über den Dateimanager mit einem Doppelklick der Maus direkt aufrufen kann.

1.5 Perl-Homepage Die offizielle Homepage von Perl ist unter http://www.perl.com im Internet zu finden. Von dort aus gelangt man über den Browser zu allen wichtigen Themen und anderen Websites, die sich mit dem Thema »Perl« beschäftigen. Unter anderem findet man hier die Anlaufstelle, um eine Perl-Distribution bzw. Zusatzmodule von CPAN herunterzuladen, die Online-Dokumentation zu Perl zu lesen, die neuesten Nachrichten zu erhalten usw. usw.

1.6 Wie sieht ein Perl-Skript aus? Bevor wir diese Frage beantworten können, müssen wir erst einmal wissen, was mit dem Begriff »Skript« (englisch: »Script«) gemeint ist. Einfach ausgedrückt ist ein Skript etwas Ähnliches wie ein Programm, das ausgeführt werden kann, indem man es zum Beispiel über die Kommandozeile der Shell aufruft. Im Gegensatz zum Programm, das Maschinencode in binärer Form enthält und somit direkt ohne Umschweife von der CPU eines Rechners ausgeführt werden kann, steht in einem Skript ASCII-Text, den man sich am Bildschirm ausgeben lassen oder mit einem Editor-Programm bearbeiten kann.

22

1

Einführung

Der ASCII Text stellt den Quellcode für einen Interpreter dar, welcher den Inhalt der Skriptdatei Zeichen für Zeichen liest, die Kommandos des Quellcodes der Reihe nach in Maschinencode umwandelt und den erzeugten binären Code dann von der CPU des Rechners ausführen lässt. Halten wir also fest: Ein Skript kann nicht direkt von der CPU ausgeführt werden, sondern benötigt immer ein Programm, den so genannten Interpreter, der die Anweisungen des Skripts in Maschinencode umwandeln muss, bevor diese unter der Kontrolle des Interpreters auf der CPU ablaufen können. Wer bereits Erfahrung mit den Programmiersprachen C oder C++ hat, kennt den Begriff »Compiler«. Dieser führt zwar die Übersetzung des gesamten Quellcodes in einem einzigen Schritt durch, speichert den entstandenen Maschinencode aber nur in einer binären Programmdatei ab, ohne das Programm anschließend auszuführen. Wie wir sehen, ist ein Interpreter nichts anderes als ein Compiler, der die Anweisungen im Skript nacheinander ausführt. Beides hat Vorteile und Nachteile, die ich an dieser Stelle nicht weiter erörtern möchte, da alle modernen Programmiersprachen wie Perl Skriptsprachen sind. Jeder, der in der DOS-Box oder in einer UNIX-Shell einmal das Kommando dir oder ls aufgerufen hat, ist im Prinzip ein kleiner Skriptprogrammierer. Die DOS-Box oder allgemein die Shell ist nämlich nichts anderes als ein so genannter KommandozeilenInterpreter, der auf Eingaben von der Tastatur wartet, jede Zeile als Anweisung auffasst, diese in Maschinencode übersetzt und ausführt. So, da wir nun wissen, was wir uns unter einem Skript vorzustellen haben, wollen wir uns nun einmal die grundsätzliche Struktur von Skripts ansehen.

Skriptdatei Hauptprogramm Anweisung 1 Anweisung 2 Anweisung 3 ... Anweisung n exit-Anweisung

Abbildung 1.1: Struktur von Perl-Skripts

Wie sieht ein Perl-Skript aus?

23

Wenn ich im Weiteren von Programm spreche, dann meine ich nicht den oben beschriebenen, binären Maschinencode, sondern den Quellcode von Skripts, der landläufig ganz einfach als Programm bezeichnet wird. Jede Skriptdatei besteht mindestens aus einem Hauptprogramm, in dem Anweisung für Anweisung der Reihe nach abgearbeitet wird. Das Hauptprogramm nennt man auch »main«.

1.6.1 Was sind Statements? Wieder ein kleiner Exkurs in Englisch: Der Begriff »Anweisung« heißt im Englischen »Statement«, was wir am Ende dieses Buches hoffentlich auswendig können. In Perl wird jedes Statement, wie fast in allen Programmiersprachen, mit einem Semikolon, landläufig auch als Strichpunkt bekannt, abgeschlossen. Die letzte Anweisung eines Hauptprogramms ist die »exit«-Anweisung, mit der das Programm explizit beendet wird. Sie kann auch entfallen, dann wird das Programm nach Abarbeiten der letzten Anweisung automatisch beendet. Meine Empfehlung: Schreiben Sie Ihren Quellcode immer so ausführlich wie möglich, das hilft ungemein, den Code später zu verstehen. Die exit-Anweisung sollte also nicht fehlen. Die exit-Anweisung kann jedoch auch mehrfach an beliebigen Stellen des Hauptprogramms stehen, in jedem Fall gilt aber, dass der Interpreter das Programm beendet, ohne dass weitere Anweisungen ausgeführt werden.

1.6.2 Was sind Direktiven? Direktiven sind nichts anderes als Statements, jedoch werden sie nicht vom Interpreter ausgeführt, sondern dienen der Kommunikation zwischen Skript und Interpreter. Mit Direktiven lädt man z.B. andere Perl-Module in sein Programm oder stellt bestimmte Eigenschaften des Interpreters ein. Die wohl wichtigste Direktive ist use. Jedes Perl Skript sollte am Beginn des Quellcodes die Direktive use strict;

enthalten. Damit stellt man den Interpreter auf »stur«, wenn man flapsigen Code schreibt. Perl verweigert dann nämlich den Dienst mit entsprechenden Fehlermeldungen. Die oben dargestellte Direktive macht im Prinzip nichts anderes, als das Perl-Modul strict.pm zu laden, wie wir später noch sehen werden. Das Modul gehört jedoch zu einer speziellen Modulgruppe, die man »Pseudomodule« nennt, weil es auch interne Einstellungen im Interpreter vornimmt.

24

1

Einführung

Ich komme später noch einmal auf das Thema zu sprechen, hier will ich Ihnen den Unterschied zwischen Direktive und Statement nur kurz am folgenden Quellcode erläutern: Die Zeile 03 des Beispiels enthält eine Direktive, während in Zeile 05 ein Statement steht. Sehen wir uns doch gleich ein sehr einfaches Perl-Skript an, das den oben gezeigten Aufbau hat (die Nummern am Anfang jeder Zeile gehören natürlich nicht zum Programmcode, sondern dienen lediglich der Nummerierung der einzelnen Zeilen des Skripts): 01 02 03 04 05 06 07

#!/usr/bin/perl -w use strict; print( "Alles klar oder?\n" ); exit( 0 );

Das gesamte Skript besteht aus einem Hauptprogramm mit insgesamt 3 Anweisungen (englisch: »Statements«). Es gibt nur den Text »Alles klar oder?« und einen Zeilenvorschub am Bildschirm aus. Sie verstehen den Code nicht? Kein Problem, mit ein wenig Geduld werden wir bald so weit sein. Nur die allererste Zeile möchte ich hier genauer erklären:

1.6.3 Die Hashbang-Zeile Was bedeutet »Hashbang«? Nun, nichts anderes, als dass die Zeile mit einem Hashzeichen gefolgt von einem Ausrufezeichen beginnt. Sie wird bei nahezu allen Skripts, die von einer Shell aus aufgerufen werden, benutzt, um dem Kommandozeilen-Interpreter mitzuteilen, welches Programm für die Ausführung des Skripts benutzt werden soll. Wie wir ja wissen, können Skripts im Gegensatz zu Programmen nicht direkt ausgeführt werden, sondern benötigen immer ein Interpreter-Programm. Nach dem »Hashbang« folgt der Dateiname des Interpreters (in unserem Fall ist der Interpreter unter /usr/bin/perl zu finden, das sieht mir ganz nach UNIX aus). Zu guter Letzt kann man in der ersten Zeile noch Argumente angeben, die dem Interpreter von der Shell übergeben werden. In unserem Beispiel lautet das übergebene Argument -w: Es schaltet den Perl-Interpreter in den Warnungsmodus, er gibt dann verschiedene Meldungen aus, wenn Laufzeitfehler auftreten. Hier ein kleines Beispiel, das den Inhalt der Variablen $v ausgibt. Aus der Kommandozeile einer DOS-Box heraus wird der Perl-Interpreter aufgerufen. Anschließend habe ich ein paar Kommandos eingegeben, die ich am Ende mit der Tastenkombination (Strg) + (Z) abschließe. Da ich in der Variablen $v nichts abgelegt habe, wird auch nichts ausgegeben:

Wie sieht ein Perl-Skript aus?

25

D:\temp>perl use strict; my $a; print( $v ); ^Z D:\temp>

Ich habe beim Aufruf des Perl-Interpreters den Schalter -w weggelassen, dies will ich nun nachholen: D:\temp>perl -w use strict; my $a; print( $v ); ^Z Use of uninitialized value in print at - line 3. D:\temp>

Nun verhält sich der Interpreter anders als vorher und gibt eine Warnung aus, dass die Variable $v nicht initialisiert ist. Was das genau bedeutet, werden wir später noch sehen, an dieser Stelle sei nur angemerkt, dass ich Sie höflich bitte, diesen Schalter IMMER anzugeben. Glauben Sie mir, er ist mehr als nur nützlich. Die Hashbang-Zeile muss übrigens immer die erste Zeile im Quellcode sein, es darf also auch keine Leerzeile davor stehen. In Perl-Modulen, die kein Hauptprogramm enthalten, kann die Hashbang-Zeile entfallen. Wie Sie sehen, sind auch Leerzeilen im Skript vorhanden. Das ist durchaus gewollt, um die einzelnen logischen Einheiten des Programms optisch voneinander zu trennen, und dient der besseren Lesbarkeit des Quellcodes. Ich werde mich in Anhang A noch ausführlicher zu diesem Thema in Form eines Styleguides für guten Programmierstil äußern. Manche Programmierer denken immer noch, Platz im Editorfenster sei Mangelware, und schreiben den Quellcode vielleicht so: 01 #!/usr/bin/perl -w 02 use strict;print "Alles klar oder?\n";exit 0;

Wie wir deutlich sehen, ist das Skript nun wesentlich kürzer geworden. Aber ich denke, schon bei diesem sehr einfachen Beispiel tut man sich schwer, auf Anhieb zu erkennen, was das Programm eigentlich macht. Also: Weniger ist nicht immer mehr. Häufig werden bestimmte Sequenzen von Anweisungen an mehreren Stellen des Hauptprogramms benötigt. Diese Sequenzen lagert man in so genannte Unterfunktionen oder auch kurz Funktionen (englisch: »functions« oder »subroutines«) aus. Damit

26

1

Einführung

vermeidet man, dass der betreffende Programmcode in Duplikaten mehrfach aufgeschrieben werden muss. Funktionen werden auch verwendet, um den Programmcode übersichtlicher zu gestalten. Im Gegensatz zum Hauptprogramm wird eine Funktion nicht mit einer exit-Anweisung, sondern mit einer return-Anweisung beendet. Das folgende Bild demonstriert die erweiterte Struktur eines Perl-Skripts:

Skriptdatei Hauptprogramm Anweisungen exit-Anweisung

Funktion 1 Anweisung 1 Anweisung 2 ... return-Anweisung Es können beliebig viele Funktionsblöcke folgen

Abbildung 1.2: Erweiterte Struktur von Perl-Skripts

Als Anschauungsmaterial noch einmal das erste Skript, Diesmal erfolgt die Ausgabe des Textes nicht im Hauptprogramm, sondern in einer Funktion, die vom Hauptprogramm aufgerufen wird: 01 02 03 04 05 06 07 08 09 10 11

#!/usr/bin/perl -w use strict; printItOut(); exit( 0 ); sub printItOut { print( "Alles klar oder?\n" ); }

Wie sieht ein Perl-Modul aus?

27

Auch bei diesem Beispiel gilt: Verstehen werden wir den Code erst später. Einen wichtigen Punkt möchte ich Ihnen aber schon hier nicht vorenthalten:

1.6.4 Exit-Status von Skripts Jedes Programm (und damit auch jedes Skript) gibt nach der Beendigung seines Hauptprogramms einen numerischen Status an das Betriebssystem zurück. Damit lässt sich überprüfen, ob ein Fehler aufgetreten ist oder nicht. Grundsätzlich gilt: Im Erfolgsfall gibt ein Skript immer den Status 0 zurück, bei einem Fehler hat sich in UNIX der Status 1 eingebürgert. Wie man im Skript einen numerischen Status zurückgibt, sehen wir im obigen Beispiel in Zeile 07.

1.6.5 Kommentare in Perl Bevor Sie sich in den folgenden Zeilen den Kopf darüber zerbrechen, was Zeilen mit einem führenden Hashzeichen # bedeuten: Das Hashzeichen leitet einen Kommentar ein, der Rest der Zeile wird also nicht vom Perl-Interpreter beachtet. Man kann in Perl-Skripts an fast allen Stellen des Codes Kommentare einfließen lassen. Ein Perl-Kommentar beginnt mit dem Hashzeichen # und erstreckt sich bis zum Ende der Zeile. Perl unterstützt keine mehrzeiligen Kommentare. Beispiel: # Das ist ein Kommentar als eigenständige Zeile print( "Hallo" ); # Kommentar am Ende einer Zeile

Als Regel des guten Programmierstils gilt: Man sollte seinem Mitteilungsbedürfnis ruhig freien Lauf gewähren und so viele Kommentare wie möglich im Quellcode einfließen lassen. Nebenbei bemerkt unterstützt Perl zusätzlich weitere Arten der Dokumentation von Quellcode. Wir werden weiter unten noch näher darauf eingehen.

1.7 Wie sieht ein Perl-Modul aus? Ich habe eingangs immer wieder den Begriff »Zusatzmodule von Perl« erwähnt. Bisher wissen Sie nur, dass man darunter etwas Nützliches versteht, nun wollen wir uns ansehen, was sich in Wirklichkeit dahinter verbirgt: Ein Perl-Modul ist im Wesentlichen nichts anderes als ein Perl-Skript ohne Hauptprogramm. Sehen wir es uns in einer Abbildung an:

28

1

Einführung

        

       

       

        

Abbildung 1.3: Struktur von Perl-Modulen

Das Auffälligste gegenüber Perl-Skripts ist, dass Perl-Module kein Hauptprogramm enthalten. Module dienen der Erweiterung von bestehender Funktionalität und werden von Perl-Skripts (oder auch von anderen Perl-Modulen) benutzt, ähnlich wie dies bei Funktionen der Fall ist. Perl-Module erhalten über die package-Direktive einen Namen (siehe Bild). Direktiven sind, wie weiter oben bereits erwähnt, spezielle Anweisungen, die nicht als Programmcode ausgeführt werden, sondern für besondere Interaktionen mit dem Interpreter verwendet werden. Wie wir im Bild sehen, habe ich dem Modul den Namen »MyPackage« gegeben. Der Modulname, den man mit der package-Direktive vergibt, ist in der Regel mit dem Dateinamen verknüpft, in welcher der Programmcode des Moduls abgespeichert wird. Der Dateiname besteht aus demselben String wie der Modulnname, an den die Endung .pm angehängt wird. Unser Beispielmodul MyPackage sollte also den Dateinamen MyPackage.pm haben. Sie sollten Modulnamen (und damit auch die entsprechenden Dateinamen) immer mit einem Großbuchstaben beginnen, auch unter Windows. Eine wichtige Eigenart von Perl-Modulen ist das letzte Element im Bild, nämlich eine Zeile mit dem Statement 1;. Das bedeutet nichts anderes, als dass das Modul an den aufrufenden Programmcode den Status 1 zurückliefert. Ohne diese Zeile läuft gar nichts, der Interpreter verweigert schlicht den Dienst mit einer Fehlermeldung, wenn

Wie sieht die Skriptumgebung in Perl aus?

29

man versucht, ein Modul zu laden, das diese Zeile nicht enthält. Nun gut, haken wir es unter dem Thema »Ist so und nicht zu ändern« ab und schreiben unsere Module in etwa nach folgendem Schema: package aNewPackage; use strict; # Programmcode ... 1;

Wie man wegen des Strichpunktes nach der 1 bereits vermuten wird, handelt es sich bei der letzten Zeile um nichts anderes als um ein Statement, das aus einer Konstanten besteht. Damit Sie sehen, wie der Quellcode wirklich aussieht, habe ich in diesem Codebeispiel auf die Zeilennummerierung verzichtet. Die Nummerierung von Zeilen in diesem Buch dient wie gesagt nur dem Zweck, Ihnen erklären zu können, in welcher Zeile was passiert.

1.8 Wie sieht die Skriptumgebung in Perl aus? Das folgende Schaubild zeigt die Vernetzung von Skript, Funktionen der StandardDistribution und Zusatzmodulen sowie Funktionen in selbst implementierten Modulen (im Beispiel werden die Module »Util« und »Log« verwendet).  

             

          

                                     

       

           

           

Abbildung 1.4: Typische Skriptumgebung in Perl

30

1

Einführung

Wie wir sehen, kann man aus dem Hauptprogramm (und natürlich auch aus Funktionen des Skripts) heraus nicht nur auf Funktionen innerhalb der Skriptdatei und der Standardmodule zugreifen, sondern auch auf die Funktionen der Zusatzmodule (die in einem Standardverzeichnis der Perl-Distribution installiert sind), sowie auf beliebige, selbst implementierte oder auch kopierte Modulfunktionen, die nicht Bestandteil der Perl-Distribution sind und sich irgendwo auf der Festplatte befinden können. Wir werden weiter unten noch sehen, was man tun muss, damit der Perl-Interpreter Module findet, wenn sie von einem Skript geladen werden. An dieser Stelle sei erwähnt, dass sich Zusatzmodule und selbst implementierte Module nur dadurch unterscheiden, dass Zusatzmodule grundsätzlich in einem ganz bestimmten Unterverzeichnis der Perl-Distribution installiert und somit Bestandteil der Perl-Installation werden, während Module der letzteren Art normalerweise an beliebiger Stelle auf der Festplatte gespeichert und damit nicht Bestandteil der PerlInstallation sind. Über Module gibt es noch viele interessante Dinge zu sagen (was ich auch tun werde), aber an dieser Stelle möchte ich Sie damit nicht überfordern, immerhin wissen wir ja noch nicht einmal, was die einzelnen Anweisungen in den bisherigen Quellcode-Beispielen bedeuten.

1.9 Wie findet Perl Module? Alle Module der Standard-Distribution von Perl werden im Unterverzeichnis lib abgelegt, während Zusatzmodule im Unterverzeichnis site landen. In diesen Verzeichnissen, sowie im aktuellen Verzeichnis der Shell, in der ein Skript ausgeführt wird, sucht der Perl Interpreter nach Modulen, wenn sie vom Skript mit der Direktive use geladen werden. Ein kleines Beispielskript soll dies erläutern (Windows-Installation): #!D:/Perl/bin/perl.exe -w print( join( ", ", @INC ), "\n" );

Wie wir an der Hashbang-Zeile sehen, habe ich einen Windows-Rechner benutzt, auf dem Perl unter D:\Perl installiert ist. Das print()-Statement gibt alle Verzeichnisse aus, in denen vom Interpreter nach Modulen gesucht wird. Was die Variable @INC bedeutet, werden wir in Anhang B sehen. Wenn das Skript ausgeführt wird, erhalten wir als Ausgabe: D:/Perl/lib, D:/Perl/site/lib, .

Wie findet Perl Module?

31

Wie wir sehen, sucht der Interpreter nur in diesen Verzeichnissen nach Modulen. Oft hat man selbst entwickelte Module irgendwo auf der Festplatte, die man in seinen Skripts benutzen möchte. Damit Perl diese Module finden kann, muss man die Liste der Suchpfade erweitern. Am einfachsten geht das mit der Umgebungsvariable PERLLIB. Auch hierzu ein Beispiel. Wir erweitern die Liste um das Verzeichnis D:\myModules, indem wir vor dem Aufruf des Skripts die Umgebungsvariable PERLLIB setzen (Hinweis für UNIX: Das Setzen der Umgebungsvariable funktioniert genauso wie weiter oben für die Variable PATH gezeigt): D:\>set PERLLIB=D:\myModules

Wenn wir nun das Skript noch einmal aufrufen, erhalten wir: D:\myModules, D:/Perl/lib, D:/Perl/site/lib, .

Die Liste der Suchpfade ist nun um unser eigenes Verzeichnis erweitert (die Betonung liegt auf »erweitert«, da die Liste in jedem Fall die Standard-Suchpfade enthält). Zu erwähnen ist noch, dass der Interpreter bei der Suche nach Modulen die Liste der Reihe nach durchläuft. In unserem Beispiel würde also als Erstes im Verzeichnis D:\myModules gesucht werden.

1.10 Wie werden Skripts ausgeführt? Bis jetzt haben wir zwar gelernt, wie Skripts aussehen, wissen aber noch nicht, wie man sie ausführt. Das will ich jetzt nachholen: Am einfachsten führt man ein Skript aus, indem man in der Shell den Perl-Interpreter aufruft und ihm als Argument in der Kommandozeile den Dateipfad des Skripts angibt (das Verzeichnis, in dem das binäre Programm perl bzw. perl.exe steht, muss in der Umgebungsvariable PATH enthalten sein, was aber bei einer Standard Installation von Perl zumindest unter Windows und Linux der Fall) ist: D:\>perl -w C:/tst.pl

Das Beispiel zeigt den Aufruf von Perl unter Windows mit dem Skript C:\tst.pl als Argument. Wie wir sehen, kann (und sollte) statt des Backslashs »\« als Verzeichnistrenner der wesentlich schönere Trenner Slash »/« verwendet werden. Der Schalter -w stellt den Interpreter auf »bei nicht initialisierten Daten bitte Warnungen ausgeben« ein.

32

1

Einführung

Ein Skript kann aber auch direkt über die Kommandozeile ausgeführt werden. Der Perl Interpreter wird dann implizit von der Shell gestartet (die Hashbang Zeile gibt der Shell den Pfad zum Interpreter an).

1.10.1 Was ist ein Skriptargument? Wenn man ein Skript von der Shell aus aufruft, kann man wie bei Binärprogrammen Aufrufparameter angeben. Im Programmcode des Skripts kann man die Argumente dann über die vordefinierte Variable @ARGV auslesen. Was diese Variable bedeutet, werden wir bald erfahren. Sie wird außerdem in Anhang B erläutert.

1.10.2 Skripts in UNIX ausführen Bevor in UNIX ein Programm oder ein Skript ausgeführt werden kann, muss man die Datei mit dem »chmod«-Kommando als ausführbar kennzeichnen. Nehmen wir eine Standard-Installation von Perl unter Linux an. Dort ist der Perl-Interpreter in der Regel unter /usr/bin/perl zu finden. Wenn wir nun ein Skript mit dem Pfad /tmp/myScript.pl erstellt haben, dann müssen wir zunächst folgendes Kommando aufrufen: chmod +x /tmp/myScript.pl

Dieses Kommando ändert den Modus der Datei /tmp/myScript.pl so, dass man es direkt über die Kommandozeile aufrufen kann. Bitte nicht vergessen: Die Hashbang-Zeile des Skripts muss wie folgt aussehen: #!/usr/bin/perl -w

Nun können wir das Skript direkt aufrufen: /tmp/myScript.pl

1.10.3 Skripts in Windows ausführen In Windows sieht es ähnlich aus, nur entfällt die Änderung des Datei-Modus, weil das Betriebssystem eine feste Zuordnung zwischen Dateiendung und Dateityp macht. Als Beispiel wollen wir das Skript C:\temp\myScript.pl nehmen. Die Hashbang-Zeile muss z.B. so aussehen (die Perl-Installation sei unter D:\Perl): #!D:/Perl/bin/perl.exe -w

Wir führen das Skript nun direkt aus: C:\temp\myScript.pl

Wie findet Perl Module?

33

1.10.4 Ausführen kurzer Programme Wenn man nur ein paar Anweisungen ausprobieren möchte, dann kann man sich das Erstellen des Quellcodes in einem Editor ganz sparen, indem man in der Kommandozeile der Shell einfach nur den Perl-Interpreter ohne ein Skript als Argument aufruft. Man landet dann im interaktiven Modus von Perl, bei dem man den Quellcode Zeile für Zeile eintippt und am Ende unter UNIX ^D, unter Windows ^Z gefolgt von einem Zeilenvorschub, eingibt. Beispiel für Windows: D:\>perl -w use strict; print( "hallo\n" ); ^Z hallo D:\>

Die ersten vier Zeilen des Listings zeigen die Tastatureingaben, nach dem ^Z sieht man die Ausgabe des Programms. Meist jedoch sieht die Sache unter anderem so aus: D:\>perl -w use strict; print( $v, "\n" ); ^Z Name "main::v" used only once: possible typo at - line 2. Use of uninitialized value in print at - line 2.

Was ist hier passiert? Nun, wir versuchen, den Inhalt einer Variable $v auszugeben, ohne dass wir diese vorher initialisiert haben. Aufgrund des Schalters -w beim Aufrufen des Interpreters schimpft dieser in Form einer Compiler-Fehlermeldung, dass wir die Variable nur ein einziges Mal benutzen, was meist auf einen Fehler hindeutet (aber nicht immer der Fall ist, wie wir weiter unten noch sehen werden). Anschließend meldet sich Perl gleich noch einmal mit einer Laufzeit-Fehlermeldung, dass die Variable nicht initialisiert ist. Für die Ungeduldigen: Ich komme auf Variablen weiter unten noch ausführlich zu sprechen. Noch ein Wort zu »Compiler-Fehlermeldung« und »Laufzeit-Fehlermeldung«: Eine »Compiler-Fehlermeldung« erscheint dann, wenn der Interpreter z.B. auf einen Syntaxfehler im Quellcode während des Parse-Phase in der Übersetzungszeit stößt (der Code wird eingelesen und geprüft) der Quellcode wird zu diesem Zeitpunkt noch nicht ausgeführt. Fehlermeldungen zur Laufzeit treten auf, wenn der Quellcode bereits

34

1

Einführung

kompiliert worden ist und von der CPU im Maschinencode ausgeführt wird. Diese Art von Fehlern sind üblicherweise die schlimmsten, weil sie häufig nur in bestimmten Situationen auftreten, die man bei den Tests des Programms während der Entwicklungsphase nicht vorhergesehen hat. Die Ausgabe at- bedeutet, dass das Einlesen des Quellcodes von der Standard-Eingabe (meist ist das die Tastatur) erfolgte. Wenn ein normales Skript ausgeführt wird, das in einer Datei abgespeichert ist, stünde an Stelle des Minuszeichens der Pfadname der Skriptdatei.

1.10.5 Prüfen von Perl-Skripts Oft ist es ratsam, ein Perl-Skript vom Interpreter nur auf seine Syntax überprüfen zu lassen, bevor man es ausführt. Für diese Prüfung kann man beim Aufruf des Interpreters den Schalter -c angeben. Wollen wir uns ein Beispiel ansehen: D:\>perl -cw C:/temp/myScript.pl

Der Schalter -c wird einfach durch Zusammenfassen mit dem Schalter -w verknüpft. Die Ausgabe im Erfolgsfall (was gerade zu Beginn oft ein Glücksfall ist, und Glücksfälle sind selten, wie wir aus leidvoller Erfahrung wissen) sieht etwa so aus: C:/temp/myScript.pl syntax OK

Kleiner Tipp meinerseits: Hat man in seinem Skript Statements in einem BEGIN-Block, dann werden diese schon bei der Syntaxprüfung des Interpreters während der Übersetzungszeit ausgeführt, alle anderen Statements im Quellcode werden während dieser Phase nur überprüft, aber nicht ausgeführt. Auf BEGIN-Blocks werden wir weiter unten noch näher eingehen.

1.10.6 Inline-Dokumentation im Quellcode Unter Inline-Dokumentation versteht man eine Dokumentation des Quellcodes innerhalb derselben Datei, also dem Perl-Skript oder Perl-Modul selbst. Die einfachste Art der Inline Dokumentation haben wir bereits in Form von Kommentaren kennen gelernt. Hier noch mal ein Beispiel: #!/usr/bin/perl -w # Mit diesem Kommentar kann ich den folgenden Code # näher beschreiben: Hier wird nur Text ausgegeben print( "hallo allerseits\n" ); my $i = 0; # Integer Variable

Wie findet Perl Module?

35

Eine weitere Möglichkeit, den Quellcode im Skript oder Modul selbst zu dokumentieren, sei in folgendem Beispiel demonstriert (man beachte die Hashbang-Zeile. Diesmal scheinen wir eine Windows-Umgebung zu haben; auf den Slash als Trenner in Verzeichnisnamen komme ich weiter unten noch zu sprechen): 01 02 03 04 05 06 07 08 09 10 11 12

#!D:/Perl/bin/perl.exe -w print( "bla bla\n" ); exit( 0 ); __END__ Mein Skript Doku zu meinem Skript ...

Der erste Teil des Skripts sieht ganz normal aus, es wird einfach nur ein Text ausgegeben. Die Zeile 07 jedoch ist neu. Diese und die darauf folgende Leerzeile teilen dem Perl-Interpreter mit, dass hier das Skript zu Ende ist. Alles, was nach Zeile 08 steht, wird vom Interpreter ignoriert und eignet sich somit hervorragend für eine möglichst ausführliche Dokumentation. Wie wir sehen, wurde in dem Beispiel das HTML-Format für die Doku benutzt, im Prinzip können aber beliebige Formate verwendet werden (auch der Inhalt einer Word-Datei könnte hier stehen, es würde den Interpreter völlig kalt lassen). Statt des Identifiers (zu deutsch »Bezeichner«) __END__ kann man übrigens auch __DATA__ verwenden; beide bewirken dasselbe. Wichtig ist nur, dass danach in jedem

Fall eine Leerzeile stehen muss. Es gibt noch eine weitere Art der Inline-Dokumentation, die so genannte pod-Dokumentation. Das Kürzel »pod« steht für »plain old documentation«, das sollte man aber nicht zu wörtlich nehmen. Der Vorteil von pod gegenüber Kommentaren ist, dass man den Text der Doku auszeichnen kann (z.B. durch Fettdruck oder Ähnliches). Außerdem sind verschiedene Formatierprogramme für pod verfügbar, mit denen sich die pod Dokumentation unter anderem in HTML oder anderen Ausgabeformaten anzeigen lässt. Wie wäre es, wenn Sie mit dem Kommando perldoc perlpod

ein bisschen schmökern und die dort beschriebenen Möglichkeiten einfach ausprobieren?

1.10.7 Namenskonventionen für Perl-Skripts und Perl-Module Hier heißt es aufpassen, wenn man des Öfteren zwischen UNIX und Windows wechselt. UNIX ist generell case sensitive, während in Windows normalerweise kein Unterschied zwischen Groß- und Kleinbuchstaben gemacht wird.

36

1

Einführung

Dateinamen von Perl-Skripts beginnen mit einem Kleinbuchstaben und haben normalerweise die Endung .pl (in UNIX muss das nicht so sein, in Windows ist es die Regel, weil Windows eine feste Zuordnung zwischen der Endung des Dateinamens und dem Dateityp besitzt). Leerzeichen im Dateinamen verbieten sich von selbst, da sie speziell in UNIX tödlich für viele Anwendungen sind. Dateinamen von Perl-Modulen beginnen mit einem Großbuchstaben und sollten immer die Endung .pm haben (Preisfrage: wofür steht wohl »pm«?). Ansonsten gilt dieselbe Regel für den Dateinamen wie bei Perl-Skripts. Außerdem sollte man sich angewöhnen, für den Packagenamen dieselbe Bezeichnung zu wählen wie für den Dateinamen (natürlich ohne die Endung .pm). Beispiel für das Modul in der Datei Util.pm: package Util; ... 1;

1.10.8 Verzeichnistrenner Als Bill Gates das Betriebssystem DOS an IBM verkaufte, hat er den Backslash »\« als Trennzeichen für Verzeichnisse auserkoren. Aus heutiger Sicht kann man sagen, dass dies so ziemlich die ungünstigste Wahl war, die man treffen konnte, weil der Backslash in sämtlichen Programmiersprachen eine besondere Bedeutung hat. Beispiel für eine Pfadangabe in Windows: C:\WINNT\system32

In UNIX würde derselbe Pfad etwa so aussehen: /dev/c/winnt/system32

Wie wir sehen, wird unter UNIX der Slash »/« als Zeichen für den Verzeichnistrenner verwendet. Perl ist so konzipiert, dass alle Pfade im Dateisystem mit dem Verzeichnistrenner »/« angegeben werden können, egal, auf welchem Betriebssystem man ein Skript ausführt. Deshalb sollten Sie sich tunlichst angewöhnen, nur Slashes als Verzeichnistrenner zu verwenden, niemals Backslashes. Die beiden folgenden Pfade in einem Perl-Skript sind in Windows für Perl also identisch: "D:/Programme/myProgram" "D:\\Programme\\myProgram"

In UNIX stellt sich diese Frage erst gar nicht, dort gibt es keine Backslashes als Verzeichnistrenner.

Wie findet Perl Module?

37

1.10.9 BEGIN und END Wie ich weiter oben bereits erwähnt habe, wird ein Skript in der Übersetzungszeit vom Interpreter zunächst gelesen und auf seine Syntax hin geprüft. Man nennt diesen Vorgang auch »parsen«. Anschließend werden die Anweisungen im Skript der Reihe nach kompiliert und ausgeführt; dies geschieht in der sogenannten »Laufzeit«. Manchmal ist es jedoch notwendig, bestimmte Dinge zu erledigen, bevor der Interpreter den eigentlichen Code übersetzt und ausführt. Hierzu dient der spezielle Programmblock BEGIN. Nun werden Sie sagen, was ist ein Programmblock? Ein Programmblock ist nichts anderes als eine Reihe von Anweisungen (englisch »Statements«), die von geschweiften Klammern umgeben sind. Wir werden später noch auf die Bedeutung von geschweiften Klammern zurückkommen, und glauben Sie mir, geschweifte Klammern können alles Mögliche bedeuten! Also merken Sie sich schon mal vor: geschweifte Klammern = wichtig. Der Clou eines BEGIN-Blocks ist, dass alle darin enthaltenen Anweisungen interpretiert und ausgeführt werden, bevor der Rest des Skripts vom Interpreter überhaupt gelesen wird. In einen solchen BEGIN-Block packt man also alle Anweisungen, die in jedem Falle vom Interpreter ausgeführt werden, bevor er Anweisungen des Hauptprogramms selbst ausführt. Ein Beispiel: Wir wollen im Perl-Skript Module verwenden, die der Interpreter normalerweise nicht findet, weil sie nicht im Standardverzeichnis für Perl-Module stehen. Deshalb müssen wir dafür sorgen, dass vor dem Lesen des Skripts das Verzeichnis, in welchem sich die benutzten Module befinden, in den Suchpfad des Interpreters mit aufgenommen werden. Das tun wir im BEGIN-Block. Angenommen, wir benutzen ein Modul namens »MyUtil«, das in der Datei D:\myModules\MyUtil.pm gespeichert ist. Wenn wir das Modul in einem Skript benutzen wollen, muss der Perl-Interpreter in der Lage sein, es auch zu finden. Dies teilen wir mit folgendem Code mit: #!D:/Perl/bin/perl.exe -w BEGIN { unshift( @INC, "D:/myModules" ); } use MyUtil; ...

Bevor der Interpreter die use-Anweisung interpretiert und ausführt, werden die Statements im BEGIN-Block ausgeführt. Dort wird der Suchpfad zum Auffinden von PerlModulen um das Verzeichnis erweitert, in dem die benötigten Module stehen. Wenn

38

1

Einführung

der Interpreter also auf die Direktive trifft, in der das Modul »MyUtil« geladen werden soll, kann er es finden, da er nun auch unter dem Verzeichnis D:\myModules danach sucht. Eine weitere Möglichkeit, den Suchpfad für Perl Module zu erweitern, besteht darin, die Umgebungsvariable PERLLIB zu setzen, oder beim Aufruf des Perl Interpreters den Schalter -I anzugeben. Beispiel für Windows: D:\>set PERLLIB=D:\myModules

Unter UNIX (für bash): /home/hemu: % export PERLLIB=/home/hemu/myModules

UNIX (für sh): $ PERLLIB=/home/hemu/myModules ; export PERLLIB

So viel zum BEGIN-Block. Wollen wir uns nun dem END-Block zuwenden. Dieser wird ähnlich wie der BEGIN-Block vor dem Rest des Skripts interpretiert, die darin enthaltenen Statements werden aber erst dann ausgeführt, wenn der zugehörige Geltungsbereich, in welchem der END-Block definiert ist, verlassen wird. Das hört sich ziemlich akademisch und damit automatisch unverständlich an. Sagen wir es in einfachen Worten: Besitzt ein Perl-Skript einen END-Block, dann werden die darin enthaltenen Anweisungen ohne Wenn und Aber ausgeführt, wenn das Skript beendet wird, egal auf welche Weise. Benutzt wird der END-Block häufig, um verwendete Ressourcen wieder freizugeben, wenn das Skript beendet wird. Eine Ressource kann zum Beispiel eine geöffnete Datei sein. Im END-Block kann man auch den exit Status eines Skripts abfragen oder sogar verändern. Beispiel für die Benutzung eines END-Blocks: 01 02 03 04 05 06 07

#!/usr/bin/perl -w use strict; use FileHandle; my $fh = new FileHandle( "/tmp/bla.txt", "w" );

Wie findet Perl Module? 08 09 10 11 12 13 14

39

# hier folgt weiterer Programmcode exit( 0 ); END { if ( $fh ) { $fh->close(); } }

Erläuterungen: In Zeile 06 wird die Datei /tmp/bla.txt für schreibenden Zugriff angelegt. Damit wird zugleich eine Ressource des Betriebssystems in Form eines so genannten »FileHandles« verbraucht (FileHandles werden in einem eigenen Abschnitt behandelt). Um sicherzustellen, dass die belegte Ressource wieder freigegeben wird, wenn das Skript beendet wird (das Skript kann nach dem Anlegen der Datei vom Interpreter auch hart abgebrochen werden, weil ein Programmfehler aufgetreten ist), geben wir das FileHandle im END-Block wieder frei. Da der END-Block in jedem Fall vor Beendigung des Skripts durchlaufen wird, ist sichergestellt, dass die Systemressource auch in Fehlerfällen wieder freigegeben wird.

2 Grundlagen Dieses Kapitel behandelt die Grundlagen der Programmiersprache Perl. Sie sind zwar meist ermüdend, aber leider notwendig, man will ja schließlich mehr als nur kopierte Skripts ausführen können. Sie lernen hier alles, was zum Programmieren in Perl wichtig ist, angefangen von Datentypen über Variablen, Operatoren, Funktionen und Modulen, bis hin zur Datenverarbeitung im Dateisystem (I/O). Sehen wir uns als Erstes an, was Perl an Datentypen zu bieten hat.

2.1 Grundbegriffe Bevor ich Sie mit unverständlichen Begriffen bombardiere, möchte ich einen kleinen Überblick geben, der manche Begriffe kurz erläutert: Begriff

Kurzbeschreibung

Skript

Ein Skript ist Perl-Code, der in einer Datei mit der Endung .pl abgespeichert ist (dies ist allerdings nur eine Namenskonvention, um ein Perl-Skript zu kennzeichnen, vor allem in UNIX kann ein Perl-Skript in einer Datei mit beliebigem Namen gespeichert sein). Ein Skript enthält mindestens ein Hauptprogramm (im Englischen nennt man dies »main«). Zusätzlich kann ein Skript auch Funktionen enthalten (die man historisch bedingt auch »Unterfunktionen« nennt). Skripts sind im Prinzip Programme, mit dem Unterschied, dass sie keinen Binärcode enthalten, der direkt von der CPU ausgeführt wird, sondern von einem Interpreter verarbeitet werden müssen.

Hauptprogramm

Darunter versteht man denjenigen Programmteil eines Skripts, der nicht als Funktion oder Unterfunktion definiert ist. Er wird durch den Interpreter ausgeführt.

Tabelle 2.1: Grundbegriffe

42

2

Grundlagen

Begriff

Kurzbeschreibung

Package

Ein Package ist derjenige Programmcode, der in einem Modul mit der packageDirektive gekennzeichnet wird. In den meisten Fällen ist ein Package der Name eines Perl-Moduls ohne die Datei-Endung .pm. Ein Package besteht im Wesentlichen aus Funktionen, Variablendefinitionen und Initialisierungscode, enthält aber kein Hauptprogramm.

Modul

Unter einem Modul versteht man eine Datei mit der Endung .pm. Sie enthält PerlCode ohne Hauptprogramm. Ein Modul ist meist identisch mit einer Datei, die ein Package gleichen Namens enthält (ohne die Endung .pm). Sie kann aber auch mehrere Packages enthalten.

Funktion, Unterfunktion

Eine Funktion (historisch bedingt auch »Unterfunktion« genannt) enthält Programmcode, der meist aus mehreren Anweisungen besteht, die bestimmte Aktionen durchführen und in einem Block zusammengefasst werden. Über so genannte »Funktionsparameter« oder auch »Funktionsargumente« kann man einer Funktion dynamisch zur Laufzeit Daten übergeben und somit das Verhalten einer Funktion von außen beeinflussen. Fast immer geben Funktionen dem Aufrufer einen Status zurück, der Erfolg oder Misserfolg kennzeichnet.

Direktive

Eine Direktive ist in Perl meist identisch mit einer Funktion, hat aber einen anderen Zweck. Mit Direktiven stellt man das Verhalten des Perl-Interpreters ein. So kann man z.B. weitere Module in den Hauptspeicher laden oder den Interpreter anpassen, so dass er z.B. Warnungen ausgibt (oder auch nicht). Die wichtigsten Direktiven in Perl sind: 왘 use 왘 require

왘 no Operator

Operatoren werden benötigt, um Daten miteinander zu verküpfen. Sie kennen sicherlich den »+«-Operator, der eine Addition durchführt. Er hat zwei so genannte »Operanden«, einen links, einen rechts (z.B. 5 + 3).

Bareword

In Perl ist alles, was nicht in so genannten »Quotes« (zu Deutsch Anführungszeichen) steht, ein Bareword (zu Deutsch: »nacktes Wort«). Immer dann, wenn der Perl-Interpreter beim Lesen des Programmcodes auf ein Bareword trifft, versucht er, dieses Bareword als Funktionsname, Packagename oder Konstantenname zu interpretieren. Wir werden im Weiteren noch sehr häufig auf diesen Begriff stoßen. Hier nur als Beispiel: use strict; enthält einen Funktionsaufruf und ein Bareword als Argument. use ist der Funktionsaufruf (der eigentlich eine Direktive darstellt), strict ist ein Bareword und bedeutet: »Lade bitte das Modul strict.pm«.

Tabelle 2.1: Grundbegriffe (Forts.)

Wenn Sie jetzt noch nicht alle Begriffe verstanden haben, macht das nichts, sie werden im weiteren Verlauf ausgiebig erläutert. Beginnen wir mit dem Einfachsten:

Datentypen

43

2.2 Datentypen Perl besitzt nur 3 fest im Sprachwortschatz eingebaute (englisch: »built-in«) Datentypen: Skalare, Arrays und Hashes (Hashes werden auch »assoziative Arrays« genannt). Die beiden letztgenannten Datentypen werden auch als Listen bezeichnet. Im Folgenden sind in den Beispielen Variablen und Funktionen enthalten, die erst in späteren Abschnitten erklärt werden. Denken Sie sich nichts dabei, wenn Teile der Beispiele noch unverständlich sind, der Aha-Effekt kommt später. Zunächst wollen wir einen Blick auf den skalaren Datentyp werfen:

2.3 Skalare Skalare sind Zahlen, Strings (Zeichenketten) oder Referenzen auf andere Daten. Sie zeichnen sich dadurch aus, dass sie nur einen einfachen Wert beinhalten. Referenzen sind ähnlich wie in der Programmiersprache C Zeiger auf andere Daten wie zum Beispiel Variablen, Funktionen etc. und enthalten die Speicheradresse des Ziels, auf das sie zeigen. Sie können überall verwendet werden, wo skalare Daten erlaubt sind, was sie zu einem sehr leistungsfähigen Programmiermittel macht. Eine Referenz wird durch Voranstellen eines Backslashs »\« gekennzeichnet. Neben dieser Art von Referenzen, die man im Englischen auch als »soft references« bezeichnet, gibt es noch harte Referenzen (englisch: »hard references«), die durch ein Sternchen »*« gekennzeichnet werden. Auf diesen Referenztyp wird hier nicht eingegangen, weil man sie schlicht so gut wie nie benötigt. Nach so vielen für den Laien nichts sagenden Begriffen nun ein paar Beispiele für skalare Werte: 3 # Ganze Zahl (Integerzahl) -3.14 # Festkommazahl 7.04E-12 # Gleitkommazahl 7.04e-12 # dasselbe "hallo" # String '17' # Zahl als String angegeben "17" # noch einmal dasselbe anders "das\tist\nein String mit Steuerzeichen" 'das\tist\nein String ohne Steuerzeichen' \"Referenz auf diesen String"

44

2

Grundlagen

2.3.1 Behandlung von Zahlen Perl unterstützt sowohl Integerwerte (ganze Zahlen) als auch Festkomma- und Gleitkommawerte für Zahlen. Integerwerte können sowohl im Dezimalsystem als auch zu den Basen 2 (Dualsystem), 8 (Oktalsystem) und 16 (Hexadezimalsystem) angegeben werden. Der Exponent von Gleitkommawerten wird gekennzeichnet durch Voranstellen des Buchstabens E oder e, beide Varianten sind erlaubt. Hier einige Beispiele: # Integerwerte 7 -3 0x7f # Hexzahl 0377 # Oktalzahl 0b100110 # Dualzahl -0x3A9F # negative Hexzahl # Festkommawerte 3.7 -17.4 # Hex-, Oktal- oder Dualzahlen als # Festkommawerte sind nicht erlaubt. 0b1.01 # ist ungültig 0x3.14 # ebenso ungültig Gleitkommawerte 5.1E3 -30.5e-3 # Werte mit Unterstrich für Tausenderstellen 3_756_455 (entspricht 3756455) # Das Komma als Dezimalpunkt ist nicht erlaubt! 320,17 # liefert einen Fehler

Für Zahlenumwandlungen aus dem Hexadezimal- und dem Oktal- bzw. Dualsystem in das Dezimalsystem stehen die Funktionen hex() sowie oct() zur Verfügung. Diese Funktionen werden weiter unten noch näher besprochen.

2.3.2 Darstellbarer Zahlenbereich In Perl hängt der darstellbare Zahlenbereich von der Breite der CPU-Register ab. Bei den heute gängigen 32-Bit-Rechnern ist die größte Integerzahl also 4 *1024 *1024 *1024 (ohne Vorzeichen). Diese Einschränkung betrifft vor allem Bit-Operationen. Bei arithmetischen Operationen können Integerzahlen bis (z.B. 100.000.000.000.000) ohne Verlust von Präzision verarbeitet werden.

ca. 10E14

Skalare

45

Mit den Zusatzpaketen Math::BigInt und Math::BigFloat können beliebig große Zahlen verarbeitet werden, jedoch sinkt in diesem Fall die Performance.

2.3.3 Kennzeichnung von Strings (Quoting) In Perl werden Zeichenketten (Strings) entweder in doppelte Anführungszeichen »"« (englisch: »double quote«) oder in einfache Anführungszeichen »'« (englisch: »single quote«) gesetzt. Sie können alle Zeichen, auch Sonderzeichen und sogar binär 0 enthalten. Bei Zeichenketten in einfachen Anführungszeichen interpretiert Perl jedes Zeichen literal (genau so, wie es geschrieben ist), während bei Strings in doppelten Anführungszeichen eine Interpretation von Sonderzeichen erfolgt. Sonderzeichen werden durch Voranstellen eines Backslash »\« gekennzeichnet. Folgende Zeichen in Strings mit doppelten Anführungszeichen haben eine besondere Bedeutung: Sonderzeichen

Bedeutung

\n

Zeilenvorschub

\t

Tabulator

\r

Wagenrücklauf (DOS und MAC)

\\

Backslash

\"

doppeltes Anführungszeichen

\'

einfaches Anführungszeichen

\x{zzzz}

Zeichen im Unicode-Format (zzzz ist der Hexcode des Zeichens)

Das Dollarzeichen »$« und das At-Zeichen »@« haben in Zeichenketten, die durch doppelte Anführungszeichen angegeben werden, ebenfalls eine besondere Bedeutung, die wir später bei der Behandlung von Variablen kennen lernen werden. Die Tabelle liefert einige Beispiele: String

Beschreibung

"einfacher String"

String in doppelten Anführungszeichen.

'einfacher String'

String in einfachen Anführungszeichen.

"String mit '"

Einfaches Anführungszeichen ist Bestandteil des Strings.

'String mit "'

Doppeltes Anführungszeichen ist Bestandteil des Strings.

"String mit \""

Hier muss vor dem doppelten Anführungszeichen ein Backslash stehen, weil der String selbst bereits durch doppelte Anführungszeichen dargestellt ist.

46

2

Grundlagen

String

Beschreibung

'String mit \''

Hier muss vor dem einfachen Anführungszeichen ein Backslash stehen, weil der String selbst bereits durch einfache Anführungszeichen dargestellt ist.

"String mit \n"

\n wird als Sonderzeichen für Zeilenvorschub interpretiert.

'String mit \n'

Hier sind \n zwei literale Zeichen ohne besondere Bedeutung.

"String mit \x{0041}"

\x{0041} wird als Unicode interpretiert und damit zum Zeichen "A".

'String mit \x{0041}'

Hier wird \x{0041} literal ohne besondere Bedeutung behandelt.

"String mit $Zeichen"

Das Dollarzeichen wird als Kennzeichen für eine skalare Variable interpretiert.

'String mit $Zeichen'

Das Dollarzeichen hat keine besondere Bedeutung.

"String mit @Zeichen"

Das At-Zeichen wird als Kennzeichen für eine Array Variable interpretiert.

'String mit @Zeichen'

Das At-Zeichen hat keine besondere Bedeutung.

Hinweis für C- und Java-Programmierer Perl kennt keinen Datentyp char: Dieser wird wie ein String behandelt, der nur aus einem Zeichen besteht. Gibt man im Skript einen String mit dem Sonderzeichen für Zeilenvorschub \n aus, dann hängt es vom Betriebssystem ab, ob wirklich nur ein Zeilenvorschub (UNIX) oder aber zusätzlich ein Wagenrücklauf \r, z.B. in DOS, ausgegeben wird. Ich habe schon Fälle erlebt, wo Programmierer deshalb Stunden und Tage auf Fehlersuche waren. Dieses Verhalten kann man abschalten, wenn man vor der Ausgabe das betreffende Ausgabemedium mit der Funktion binmode() auf Binärmodus umstellt. Beispiel: print( "hallo\n" ); # Das Statement gibt unter UNIX exakt # den angegebenen String aus # Unter Windows wird der String "hallo\r\n" ausgegeben # Auf einem Macintosh wird der String # "hallo\r" ausgegeben

Weiter unten komme ich noch einmal auf dieses Thema zurück.

Skalare

47

2.3.4 Der skalare Wert »undef« Dieser Pseudowert wird bei skalaren Daten verwendet, um diesen einen nicht definierten Zustand zu geben. In Funktionen kann man den Wert undef an den Aufrufer zurückgeben, um einen Fehler anzuzeigen. Meist jedoch macht sich der Wert undef unangenehm in Fehlermeldungen des Interpreters bemerkbar. Ich habe noch keinen Entwickler gesehen, der ein umfangreiches Programm entwickelt hat, ohne solche Fehlermeldungen zu erhalten. Viele Module, die man aus dem Internet herunterladen kann, funktionieren in dem Moment nicht mehr, in dem man den Schalter -w von Perl aktiviert, weil sie unsauber geschrieben sind oder nicht initialisierte Variablen enthalten. undef ist insofern ein Pseudowert, als dass er im Gegensatz zu Zahlenwerten oder

Stringwerten das absolute Nichts darstellt (vergleichbar mit NULL-Werten in einer Datenbank). Konkret bedeutet undef bei Variablen, dass der Interpreter noch keinen Speicherplatz für eine Variable reserviert hat, die Variable also noch nicht initialisiert ist. Hierzu ein kleines Beispiel: 01 02 03 04

#!D:/Perl/bin/perl.exe -w my $i; print( "i = $i\n" ); exit( 0 );

In Zeile 02 wird die skalare Variable $i deklariert (ohne Initialisierung), in Zeile 03 soll der Variablenwert ausgegeben werden. Wenn wir das Skript ausführen, erhalten wir folgende Fehlermeldung: Use of uninitialized value in concatenation (.) or string at C:\temp\tst.pl line 3. i =

Durch eine kurze Überlegung ist klar, was passiert: Mit der Deklaration der Variable allein wird im Hauptspeicher noch kein Speicherplatz für den Wert der Variable belegt (englisch: »allocated«, neudeutsch: »alloziert«). In Zeile 03 soll aber der Wert der Variable ausgegeben werden. Dafür ist jedoch ein belegter Speicherplatz Voraussetzung, deshalb die Fehlermeldung. Wenn wir nun den Schalter -w weglassen: #!D:/Perl/bin/perl.exe my $i; print( "i = $i\n" ); exit( 0 );

dann erhalten wir folgende Ausgabe: i =

48

2

Grundlagen

Wie wir sehen, fehlt die Fehlermeldung des Interpreters. Ich möchte jedoch betonen, dass damit zwar die Auswirkung, nicht aber die Ursache behoben ist, der Fehler ist nach wie vor im Programm. Also merken wir uns: Immer den Schalter -w verwenden. Man sollte sich angewöhnen, Variablen bei ihrer Deklaration immer einen Wert zuzuweisen, auch wenn dieser undef ist. Damit sieht derjenige, der den Quellcode liest, sofort, was gemeint ist: #!D:/Perl/bin/perl.exe -w my $i = undef; ...

Wie wir später noch sehen werden, stellt Perl die Funktion defined() zur Verfügung, mit deren Hilfe man überprüfen kann, ob eine Variable initialisiert ist oder nicht. Bei Listen (Arrays und Hashes) gibt es den Wert undef nicht. Das Gefährliche ist nur, dass der Interpreter keine Fehlermeldung ausgibt, wenn man versucht, einem Array den Wert undef zuzuweisen. Bei Hashes hingegen wird ein Fehler gemeldet. Mehr zu diesem Verhalten, wenn wir Arrays kennen lernen.

2.3.5 Boolesche Werte Ein Boolean-Wert kann im Gegensatz zum Dezimalsystem, wo wir 10 Ziffern haben, nur in zwei verschiedenen Varianten vorkommen: unwahr (englisch: FALSE) oder wahr (englisch: TRUE). In vielen Programmiersprachen sind diese beiden Werte fester Bestandteil des Sprachwortschatzes in Form von reservierten Wörtern. Hier ein kleiner Auszug aus einem Java-Programm: boolean flag = false; // Hier kommt weiterer Quellcode ... if ( flag == false )

Leider existieren in Perl die booleschen Werte FALSE und TRUE nicht als reservierte Wörter, die man direkt im Programmcode benutzen kann. Im Gegensatz zu fast allen anderen Programmiersprachen ist in Perl ein Wert logisch unwahr (FALSE), wenn er entweder die Zahl 0, einen leeren String, die Ziffer 0 als einziges Zeichen eines Strings oder den Pseudowert undef enthält. Beispiele: if if if if if if

( ( ( ( ( (

0 ) # logisch false, unwahr "" ) # ebenfalls logisch false "0" ) # logisch false undef ) # gleichfalls logisch false -1 ) # logisch wahr "a" ) # logisch wahr

Listen

49

Man kann jedoch Konstanten mit einem TRUE- bzw. FALSE-Wert definieren, die den Namen true bzw. TRUE und false bzw. FALSE haben, wir werden bei der Beschreibung von Konstanten noch darauf zu sprechen kommen.

2.3.6 Referenzen Eine Referenz ist kein normaler Wert wie zum Beispiel eine Zahl oder ein String, sondern die Speicheradresse eines anderen Werts. In Perl kann man praktisch für alles eine Referenz verwenden, selbst für Programmcode. Wir werden uns weiter unten noch ausführlich mit Referenzen beschäftigen.

2.4 Listen Arrays und Hashes enthalten mehrere skalare Werte (Elemente). Man bezeichnet sie deshalb auch als Listen. Gekennzeichnet werden Listen durch runde Klammern: ( 1, "3", -4, "Stringwert" )

kennzeichnet eine Liste. In diesem Fall handelt es sich um ein Array mit 4 Elementen. Als Trennzeichen für die einzelnen Elemente der Liste wird das Komma verwendet. Wir können auch eine leere Liste angeben: ()

Eine leere Liste wird durch ein Paar runder Klammern ohne Inhalt angegeben. Ein Paar geschweifter Klammern hat eine andere Bedeutung!

2.4.1 Arrays Arrays sind Container für mehrere Elemente, die fortlaufend nummeriert werden. Das erste Element besitzt die Nummer 0. Die Nummern, unter denen die einzelnen Elemente angesprochen werden, nennt man auch Indizes. Jedes Element kann einen beliebigen skalaren Datentyp haben, es können also Zahlen und Strings sowie weitere skalare Datentypen wie Referenzen gemischt in einem Array enthalten sein. Man kann die Nummer für das erste Array-Element mit Hilfe der vordefinierten Perl-Variable $[ auch verändern, zum Beispiel auf den Wert 1. Davon möchte ich jedoch aus Portabilitätsgründen dringend abraten.

50

2

Grundlagen

Arrays sind Listen und werden als solche in runde Klammern eingeschlossen.

Beispiel für ein Array: # # # # # (

Die folgende Liste stellt ein Array bestehend aus 5 Elementen dar Jedes Element muss ein Skalar sein, es dürfen aber alle skalaren Datentypen gemischt vorkommen 17, 0, -1.3, "Stringwert", 'noch ein String', )

# Array mit 4 Elementen (Index 0 bis 3) # Das letzte Element enthält eine Referenz-Variable ( "3", 1, "wort", $referenz, ) # Leere Liste ()

Die einzelnen Array-Elemente werden durch ein Komma getrennt. Perl erlaubt ein Komma auch nach dem letzten Element. Man sollte dieses Feature nutzen, weil damit weniger Fehler entstehen, wenn man zu einem späteren Zeitpunkt weitere Elemente hinzufügt.

Auf Array-Elemente zugreifen Bis jetzt haben wir nur Listen in Form von Arrays angelegt, ohne auf einzelne Elemente des Arrays zuzugreifen. Das wollen wir jetzt nachholen. Wie in anderen Programmiersprachen auch sind eckige Klammern mit einem Index vorgesehen, um auf ein einzelnes Element des Arrays zuzugreifen: # Liste aus 3 Elementen ( "17", "hallo", 4, ) # Wir extrahieren das zweite Element "hallo" ( "17", "hallo", 4, )[ 1 ]

Mit der Zahl in eckigen Klammern gibt man die Indexnummer des Elements der Liste an, auf das man zugreifen möchte (in diesem Fall das zweite Element, da die Nummerierung bei 0 beginnt). Weitere Beispiele: ( # # #

stat( "/etc/passwd" ) )[ 7 ] liefert das 8. Element der Liste, die von der Funktion stat() zurückgegeben wird, das ist die Dateigrösse in Bytes

Listen

51

Der aufmerksame Leser wird bemerkt haben, dass um den Funktionsaufruf von stat() noch einmal runde Klammern gesetzt sind. Die Funktion gibt jedoch bereits selbst eine Liste zurück, daher könnte man meinen, dass auch folgender Code funktioniert: stat( "/etc/passwd" )[ 7 ]

Der Interpreter liefert hier jedoch einen Syntaxfehler, weil Perl einen Unterschied zwischen Listen und Array-Listen macht (auch Hashes sind ja Listen). Man muss um die Funktion Klammern setzen wie oben gezeigt, damit Perl weiß, dass es sich hier um eine Array-Liste handelt. Arrays können niemals den Pseudowert undef haben. Falls man einer Array-Variable gezielt den Wert undef zuweist, wird ein Array mit einem einzigen Element, das den Wert undef besitzt, angelegt, ohne dass der Interpreter eine Fehlermeldung ausgibt: my @array = undef; # @array hat folgenden Inhalt # ( undef, )

List-Kontext und skalarer Kontext Perl unterscheidet grundsätzlich zwischen skalarem Kontext und List-Kontext im Programm (es gibt auch noch den Void-Kontext, bei dem der Aufrufer einer Funktion keinen Rückgabewert erwartet, siehe hierzu das Beispiel unten). List-Kontext liegt immer bei Verwendung von Arrays oder Hashes vor, skalarer Kontext bei Verwendung von skalaren Variablen. Beispiele für einen skalaren Kontext: my $scalar = myFunc(); # Der Rückgabewert des Funktionsaufrufes wird einer # skalaren Variable zugewiesen, deshalb liegt hier ein # skalarer Kontext vor. my $line = ; # Einlesen einer einzelnen Zeile # von einem Eingabemedium wie der Tastatur # in skalarem Kontext # Vorsicht: my ( $line ) = ; # Das ist kein skalarer Kontext. Durch die runden # Klammern um die skalare Variable $line wird die # linke Seite der Zuweisung zu einer Liste! # Auswirkung: Es werden alle Zeilen des

52

2

Grundlagen

# Eingabemediums gelesen, die erste Zeile wird an # die Variable zugewiesen, alle weiteren werden # verworfen.

Beispiele für einen List-Kontext: my @array = myFunc(); # Der Rückgabewert des Funktionsaufrufes wird einer # Array-Variable zugewiesen, deshalb liegt hier # List-Kontext vor. my ( $scalar1, $scalar2 ) = myFunc1(); # Auch hier liegt List Kontext vor, obwohl links vom # Gleichheitszeichen nur skalare Variablen verwendet # werden. Durch die runden Klammern um die Variablen # wird ein List Kontext erzeugt. my ( $line ) = ; # Auch hier liegt ein List Kontext vor. Dieses Beispiel # werden wir weiter unten noch vertiefen.

Die print()-Funktion von Perl erzeugt immer einen List-Kontext. Hier muss man aufpassen, weil manche Funktionen unterschiedlich reagieren, je nachdem, in welchem Programm-Kontext sie aufgerufen werden: print( localtime(), "\n" );

Das Code-Fragment könnte zum Beispiel folgende Ausgabe hervorrufen: 354681301020120

Diese scheinbar sinnlose Ausgabe stellt in Wirklichkeit eine Liste aus Sekunden, Minuten, Stunden, Tag des Monats, Monat, Jahr, Wochentag, Jahrestag und Sommerzeit dar. Wir werden es gleich besser verstehen, wenn wir dieselbe Funktion in skalarem Kontext aufrufen: my $date = localtime(); print( "$date\n" );

Nun gibt die print()-Funktion das Datum so aus: Sun Jan 13 08:46:35 2002

Das können wir schon besser lesen, obwohl es sich in beiden Fällen um dasselbe Datum handelt. Wenn wir jetzt noch die einzelnen Elemente der Rückgabeliste der Funktion localtime() besser ausgeben, kann man es auch erkennen: print( join( ", ", localtime() ), "\n" );

Listen

53

Nun werden die einzelnen Elemente der Rückgabeliste durch Kommata getrennt ausgegeben: 35, 46, 8, 13, 0, 102, 0, 12, 0

Bemerkenswert ist die 0 für den Monat, die 102 für das Jahr, die 0 für den Wochentag, sowie die 12 für den Jahrestag. Dazu müssen wir wissen, dass der erste Monat des Jahres die Nummer 0 hat, das Jahr als Offset des Jahres 1900 ausgegeben wird, Wochentage mit dem Index 0 begonnen werden (der erste Wochentag ist Sonntag, der letzte ist der Samstag), ebenso wie die Nummer des Jahrestages. Ich glaube, nach diesen Erläuterungen sind die beiden Ausgaben wirklich identisch. Mit Hilfe der Funktion scalar() kann man auch skalaren Kontext erzwingen: print( scalar( localtime() ), "\n" );

Nun wird das Datum wieder als String ausgegeben. my @date = localtime(); print( scalar( @date ), "\n" );

Jetzt wird nicht etwa das Datum als String ausgegeben, sondern die Zahl 9. Das liegt daran, dass die Funktion localtime() eine Liste und keine Array-Liste zurückliefert. Die Funktion scalar() gibt bei einem Array als Argument die Anzahl der im Array enthaltenen Elemente zurück (in unserem Fall sind es 9 Elemente). Weiter oben habe ich gesagt, dass sich die Funktion localtime() unterschiedlich verhält, je nachdem, in welchem Programm-Kontext sie aufgerufen wird. Wie erkennt sie, welcher Kontext vorliegt? Die Antwort heißt wantarray(). Das ist eine Perl-Funktion, die TRUE zurückgibt, wenn sich das Programm in List-Kontext befindet, und ansonsten FALSE. Wir werden dieses Thema noch ausführlicher behandeln. Zum Schluss noch ein Beispiel für den Void-Kontext: myFunc();

Wird eine Funktion aufgerufen, ohne dass deren Rückgabewert benutzt wird, dann haben wir den klassischen Fall von Void-Kontext.

2.4.2 Hashes Ein Hash (auch assoziatives Array genannt) ist wie ein Array eine Liste und wird ebenfalls in runde Klammern eingeschlossen. Im Gegensatz zu Arrays werden die Hash-Elemente nicht über einen numerischen Index, sondern über einen Schlüssel (englisch:

54

2

Grundlagen

»Key«) angesprochen, das ist immer ein String, der dem Element einen Namen gibt (deshalb auch assoziativ, weil man sich unter dem Schlüssel etwas vorstellen kann). Der Wert eines Hash-Elements wird auch »Value« genannt und muss ein skalarer Wert sein. Einen Hash kann man sich unter anderem als Register vorstellen, das aus Karten besteht. Auf den Reitern der Karten steht der Nachname, unten steht zum Beispiel der Vorname. Wenn wir nun die Karte für »Hans Dampf« brauchen, müssen wir in den Reitern nach dem String »Dampf« suchen. Diese Suche kann vor allem dann recht lange dauern, wenn man ein Chaot ist und die Karten nicht sortiert hat. Deshalb haben wir Menschen uns ein System für die Sortierung einfallen lassen, damit man bei der Suche möglichst schnell zum Ziel kommt. Der Algorithmus für die Speicherung von Hash-Elementen in Perl ist ganz anders. Hier sind die Elemente nicht sortiert. Stattdessen wird für jeden Schlüssel der Elemente ein Hashwert berechnet und dieser in einer Liste abgelegt. Wenn man nun durch die Angabe des Schlüssels auf ein bestimmtes Element zugreift, dann sucht Perl nicht den Schlüssel, sondern erst den Hashwert des Schlüssels in der Liste. Dieser Algorithmus ist wesentlich performanter als eine Suche in sortierten Elementen. Merke Hash-Elemente sind immer unsortiert! Dies ist das wesentliche Unterscheidungsmerkmal zu Arrays, bei denen die einzelnen Elemente ja durch den Index sequenziell aufsteigend sortiert abgelegt werden. Da ein Hash-Element immer einen Key und einen Value besitzt, ist die Anzahl der resultierenden List-Elemente immer ein Vielfaches von 2. Ein Hash wird also durch Key/Value-Paare gebildet. Ebenso wie Arrays können Hashes niemals den Pseudowert undef haben, da dieser nur bei Skalaren gültig ist. Allerdings erzeugt der Interpreter bei Hashes eine Fehlermeldung, wenn man versucht, einem Hash den Wert undef zuzuweisen, was bei Arrays nicht der Fall ist. Beispiel für ein Hash: ( "fn" => "Gundel", "ln" => "Gaukel", "age" => 50, )

Die Liste im Beispiel definiert ein Hash mit insgesamt 3 Elementen (gekennzeichnet durch die Keys »fn«, »ln«, »age«). Das resultierende assoziative Array hat insgesamt 6 Elemente, weil jedes Hash-Element aus einem Key/Value-Paar besteht. Aufgrund dieser Tatsache könnte man obiges Hash auch wie folgt angeben: ( "fn", "Gundel", "ln", "Gaukel", "age", 50, )

Listen

55

Jedoch wird von dieser Art abgeraten, da man hier den Unterschied zu einem normalen Array nicht erkennen kann. Wenn ich an die Vorteile von Perl denke, fallen mir als Erstes die Begiffe »Hash« und »reguläre Ausdrücke« ein. Zu regulären Ausdrücken werden wir weiter unten noch ausgiebig kommen. Hashes sind in Perl das Vehikel, um beliebige neue Datenstrukturen aufzubauen, was besonders in der Objektorientierten Programmierung ein zentraler Punkt ist. Das erste, was man in Perl verstanden haben muss, sind Hashes. Deshalb werde ich mein Bestes geben, damit Hashes für Sie keine böhmischen Dörfer bleiben. Seit Version 5.6 von Perl darf man Hash-Keys auch als Barewords ohne Quotes angeben, da der Interpreter Keys grundsätzlich in Strings umwandelt. Ich persönlich bevorzuge allerdings die explizite String-Darstellung: # erlaubt: ( fn => "Gundel", ln => "Gaukel", age => 50, ) # explizite Angabe: ( "fn" => "Gundel", "ln" => "Gaukel", "age" => 50, )

Sie werden sicherlich zu Recht fragen: Was ist ein Bareword? Wörtlich übersetzt bedeutet »Bareword« nichts anderes als »nacktes Wort«. Beim Parsen von Quelltext muss der Interpreter genau wissen, was er gerade vor sich hat, einen String, eine Zahl, eine Variable, eine Funktion etc. Trifft der Interpreter auf ein Wort, das nicht als String oder Zahl erkannt wird, dann ist es zunächst ein »Bareword«, und der Interpreter versucht nun, dieses Bareword zuzuordnen. Er sucht also in seiner Liste von Funktionen, Konstanten und sonstigen bekannten Dingen, ob er dort einen passenden Identifier (deutsch: »Bezeichner«) findet. Verläuft diese Suche erfolglos, kommt prompt eine Fehlermeldung wie im folgenden Beispiel: #!/usr/bin/perl -w use strict; my $a = a;

In diesem Beispiel haben wir vergessen, das Zeichen a als String zu kennzeichnen, es ist deshalb für den Interpreter ein Bareword. Wenn wir das Skript ausführen, erhalten wir folgende Fehlermeldung: Bareword "a" not allowed while "strict subs" in use at /tmp/tst.pl line 3. Execution of /tmp/tst.pl aborted due to compilation errors.

56

2

Grundlagen

2.5 Konstanten Üblicherweise dürfen in Perl keine Variablenidentifier (die deutsche Übersetzung von »Identifier« ist »Kennzeichner«) ohne Typkennzeichen verwendet werden, denn Barewords sind normalerweise für Funktionsnamen vorgesehen. Mit Hilfe der use-Direktive in Verbindung mit dem Package constant (Perl-Moduldatei constant.pm) können jedoch Barewords als Konstanten (englisch: »constants«) definiert werden: # Definition use constant use constant use constant

von Konstanten PI => 4 * atan2( 1, 1 ); FALSE => 0; TRUE => !FALSE;

# Benutzung der Konstanten als Bareword: print( "PI = ", PI, "\n" ); # Falsch wäre: print( "PI = PI\n" ); # da in diesem Fall das "PI" kein Bareword ist, # sondern Bestandteil des Strings, weil es # in Quotes steht, und somit vom # Interpreter nicht evaluiert wird. # Evaluierung bedeutet, dass der Interpreter # statt des Wortes PI den Wert der Konstanten "PI" # einsetzt. my $flag = TRUE; my $otherFlag = FALSE;

Bei der Deklaration von Konstanten wird der Operator => verwendet und nicht das Gleichheitszeichen =, das wäre nämlich eine Zuweisung mit linker und rechter Seite. »Moment mal«, werden Sie sich sagen, »den Operator kenne ich doch von Hashes.« Richtig, der Ausdruck: FALSE => 0

ist tatsächlich ein Hash mit einem Element, nämlich FALSE als Key und 0 als Value. Vorsicht bei Listen: Nehmen wir eine List-Konstante für Wochentage als Beispiel: use strict; use constant WDAYS => ( "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ); print( WDAYS[ 2 ], "\n" );

Zunächst haben wir die Konstante WDAYS als Liste definiert, die alle Wochentage als Elemente enthält (wie üblich, beginnt für Programmierer die Woche mit Sonntag). Mit der print()-Funktion versuchen wir, das dritte Element »Dienstag« auszugeben, die Kon-

Konstanten

57

stante WDAYS verwenden wir nun als Array. Prompt haben wir uns eine Fehlermeldung des Interpreters eingehandelt, die in etwa so aussieht: syntax error at - line 4, near "WDAYS[" Execution of - aborted due to compilation errors.

Wie man an diesem Beispiel sieht, sind Listen und Arrays in Perl nicht identisch. Erst wenn wir die Konstante explizit als Liste kennzeichnen, indem wir sie in runde Klammern einschließen, funktioniert die Sache: print( ( WDAYS )[ 2 ], "\n" );

Mit den runden Klammern teilen wir dem Interpreter mit: Die Konstante WDAYS ist eine Liste, und von dieser möchten wir jetzt bitteschön das dritte Element ausgeben. Natürlich gibt es wie immer eine Alternative: Wenn wir die Konstante WDAYS nicht als Liste, sondern als Array-Referenz (Erklärung kommt weiter unten) deklarieren: use constant WDAYS => [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ];

dann ist es möglich, WDAYS direkt als Array anzugeben: print( WDAYS->[ 2 ], "\n" );

Auch der noch etwas seltsam anmutende Ausdruck WDAYS->[ 2 ] wird weiter unten klar werden, hier sei nur angemerkt, dass damit das dritte Element des Arrays angesprochen wird. Hinweis für Vereinfachung: Alle Elemente der Liste im Beispiel sind Strings und müssen in Quotes angegeben werden. Wenn wir den Operator qw verwenden (den ich weiter unten noch erklären werde), können wir uns Schreibarbeit sparen: use constant WDAYS => qw( Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag ); # oder als Array-Referenz: use constant WDAYS => [ qw( Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag ) ];

Eine weitere Möglichkeit, Konstanten zu definieren, bieten Funktionen. Da wir Funktionen bisher noch nicht besprochen haben, werden Sie den folgenden Programmcode vielleicht erst nach Durchlesen des Abschnitts über Funktionen verstehen: # Konstante mit "use": use constant MYCONST => 1; # Konstante mit einer Funktion: sub MYCONST { return 1; }

58

2

Grundlagen

2.6 Variablen Das Leben als Programmierer wäre ziemlich langweilig, wenn man nur mit Konstanten arbeiten könnte. Deshalb haben die Entwickler aller Programmiersprachen Variablen erfunden, die das Programmiererdasein wesentlich interessanter machen. Eine Variable ist ein Speicherplatz mit einem Namen, den man im Englischen als »Identifier« bezeichnet. In Perl wird jede Variable mit einem Typkennzeichen versehen, das vor dem Namen der Variable steht. Durch dieses Typkennzeichen kann man sogar ansonsten reservierte Namen für Variablen verwenden, was in anderen Programmiersprachen unmöglich ist. Es ist auch erlaubt, für eine skalare Variable denselben Namen zu vergeben wie für eine Array- oder Hash-Variable, da sie aufgrund des Typkennzeichens unterschieden werden können. Perl hat keine streng typisierten Variablen, die nur einen bestimmten Datentyp aufnehmen können. Es wird nur zwischen Skalaren, Arrays und Hashes unterschieden, jedoch kann eine skalare Variable sowohl Zahlen und Strings als auch Referenzen enthalten. Für die drei unterschiedlichen Datentypen sind folgende Typkennzeichen reserviert: $ für skalare Variablen (einschließlich Referenzvariablen) @ für Array Variablen % für Hash Variablen

Beispiele für Variablen in Perl: $scalarVar # Skalare Variable namens "scalarVar" @arrayVar # Array-Variable namens "arrayVar" %hashVar # Hash-Variable namens "hashVar" # Da jeder Variablentyp ein Kennzeichen besitzt, # ist auch Folgendes möglich: $var @var %var # Obwohl die Namen der Variablen gleich sind, # handelt es sich hier um drei völlig verschiedene # Variablen, da sie durch das Typkennzeichen # eindeutig gekennzeichnet sind.

Variablen

59

2.6.1 Variablennamen (Identifier) Ein Variablenname muss mit einem Buchstaben oder einem Unterstrich beginnen und darf nur aus Buchstaben, Unterstrichen sowie Ziffern bestehen. Variablennamen dürfen nicht länger als 252 Zeichen sein. (Ich glaube, dieses Limit reicht für alle denkbaren Identifier aus, selbst in Finnland.) Man sollte sich bei der Namensgebung von Variablen angewöhnen, den ersten Buchstaben des Namens klein zu schreiben. Die Ausnahme von der Regel sind Variablen, die als Konstanten benutzt werden. Deren Namen sollten komplett in Großbuchstaben sein. Alle Namen von Variablen und sonstigen Identifiern in Perl sollten grundsätzlich englischsprachig gewählt werden, denn damit schlägt man zwei Fliegen mit einer Klappe: Es gibt keine deutschen Sonderzeichen in den Namen, und jeder Programmierer auf der Welt versteht, was mit einem Namen gemeint ist, weil Englisch die Muttersprache aller Programmierer ist. Perl ist case-sensitive, d.h. zwischen Groß- und Kleinschreibung wird ein Unterschied gemacht. Beispiele: $myVar $myvar # $myvar ist eine andere Variable als $myVar # Wenn ein Variablenname aus mehreren logischen # Einheiten besteht, dann schreibt man zu Beginn # eines neuen Teils einen Großbuchstaben: $_thisIsALongVariableNameButOK # Es geht zwar auch mit dem Unterstrich als Trenner für die # einzelnen logischen Einheiten, # er sollte aber nicht verwendet werden: $_this_is_a_long_variable_name_and_should_not_be_used $123 # Ungültiger Variablenname, weil er mit einer # Ziffer beginnt $var123 # Gültiger Variablenname, # weil er mit einem Buchstaben beginnt

Ein Variablenname kann auch in geschweifte Klammern gestellt werden. Dies ist in manchen Fällen erforderlich, um den Namen einer Variable von konstantem Text zu trennen:

60

2

Grundlagen

Es soll ein Dateiname verwendet werden, der sich aus dem Inhalt der Variable $prefix und dem konstanten String _test.txt ergibt. Die folgende Zuweisung führt zu einem Fehler: my $fileName = "$prefix_test.txt";

Da der Unterstrich (_) ein gültiges Zeichen für einen Variablennamen ist, versucht der Perl-Interpreter, auf den Inhalt der Variable $prefix_test zuzugreifen, gemeint war aber die Variable $prefix, gefolgt vom konstanten Text _test.txt. Mit geschweiften Klammern kann man dieses Problem lösen: my $fileName = "${ prefix }_test.txt";

Zwischen dem Variablennamen und den geschweiften Klammern dürfen Leerzeichen stehen (dies erhöht die Lesbarkeit). Natürlich geht auch: my $fileName = "${prefix}_test.txt";

Diese Notation ist beim Pattern Matching notwendig, da in diesem Fall die Leerzeichen mit in das Pattern (deutsch: »Muster«) eingehen. Über Pattern Matching werden wir uns noch recht ausführlich unterhalten. Beispiel für das Einrahmen von Variablennamen in geschweifte Klammern und Pattern Matching: /${ prefix }_test/

führt nicht immer zum gewünschten Ergebnis, aber: /${prefix}_test/

funktioniert immer.

2.6.2 Reservierte Wörter in Perl Hier habe ich eine sehr erfreuliche Nachricht für Sie: In Perl gibt es hinsichtlich der Namensgebung von Variablen keine reservierten Wörter. Das liegt ganz einfach daran, dass vor dem Namen jeder Variable ihr Typkennzeichen stehen muss. Deshalb dürfen Sie sich alle möglichen Namen für Ihre Variablen einfallen lassen, solange sie der oben genannten Syntax entsprechen. Reservierte Wörter existieren in Perl nur für Barewords, es sind jedoch so wenige, dass man sie sich leicht merken kann. Als ich zum ersten Mal ein Perl-Buch gelesen habe, wunderte ich mich anfangs darüber, dass es keine Liste von reservierten Wörtern in Perl gab. Heute weiß ich, warum.

Variablen

61

2.6.3 Geltungsbereich von Variablen Alle Identifier (Namen von Variablen und Funktionen) haben einen mehr oder weniger eingeschränkten Geltungsbereich (englisch: »scope«), innerhalb dessen Grenzen sie gültig sind. Grundsätzlich können in Perl Variablen dort definiert werden, wo sie gebraucht werden, sie müssen also nicht unbedingt am Beginn eines Programms definiert sein. Im Gegenteil, der Geltungsbereich von Variablen sollte so klein wie möglich gehalten werden, um Nebeneffekte zu vermeiden. Dieses Ziel erreicht man durch das Bareword my, das den Geltungsbereich einer Variable so einschränkt, dass sie nur innerhalb des umgebenden Programmblocks gültig ist. Ein Programmblock ist zum Beispiel das Hauptprogramm oder die Datei eines PerlModuls. Eine Funktion ist ebenfalls ein Programmblock (richtigerweise ist der Funktionsrumpf ein Block, aber dazu später). Ganz allgemein wird ein Programmblock durch geschweifte Klammern erzeugt. Man kann auch gezielt anonyme Programmblöcke einsetzen, um kurzlebige Variablen zu benutzen, die nur innerhalb des umgebenden Blocks, begrenzt durch die geschweiften Klammern, gültig sind, z.B.: # Hauptprogramm # Die Variable $var1 ist innerhalb # des Hauptprogrammes gültig. my $var1 = 1; ... # Beginn eines anonymen Blocks, er hat im Gegensatz # zu einer Funktion keinen Namen, deshalb anonym. { # Die Variable $var3 ist nur innerhalb des Blockes, # der durch geschweifte # Klammern begrenzt ist, gültig. my $var3 = 3; # # # #

Die Variable $var1 des Hauptprogramms ist hier gültig, man kann hier aber auch eine neue Variable $var1 definieren:

my $var1 = 2; # Jetzt existiert innerhalb dieses Blocks # die äussere Variable desselben

62

2

Grundlagen

# Namens nicht mehr. # Ausserhalb des Blocks ist dann wieder nur die # äussere Variable $var1 gültig. } # An dieser Stelle ist die ursprüngliche Variable # $var1 mit dem Wert 1 wieder gültig, $var2 existiert # nicht mehr. exit( 0 ); # Ende des Hauptprogramms # Funktion "myFunc" sub myFunc { # Die Variable $var1 aus dem Hauptprogramm hat # innerhalb der Funktion # keine Gültigkeit (auch wenn Perl in diesem Fall # sehr kulant ist und # den Gebrauch der Variable meist auch in einer # Funktion zulässt). # Die folgende Variable $var2 ist nur innerhalb der # Funktion myFunc gültig. my $var2 = 2; }

Will man eine Variable innerhalb der gesamten Datei global verfügbar machen, dann kann man statt einer my-Deklaration das Bareword our verwenden: # Hauptprogramm oder Modul (*.pm) ... our $globalVar = 1; # Die Variable $globalVar ist ab der Deklaration # innerhalb derselben Datei überall gültig, # ausserdem kann die Variable von anderen Dateien über # den Packagenamen angesprochen werden, # siehe weiter unten.

Deklariert man eine Variable innerhalb eines Blocks mit our, dann gilt sie nur in diesem Block. Hier eine typische und böse Falle für Anfänger: BEGIN { our $globalVar = 1; } # Hier ist die Variable $globalVar nicht mehr gültig.

Variablen

63

Anstelle einer Deklaration mit our kann man auch mit der Direktive use Variablen des Hauptprogramms in Funktionen innerhalb derselben Programmdatei zur Verfügung stellen: # Hauptprogramm use vars ( '$var1', ); # Mit dem speziellen Operator 'qw' (quote word) geht es # auch so: use vars qw ( $var1 ); my $var1 = 1; ... sub myFunc { # Die Variable $var1 ist nun auch in der Funktion # myFunc verfügbar. }

Mit der Direktive use wird eine Liste von Variablen angegeben (deswegen die runden Klammern), die den Zustand shared erhalten, d.h. sie sind auch in Funktionen innerhalb derselben Datei verfügbar. Die List-Elemente müssen in einfache Quotes gesetzt werden, da sonst der Interpreter den Wert der Variablen einsetzen würde. Das Sonderzeichen $ muss also mit einfachen Quotes entwertet werden. Beispiel: # Die folgende Zeile führt zu einem Fehler: use strict; use vars ( "$myVar" ); my $myVar = 1;

Wenn wir versuchen, den Programmcode auszuführen, dann passiert Folgendes (der Code wurde direkt über die Kommandozeile der Shell eingegeben): D:\>perl -w use strict; use vars ( "$myVar" ); Global symbol "$myVar" requires explicit package name at - line 2.

Wenn wir die zweite Zeile des Codes mit einem Zeilenvorschub abgeschickt haben, kommen wir gar nicht mehr dazu, den Rest einzutippen, weil der Interpreter sofort die Fehlermeldung ausgibt und das Programm beendet. Unser Fehler war, so meinen wir

64

2

Grundlagen

aufgrund der Fehlermeldung, dass wir die Variable $myVar vorher deklarieren müssen, also noch ein Versuch: D:\>perl -w use strict; my $myVar = 1; use vars ( "$myVar" ); Use of uninitialized value in string at - line 3. '' is not a valid variable name at - line 3 BEGIN failed--compilation aborted at - line 3. D:\>

Was ist das nun? Perl beschwert sich darüber, dass die Variable $myVar nicht initialisiert ist, aber wir haben ihr doch eine Zeile vorher einen definierten Wert gegeben! Auskunft gibt uns die letzte Zeile der Fehlermeldung: Die Direktive use wird anscheinend ausgeführt, bevor die Deklarationszeile darüber interpretiert wird (siehe auch »BEGIN-Block von Perl-Programmen«). Richtig, die use-Direktive wird zur Übersetzungszeit vom Interpreter abgearbeitet, während das Statement darüber erst im nächsten Schritt gelesen wird. Damit kann die Variable natürlich noch nicht initialisiert sein. Der eigentliche Fehler jedoch steckt in den doppelten Anführungszeichen der Direktive use. Damit versucht der Interpreter, den Wert der noch nicht vorhandenen Variable einzusetzen, was natürlich unmöglich ist. Also machen wir es jetzt richtig: D:\>perl -w use strict; use vars ( '$myVar' ); # Es geht auch mit dem Operator "qw": # use vars qw( $myVar ); my $myVar = 1; ^Z D:\>

So, jetzt ist unsere Welt wieder in Ordnung. Keine Fehlermeldung, kein Problem. Durch die einfachen Quotes wird vom Interpreter nicht versucht, den Variablenwert zu ermitteln, da das Dollarzeichen $ nun ein ganz normales Zeichen wie A oder x ist. Will man Variablen auch Programmcode zur Verfügung stellen, der nicht in derselben Datei steht (z.B. in Perl-Modulen), dann kann man diese nicht mit dem Bareword my deklarieren.

Variablen

65

Allerdings verhindert die Anweisung use strict;, dass man den Geltungsbereich der Variablen einfach weglässt. So liefert folgender Code eine Fehlermeldung des Interpreters: # Perl-Modul Util.pm package Util; use strict; # Diese Anweisung führt zu einer Fehlermeldung: $var = 5;

Im Beispielcode ist die Anweisung package enthalten, welche dazu führt, dass der Interpreter den darauf folgenden Code in einem eigenen Geltungsbereich, oft auch Namespace genannt, verwaltet. In unserem Beispiel hat der Geltungsbereich den Namen »Util«. Auch Hauptprogramme haben einen eigenen Namespace, ohne dass man diesen explizit angeben muss, da er vordefiniert ist und den Namen main trägt. Man kann dies auch explizit angeben: #!D:/Perl/bin/perl.exe -w package main; use strict; ... exit( 0 );

Will man nun Variablen deklarieren, die von Programmcode aus anderen Dateien heraus angesprochen werden sollen, muss man den voll qualifizierten Geltungsbereich für die Variablen angeben. Diesen erhält man, indem man zwischen das Typkennzeichen der Variable und den Namen der Variable den Namen des Packages schreibt und vom Variablennamen durch einen doppelten Doppelpunkt »::« trennt: # Perl-Modul (Datei Util.pm) package Util; use strict; # Variablendeklaration mit Angabe des # voll qualifizierten Geltungsbereichs $Util::var = 5; # Auch die Deklaration mit der our-Direktive # ist möglich: our $var1 = 10;

66

2

Grundlagen

Benutzt werden können so definierte Variablen von Programmcode in anderen Dateien wie folgt: # Hauptprogramm # Mit der folgenden Direktive lädt der Interpreter das # Modul Util: use Util; print( $Util::var, "\n" ); print( $Util::var1, "\n" );

Globale Variablen eines Skripts können natürlich auch im Hauptprogramm mit der voll qualifizierten Angabe des Geltungsbereichs verwendet werden: #!/usr/bin/perl -w use strict; $main::debugLevel = 1; ... print( "Debug level = $main::debugLevel\n" );

Allerdings lege ich Ihnen die Deklaration solcher globalen Variablen, die innerhalb der gesamten Datei gültig sind, mit dem Bareword our ans Herz, denn damit spart man sich lästige Schreibarbeit.

2.6.4 Skalare Variablen Skalare Variablen werden mit dem Typkennzeichen $ versehen, das vor dem Variablennamen steht. Perl wandelt unterschiedliche skalare Variablentypen um, falls dies möglich ist: my $var1 = 1; my $var2 = "3"; # Addition my $var3 = $var1 + $var2; # $var3 enthält 4 # Aneinanderhängen von Strings my $var4 = $var1 . $var2; # $var4 enthält "13";

Eine skalare Variable ist also nicht auf einen bestimmten skalaren Typ festgelegt, sondern kann zu verschiedenen Zeitpunkten alle mögliche Arten von skalaren Werten enthalten. Perl wandelt den Typ automatisch um. Allerdings liefert der Interpreter eine Fehlermeldung, wenn die implizite Umwandlung eines Datentyps aufgrund von Inkompatibilität unmöglich ist:

Variablen

67

my $var1 = 1; my $var2 = "a"; my $var3 = $var1 + $var2; # Fehlermeldung $var3 = $var1 . $var2; # Zulässig, da hier in String # umgewandelt wird

Aber folgendes Beispiel funktioniert: my $v1 = "1\n"; my $v2 = 5; print( $v1 + $v2 ); # Es wird "6" ausgegeben, da Zeilenende-Zeichen bei der # Konvertierung entfernt werden

Normalerweise werden Sonderzeichen in Strings, die durch einfache Quotes definiert werden, nicht als Sonderzeichen, sondern als literale Zeichen interpretiert. Ist ein einfaches Quote Bestandteil eines Strings, der durch doppelte Quotes definiert wurde, dann ist das einfache Quote kein Sonderzeichen mehr. Deswegen wird die Variable im folgenden Beispiel evaluiert (der gespeicherte Variablenwert wird eingesetzt): my $val = 50; my $string = "Inhalt der Variable var = '$var'"; # $string enthält "Inhalt der Variable var = '50'"

Macht man es umgekehrt: $string = 'Inhalt der Variable var = "$var"';

dann wird $var nicht evaluiert, sondern der String enthält exakt die Zeichenkette: Inhalt der Variable var = "$var"

Alle skalaren Variablen können den Pseudowert undef enthalten. In diesem Fall ist Vorsicht bei der Verarbeitung geboten. So können keine Variablen ausgegeben werden, die undef sind. Perl liefert hier eine Laufzeit-Warnung. Beispiel: my $var; # Die folgende Anweisung liefert eine Laufzeit-Warnung, # da die Variable $var # nicht initialisiert ist. print( "var = $var\n" );

68

2

Grundlagen

Besonders lästig sind Listen (in der Regel Arrays), die mit der print()-Funktion von Perl ausgegeben werden sollen. Enthält ein Element der Liste den Wert undef, dann wird eine Laufzeitmeldung ausgegeben: print( ( 1, 2, undef, 4, 5 ) ); # Beim Versuch, das dritte Element auszugeben, erzeugt # die print()-Funktion eine Warnungsmeldung.

Definition von Variablen Wie oben bereits erwähnt, sollte man Variablen immer mit dem kleinstmöglichen Geltungsbereich definieren, um unerwünschte Nebeneffekte zu vermeiden. In der Regel wird dafür das Bareword my verwendet. Meist werden Variablen bei der Deklaration gleich initialisiert: # Variable, die später mit einem brauchbaren Wert # belegt wird. Sie sollte zumindest mit "undef" # initialisiert werden. my $toBeFilledLater = undef; # Variable, die bei ihrer Deklaration selbst # bereits mit dem Rückgabewert einer Funktion # initialisiert wird. my $date = localtime();

Will man mehrere Variablen gemeinsam definieren und initialisieren, dann verwendet man den List-Operator in Form von runden Klammern: my ( $secs, $mins, $hours ) = localtime();

Diese Art, Variablen zu definieren, wird sehr häufig verwendet, vor allem auch in Funktionen, wie wir später sehen werden. Die Codezeile enthält mehr als nur eine Initialisierung mehrerer Variablen: Durch den List-Operator in Form von runden Klammern entsteht eine Zuweisung an eine Liste. Die Perl-Funktion localtime() wird also in List-Kontext aufgerufen und gibt somit keinen skalaren String, sondern eine Liste zurück (mehr Informationen über localtime() finden Sie in Anhang C). Diese Liste rechts vom Gleichheitszeichen wird nun in die links vom Gleichheitszeichen stehende Liste kopiert. $secs erhält das erste Element der Rückgabeliste, $mins das zweite, und $hours das dritte Element. Alle weiteren Rückgabeelemente werden verworfen. Hätten wir die Zuweisung so geschrieben: my $secs, $mins, $hours = localtime();

dann würden wir sofort eine Fehlermeldung vom Interpreter »ernten«, weil der ListOperator fehlt.

Variablen

69

Wichtig ist auch, dass Sie sich Folgendes einprägen: # Aufruf der Funktion und Zuweisung an $secs # in List-Kontext: # Die Funktion liefert eine Liste zurück. # Deren erstes Element (die Sekunden) # wird in das erste Element der links vom # Gleichheitszeichen stehenden Liste kopiert, # also in $secs abgespeichert. my ( $secs ) = localtime(); # Dasselbe in skalarem Kontext: # Die Funktion liefert einen skalaren String, # der direkt in der Variable abgespeichert wird. my $secs = localtime(); # Mit der Perl-Funktion "scalar()" wird ein # skalarer Kontext für den Funktionsaufruf # von localtime() erzwungen, sie gibt also # einen skalaren String zurück. # Dieser wird an eine Liste bestehend aus einem # Element (unsere zu definierende Variable) # zugewiesen. Der Perl-Interpreter wandelt # hier implizit den skalaren Rückgabewert # in eine Liste um und kopiert das erste # Element in die Variable $secs. my ( $secs ) = scalar( localtime() );

Wir werden dieses Konstrukt der Variablendefinition noch sehr häufig antreffen, merken Sie sich also bitte: Eine Variablendefinition mit dem List-Operator stellt immer List-Kontext her.

2.6.5 Array-Variablen Array-Variablen werden mit dem Typkennzeichen @ versehen, das vor den Variablennamen gestellt wird. Die Elemente eines Arrays müssen wiederum Skalare sein. Im Gegensatz zu skalaren Variablen können Array-Variablen niemals undef sein: Ein Beispiel, wie man es nicht machen sollte: my @array = undef; # @array ist dennoch definiert, es enthält 1 Element, # das den Wert undef hat: ( undef, ) # Der Interpreter liefert keine Fehlermeldung!

70

2

Grundlagen

Initialisiert werden Array-Variablen mit dem List-Operator: # leeres Array my @array = (); # Array mit 3 Elementen my @array1 = ( 1, "drei", 5, );

Auf einzelne Elemente des Arrays kann man durch Angabe des numerischen Index in eckigen Klammern zugreifen: $array1[ 0 ] # greift auf den skalaren Wert des ersten # Elements zu, das die Zahl 1 enthält $array1[ 2 ] = 1; # Das 3. Element des Arrays erhält den # Wert 1

Wir haben gelernt, dass ein Dollarzeichen $ als Typkennzeichen vor dem Variablennamen auf eine skalare Variable hindeutet. Die Variable array1 ist jedoch eine ArrayVariable. Ist das ein Widerspruch? Mitnichten. Mit dem Ausdruck $array1[ 0 ]

greifen wir ja nicht auf das Array als solches zu, sondern vielmehr auf ein Element des Arrays. Da Elemente von Arrays grundsätzlich Skalare sein müssen, stimmt die Sache wieder. Dass die Variable array1 wirklich eine Array-Variable ist, erkennt man daran, dass hinter dem Variablennamen eine eckige Klammer steht. Natürlich kann man auch den Index in einer Variable speichern und diese anstelle einer konstanten Nummer verwenden: my $ind = 2; $array1[ $ind ] # # # #

Greift auf das Element mit dem Index zu, der durch die Variable $ind angezeigt wird (hier also auf das 3. Element).

Mehrere Array-Elemente können gezielt durch eine kommaseparierte Liste von Indizes extrahiert werden (beachte das @-Zeichen anstelle von $): my @extract = @array1[ 0,2 ]; # @extract enthält ( 1, 5 ) $array[ 0,2 ] ist falsch, @array[ 0,2 ] ist richtig!

Wir wollen ja nicht ein skalares Element extrahieren, sondern eine Teilmenge des Arrays. Diese ist zwangsläufig wiederum eine Array-Liste.

Variablen

71

Beispiel für den Auszug einiger Werte von localtime(): # Zuweisung des Monats, der Stunde und der Minute # an entsprechende Variablen über eine # List-Definition mit Zuweisung aus einer # Teilliste: my ( $mon, $hours, $minutes ) = ( localtime() )[ 4, 2, 1 ];

Perl bietet im Zusammenhang mit Arrays eine spezielle Variable an, die den Index des letzten Elements des Arrays enthält. Der Name dieser (skalaren) Variable wird gebildet aus $# und dem Namen der Array-Variable: # Leeres Array initialisieren my @array = (); # Index des letzten Elements lesen my $lastIndex = $#array; # $lastIndex enthält -1, da das Array leer ist. # Array neu initialisieren @array = ( 1, 3, 4 ); # $#array enthält nun 2 (Index des letzten Elements)

Man kann auch ein Array mit einer bestimmten Anzahl von Elementen initialisieren: # Leeres Array initialisieren my @array = (); # Index des letzten Elements auf die Zahl 999 setzen $#array = 999; # Es wird Speicher für 1000 Elemente "reserviert". # Die einzelenen Elemente sind allerdings alle "undef".

Mit der Funktion scalar() kann man die Anzahl der Elemente eines Arrays erhalten (die Funktion wird im Anhang C ausführlich beschrieben): my @array = ( 1, 10, "hallo", ); my $elementCount = scalar( @array ); # $elementCount enthält den Wert 3

Um zu prüfen, ob ein Array leer ist, hat man folgende Möglichkeiten (die jetzt noch nicht bekannten Begriffe unless und if werden später erklärt): # Alle folgenden Ausdrücke liefern TRUE, # wenn das Array leer ist, ansonsten liefern # sie FALSE. unless ( @arrayVar ) { ... } if ( $#arrayVar < 0 ) { ... } unless ( scalar( @arrayVar ) ) { ... } if ( scalar( @arrayVar ) == 0 ) { ... }

72

2

Grundlagen

Man kann eine Array-Variable mit mehreren Arrays initialisieren: my @a1 = ( 1, 2, ); my @a2 = ( 3, 4, ); my @a3 = (0, @a1, @a2, 5, ); # @a3 enthält ( 0, 1, 2, 3, 4, 5, ) # Alle Elemente von @a1 und @a2 werden in @a3 kopiert

Mit den Funktionen unshift() und push() kann man neue Elemente hinzufügen (die Funktionen unshift() und push() sind in Anhang C ausführlich beschrieben): my @array = ( 1, 2, 3, ); # Neues Element am Beginn eines Arrays einfügen unshift( @array, 0, ); # @array enthält jetzt ( 0, 1, 2, 3, ) # Neues Element am Ende des Arrays anhängen push( @array, 4 ); # @array enthält jetzt ( 0, 1, 2, 3, 4, ) # Beide Funktionen arbeiten auch mit Listen: push( @array, ( 5, 6, ) ); # @array enthält jetzt ( 0, 1, 2, 3, 4, 5, 6, ) # Dasselbe am Anfang, statt Liste in runden Klammern # werden die einzelnen neuen Elemente als variable # Argumente an unshift() übergeben: unshift( @array, -2, -1 ); # @array enthält jetzt ( -2, -1, 0, 1, 2, 3, 4, 5, 6, )

Mit den Funktionen shift() und pop() kann man Elemente am Beginn bzw. Ende des Arrays entfernen (die Funktionen shift() und pop() sind in Anhang C ausführlich beschrieben): my @array = ( 0, 1, 2, 3, 4, 5, ); # Erstes Element entfernen my $ele = shift( @array ); # @array enthält jetzt ( 1, 2, 3, 4, 5, ) # $ele enthält 0 # Letztes Element entfernen $ele = pop( @array ); # @array enthält jetzt ( 1, 2, 3, 4, ) # $ele enthält 5

Variablen

73

Mit der Funktion splice() kann man sowohl beliebige Elemente an beliebiger Stelle hinzufügen als auch entfernen (die Funktion splice() ist in Anhang C ausführlich beschrieben): my @array = ( 1, 2, 3, 4, ); # Zweites Element entfernen my $ele = splice( @array, 1, 1 ); # @array enthält jetzt ( 1, 3, 4, ) # $ele enthält 2 # Vorletztes Element entfernen $ele = splice( @array, -2, 1 ); # @array enthält jetzt ( 1, 4, ) # $ele enthält 3 # Elemente ab Index 1 einfügen splice( @array, 1, 0, 2, 3 ); # @array enthält jetzt ( 1, 2, 3, 4, ) # Zweites und drittes Element entfernen und 3 neue # Elemente einfügen my @removed = splice( @array, 1, 2, "zwei", "drei", "dreieinhalb" ); # @array enthält jetzt ( 1, "zwei", "drei", "dreieinhalb", 4, ) # @removed enthält ( 2, 3, )

In skalarem Kontext liefert die Funktion splice() das letzte entfernte Element zurück (oder undef, falls kein Element entfernt wurde), in List-Kontext werden alle entfernten Elemente zurückgeliefert. Die Funktion splice() wird, ebenso wie alle anderen verwendeten Funktionen, in Anhang C noch ausführlich behandelt. Mit der Funktion join() kann man ein Array in einen Skalar umwandeln. Jedoch ist Vorsicht geboten, wenn einzelne Elemente des Arrays den Pseudowert undef haben: my @array = ( 1, 2, 3, 4, ); my $scalar = join( "", @array ); # $scalar enthält "1234" $scalar = join( ", ", @array ); # $scalar enthält "1, 2, 3, 4" # Vorsicht bei undef-Elementen my @array1 = ( 1, undef, 2 ); # Die folgende Anweisung ergibt eine Laufzeitwarnung: print( join( ", ", @array ), "\n" );

74

2

Grundlagen

Mit der Funktion split() kann man eine skalare Variable in ein Array umwandeln: my $scalar = 12345; my @array = split( "", $scalar ); # @array enthält ( 1, 2, 3, 4, 5, ) $scalar = "Das ist ein Satz"; @array = split( " ", $scalar ); # @array enthält ( "Das", "ist", "ein", "Satz", ) # Gleiches Beispiel, nur wird nun das Leerzeichen mit in # das Array übernommen @array = split( "( )", $scalar ); # @array enthält ( "Das", " ", "ist", " ", "ein", " ", "Satz", )

Mit der Funktion reverse() kann man die Elemente eines Arrays in der Reihenfolge vertauschen: my @array = ( 1, 2, 3, 4, ); @array = reverse( @array ); # @array enthält jetzt ( 4, 3, 2, 1, )

Mit der Funktion sort() kann man Arrays sortieren: my @array = ( 1, 17, 25, 3, -1 ); my @sortedArray = sort( @array ); # @sortedArray enthält ( -1, 1, 17, 25, 3, )

Wie man sieht, wurde das Array nicht numerisch, sondern lexikalisch sortiert. Das ist die Standardeinstellung von sort(). Will man numerisch sortieren, muss man den Operator verwenden (keine Angst, auch diese Funktion wird noch mit einigen Beispielen erklärt). @sortedArray = sort( { $a $b } @array ); # @sortedArray enthält ( -1, 1, 3, 17, 25, )

Zwischen den geschweiften Klammern und dem zu sortierenden Array darf kein Komma stehen. Dreht man die innerhalb der geschweiften Klammern vordefinierten Variablen $a und $b um, kann man umgekehrt numerisch sortieren: @sortedArray = sort( { $b, $a } @array ); # @sortedArray enthält ( 25, 17, 3, 1, -1, )

Variablen

75

Mit dem Operator .. und der Funktion rand() lassen sich auf einfache Weise zufällige Zeichenfolgen erzeugen: ... # Programmcode, der einen Zufallsstring # mit 10 Zeichen Länge erzeugt und ausgibt # Hinweis: Die Variablen @cs und $rs sollten # eigentlich @characters und $randomString # heißen, wurden jedoch aus Platzgründen # abgekürzt. my @cs = ( "A" .. "Z", "a" .. "z", "0" .. "9" ); my $rs = ""; for ( my $i = 1; $i voneinander getrennt. Die Values eines Hashs müssen Skalare sein. Beispiele für die Initialisierung von Hashes: # Leeres Hash initialisieren my %hash = (); # Hash mit 2 Elementen initialisieren my %hash1 = ( "fn" => "Hugo", "ln" => "Hofmannsthal", ); # Hinweis: Die folgende Initialisierung führt zu # einer Fehlermeldung, da Hash-Variablen niemals # undef sein können. my %hash = undef;

Auch bei Hash-Initialisierungen gilt, wie schon von Arrays bekannt: Nach dem letzten Element darf (und sollte) ebenfalls ein Komma stehen. Auf einzelne Elemente eines Hashs greift man immer über den Key zu, der in geschweiften Klammern angegeben wird: my $firstname = $hash{ "fn" }; # $firstname enthält den Value des Elements, # das durch den Key "fn" # identifiziert wird, hier also "Hugo". # Vergleiche die Analogie zu Arrays: # $array[ $index ] # $hash{ $key }

Wir haben gelernt, dass ein Dollarzeichen $ als Typkennzeichen vor dem Variablennamen auf eine skalare Variable hindeutet. Die Variable hash ist jedoch eine Hash-Variable. Ist das ein Widerspruch?

Variablen

77

Mitnichten. Es gilt dasselbe wie schon vorher bei Arrays. Mit dem Ausdruck $hash{ "fn" }

greifen wir ja nicht auf das Hash als solches zu, sondern vielmehr auf ein Element des Hash. Da Elemente von Hashes grundsätzlich Skalare sein müssen, stimmt die Sache wieder. Dass die Variable hash wirklich eine Hash-Variable ist, erkennt man daran, dass nach dem Variablennamen eine geschweifte Klammer steht. Ein Hash-Element wird angelegt, indem man eine Zuweisung über den Key durchführt: my %hash = (); $hash{ "fn" } = "Egon"; # Hinweis: Auch die folgende Zuweisung ist möglich: $hash{ fn } = "Egon"; # Perl interpretiert Hash-Keys immer als Strings, # deshalb wird das Bareword "fn" implizit in einen # String umgewandelt.

Wenn bereits ein Element mit demselben Key existiert, dann überschreibt man den vorher gespeicherten Value des Hash-Elements, da jeder Key eindeutig ist, also nur einmal vorkommen kann. Beispiel: my %h = ( "fn" => "Hugo", ); $h{ "fn" } = "Egon"; # Der ursprüngliche Wert "Hugo" des Elements, das durch # den Key "fn" identifiziert wird, enthält nun "Egon".

Löschen kann man ein Hash-Element mit der delete()-Funktion: Die delete()-Funktion sowie alle weiteren hier besprochenen Funktionen werden weiter unten sowie in Anhang C noch ausführlich besprochen. delete( $hash{ "fn" } ); # Löscht das Hash-Element (Key und Value) mit dem Key # "fn"

Mit der Funktion exists() kann abgefragt werden, ob ein Hash-Element mit dem angegebenen Key existiert oder nicht: if ( exists( $hash{ "age" } ) ) { # Es existiert ein Element mit dem Key "age". # Hinweis: Es wird nicht auf den Value zugegriffen, # sondern nur auf den Key. } else { # Es gibt kein Element mit dem Key "age". }

78

2

Grundlagen

Will man den Zustand des Wertes anstelle des Schlüssels prüfen, muss man zum Beispiel folgende Abfrage machen: if ( $hash{ "age" } ) { # Es existiert ein Element mit dem Key "age" und der # Value ist logisch TRUE # (er hat also nicht den Wert "0", 0, "" oder undef). }

Mit der Funktion defined() ist es möglich, festzustellen, ob der Value des Hash-Elements mit dem angegebenen Key einen definierten Wert hat oder nicht: if ( defined( $hash{ "age" } ) ) { # Es existiert ein Hash-Element mit dem Key "age" # und der Wert des Elements ist definiert # (kann also auch die Werte # "0", 0 oder "" enthalten). }

Ähnlich wie bei Arrays kann man in einer Schleife nacheinander auf alle Hash-Elemente zugreifen, allerdings nicht geordnet nach aufsteigenden Indizes, Stattdessen erhält man eine unsortierte Liste der Hash-Keys mit der Perl-Funktion keys(): my %hash = "key1" "key2" "3" => );

( => 1, => 2, "value3"

foreach my $key ( keys( %hash ) ) { my $val = $hash{ $key }; print( "key = $key, value = $val\n" ); } # Man kann die Liste der Keys auch in einer Array # Variable zwischenspeichern. # Damit kann man eine Laufvariable verwenden, um # direkt auf die Keys des Arrays zuzugreifen. my @keys = keys( %hash ); for ( my $i = 0; $i );

( => 1, => 2, "value3"

# Alphabetische Sortierung der Keys foreach my $key ( sort( keys( %hash ) ) ) { my $val = $hash{ $key }; print( "key = $key, value = $val\n" ); } # Umgekehrte alphabetische Sortierung der Keys foreach my $key ( reverse( sort( keys( %hash ) ) ) ) { my $val = $hash{ $key }; print( "key = $key, value = $val\n" ); }

Eine weitere Möglichkeit, auf Hash-Elemente nacheinander zuzugreifen, bietet die each()-Funktion, mit der man in einem Schleifendurchlauf sowohl den Key als auch den zugehörigen Value bekommt: my %hash = "key1" "key2" "3" => );

( => 1, => 2, "value3"

while ( my ( $key, $val ) = each( %hash ) ) { print( "key = $key, value = $val\n" ); }

Mit der Funktion values() kann man eine Liste der Values eines Hashs erzeugen, ohne dass Keys direkt involviert sind. Wie die zurückgegebenen List-Elemente sortiert werden, kann nicht beeinflusst werden. Man kann nicht davon ausgehen, dass die zurückgegebenen List-Elemente in irgendeiner Weise sortiert sind:

80

2

Grundlagen

my %hash = ( "key1" => 1, "key2" => 2, "3" => "value3" ); my @values = values( %hash ); # @values enthält # ( 2, 1, "value3", ) # oder ( 2, "value3", 1, ) # oder ( "value3", 2, 1, ) # oder ( "value3", 1, 2, ) # oder ( 1, 2, "value3", ) # oder ( 1, "value3", 2, )

2.6.7 Referenzvariablen Während alle bisherigen Variablentypen einem bestimmten Datentyp zugeordnet waren, können Referenzvariablen alle denkbaren Typen enthalten (obwohl die Referenzvariable selbst natürlich immer eine skalare Variable ist). In einer Referenzvariable wird die Adresse eines Wertes oder einer anderen Variable oder auch einer Funktion bzw. eines anonymen Codeblocks gespeichert. Sie sind also ähnlich aufzufassen wie Pointer-Variablen aus der Programmiersprache C. Da Referenzvariablen Skalare sind, werden sie mit dem Typkennzeichen $ versehen. Ich möchte Referenzvariablen anhand des folgenden Schaubildes verdeutlichen:

 

    



 



 





Abbildung 2.1: Referenzvariablen

Erläuterungen: In der Symboltabelle speichert der Perl-Interpreter alle bekannten Identifier (das sind die Namen von Variablen und Funktionen) eines Programms mit ihrer Adresse im Hauptspeicher ab.

Variablen

81

Angenommen, die erste freie Adresse im Hauptspeicher beginnt bei 0 (hex 0x00), und wir besitzen (immer noch) einen 32-Bit-Rechner, dann enthält die Symboltabelle des folgenden Programms: #!/usr/bin/perl -w use strict; my $var1 = 7; my $ref1 = \$var1;

in vereinfachter Form folgende Einträge: Symbol

Adresse

$var1

0x00

$ref1

0x04

Die skalare Variable $var1 wird in der Länge 4 Byte im Hauptspeicher unter der Adresse 0x00 gespeichert, der Wert dieser Speicheradresse ist 7. Die ebenfalls skalare Variable $ref1 wird in der Länge 4 Byte im Hauptspeicher unter der nächsten freien Adresse gespeichert, also unter 0x04. Da die Variable keinen normalen Wert, sondern die Adresse von $var1 enthält, ist im Hauptspeicher für die Referenzvariable der Wert 0x00 gespeichert, die Referenzvariable »zeigt« also auf die Adresse der Variable $var1.

Definition von Referenzvariablen Wenn Referenzvariablen als Zeiger auf andere Variablen oder auf Funktionen definiert werden, stellt man einen Backslash »\« vor die Variable, auf welche die Referenzvariable zeigen soll. Bei Funktionsreferenzen muss nach dem Backslash zusätzlich ein Kaufmännisches Und »&« angegeben werden, um dem Interpreter mitzuteilen, dass der darauf folgende Identifier der Name einer Funktion ist: # Hier ein paar Definitionen von normalen Variablen my $scalar = 5; my @array = ( 1, 2, ); my %hash = ( "firstname" => "Hugo", "age" => 39, ); # Und nun die Definition einer Funktion mit dem # Namen "myFunc". Sie tut nichts anderes, als alle # Argumente der Funktion auszugeben. sub myFunc { my ( $arg ) = @_;

82

2

Grundlagen

print( "$arg\n" ); } # Definition einer Referenzvariable auf $scalar my $scalarRef = \$scalar; # Die Variable "$scalarRef" enthält die Speicheradresse # der Variable "$scalar". Deren Wert kann nun sowohl # über $scalar = 5; # als auch über $$scalarRef = 5; # verändert werden. Machen Sie sich keine Gedanken # über das komische "$$", die Erklärung kommt noch. # Definition einer Referenzvariable auf @array my $arrayRef = \@array; # Die Variable "$arrayRef" enthält die Speicheradresse # der Variable "@array". Man kann nun auf die Elemente # des Arrays entweder über @array oder über $arrayRef # zugreifen. Beispiele: # Das 4. Element des Arrays bekommt den Wert 5 $array[ 3 ] = 5; $arrayRef->[ 3 ] = 5; # Die Bedeutung von "->" wird später noch erklärt. # Definition einer Referenzvariable auf %hash my $hashRef = \%hash; # Die Variable "$hashRef" enthält die Speicheradresse # der Variable "%hash". Man kann nun auf die Elemente # des Hashs entweder über %hash oder über $hashRef # zugreifen. Beispiele: $hash{ "firstname" } = "Egon"; $hashRef->{ "firstname" } = "Egon"; # Die Bedeutung von "->" wird später noch erklärt. # Definition einer Referenzvariable auf die Funktion # myFunc my $funcRef = \&myFunc; # Beachte: nach dem Backslash muss ein Kaufmänisches Und "&" # stehen, damit Perl weiß, dass es sich um eine # Funktion handelt. # Normaler Aufruf der Funktion: myFunc( "bla" ); # Aufruf der Funktion über die Referenzvariable: &{ $funcRef }( "bla" );

Man kann auch anonyme Referenzen mit Hilfe von Referenzvariablen definieren, das sind Referenzen, die nicht auf andere Variablen oder Funktionen zeigen, sondern direkt auf die Daten bzw. den Programmcode:

Variablen

83

# Direkte Referenzvariable auf ein Array: # Anstelle der bei Listen üblichen runden Klammern # werden hier eckige Klammern verwendet. my $refArray = [ 1, 2, 3, ]; # Direkte Referenzvariable auf ein Hash: # Anstelle der bei Listen üblichen runden Klammern # werden hier geschweifte Klammern verwendet. my $refHash = { "key1" => "value1", "key2" => "value2", }; # Direkte Referenzvariable auf Programmcode, # in diesem Fall auf eine anonyme Funktion ohne Namen: # Beachte: Nach der schliessenden geschweiften Klammer # muss ein Strichpunkt stehen, da es sich hier nicht um # eine Funktionsdeklaration, sondern um eine Zuweisung # einer anonymen Funktion an eine Variable handelt. # Die Funktion ist anonym, weil sie keinen Namen # hat. Sie kann nur über die Referenzvariable # $refFunc aufgerufen werden. # Damit kann man wunderschöne private Funktionen # schreiben, die nur innerhalb derselben Datei # bekannt sind! Mehr hierzu bei Objektorientierter # Programmierung. my $refFunc = sub { my ( $arg ) = @_; print( "$arg\n" ); }; # Aufruf der Funktion über die Referenzvariable: &{ $refFunc }( "bla" ); # # # # #

Da die Variable "$refFunc" mit der Deklaration durch 'my' nur im umgebenden Codeblock bekannt ist, kann sie auch nur dort verwendet werden. Damit ist die Funktion, auf die "$refFunc" zeigt, privat gemacht und nach außen unbekannt.

Dereferenzierung von Referenzvariablen Unter Dereferenzierung versteht man den Zugriff auf den Wert derjenigen Variablen, auf die eine Referenzvariable zeigt. Für die unterschiedlichen Dereferenzierungsarten gelten folgende Regeln:

Dereferenzierung von skalaren Variablen Die Dereferenzierung von skalaren Referenzvariablen erfolgt durch ein Voranstellen von $ vor die Referenzvariable (einschließlich des Typkennzeichens).

84

2

Grundlagen

Im folgenden Programmcode versuchen wir, den Wert einer Referenzvariablen direkt auszugeben: # Definition einer skalaren Variable my $scalar = 5; # Definition einer Referenzvariable auf "$scalar" my $scalarRef = \$scalar; # Während über $scalar direkt der Wert der Variable # angesprochen wird, # erhalten wir über $scalarRef zunächst die # Hauptspeicheradresse von $scalar. print( "scalar = $scalar\n" ); print( "scalarRef = $scalarRef\n" );

Wenn wir den Code ausführen, dann erhalten wir folgende Ausgabe: scalar = 5 scalarRef = SCALAR(0x1a72f04)

Wie wir sehen, wird für die normale skalare Variable direkt der im Hauptspeicher abgelegte Wert ausgegeben, während bei der Referenzvariable die Adresse erscheint, auf welche die Variable zeigt (zusätzlich zur Information, dass es sich um eine Referenzvariable auf eine skalare Variable handelt). Der Wert 5 unserer normalen Variable $var1 ist also im Hauptspeicher unter der Adresse 0x1a72f04 abgelegt. Damit wir auf den Wert des Wertes der Referenzvariable zugreifen können, müssen wir ein zusätzliches Dollarzeichen $ für die Dereferenzierung angeben. Dabei gehört es nicht nur zum guten Ton, dass man um das Dereferenzierungszeichen und die Variable geschweifte Klammern setzt, vielmehr wird der Programmcode damit leichter lesbar: print( "Inhalt der dereferenzierten Variable scalarRef", " = ", ${ $scalarRef }, "\n" );

Die Ausgabe ist nun: Inhalt der dereferenzierten Variable scalarRef = 5

Man kann die geschweiften Klammern um $scalarRef weglassen und den Code wie folgt ändern: ${ $scalarRef } # ausführliche Form $$scalarRef # vereinfachte Form

Ich empfehle allerdings die ausführliche Form mit geschweiften Klammern, denn sie ist einfach besser lesbar.

Variablen

85

Die Abhängigkeiten zwischen Variablen und Referenzvariablen gelten sowohl für lesenden als auch für schreibenden Zugriff: $scalar = 1; # Da die Variable $scalarRef auf dieselbe # Speicheradresse zeigt, hat sich auch der Wert # von ${ $scalarRef } geändert: $val = ${ $scalarRef }; # $val enthält 1 # Nun ändern wir den Wert über die Referenzvariable: ${ $scalarRef } = -17; # Die Originalvariable $scalar enthält ebenfalls -17

Dereferenzierung von Array-Variablen Array-Referenzvariablen werden dereferenziert, indem ein @ vor die Referenzvariable (einschließlich des Typkennzeichens $) gestellt wird. Auf Array-Elemente wird über die Referenzvariable zugegriffen, indem der Operator -> zwischen den Namen der Referenzvariable und die öffnende eckige Klammer gesetzt wird: my @array = ( 1, 2, 3, ); # Definition der Referenzvariablen durch Voranstellen # eines Backslashs: my $arrayRef = \@array; # Zugriff auf einzelne Elemente des Arrays über die # Originalvariable: my $ele = $array[ 1 ]; # $ele enthält 2 # Zugriff auf einzelne Elemente des Arrays über die # Referenzvariable: $ele = $arrayRef->[ 1 ]; # Beachte: Hier muss der Dereferenzierungsoperator # -> verwendet werden. # $ele enthält ebenfalls 2 # Vergisst man den Dereferenzierungsoperator "->", # bekommt man als Dank eine schöne Fehlermeldung. # Beispiel: $ele = $arrayRef[ 1 ]; # Perl sucht in diesem Fall nach einer Array-Variable # @arrayRef, die es natürlich nicht gibt, wir haben # ja nur die skalare Referenzvariable $arrayRef # definiert. # Eine Fehlermeldung ist die Folge. # Anzahl der Array-Elemente # über die Array-Variable:

86

2 my $eleCount = scalar( @array ); # $eleCount enthält 3 # Dasselbe über die Referenzvariable: $eleCount = scalar( @{ $arrayRef } ); # Wir müssen dem Interpreter ausdrücklich sagen, # dass sich hinter der Variable "$arrayRef" in # Wirklichkeit ein Array verbirgt # $eleCount enthält 3 # Index des letzten Elements # über die Array-Variable: my $lastIndex = $#array; # $lastIndex enthält 2 # Dasselbe über die Referenzvariable: $lastIndex = $#{ $arrayRef }; # $lastIndex enthält 2 # Neues Element ans Ende des Array stellen # über die Array-Variable: push( @array, 7 ); # @array enthält jetzt ( 1, 2, 3, 7, ) # Dasselbe über die Referenzvariable: push( @{ $arrayRef }, 8 ); # @array enthält jetzt ( 1, 2, 3, 7, 8, ) # Liste leeren # über die Array-Variable @array = (); # @array enthält jetzt eine leere Liste # Dasselbe über die Referenzvariable: @{ $arrayRef } = (); # @array enthält jetzt eine leere Liste # # VORSICHT # # Die folgende Zeile weist der Variable # "$arrayRef" ein anonymes Array zu. $arrayRef = [ 7, 4, ]; # @array enthält immer noch eine leere Liste, # während $arrayRef nun zu einer Referenz auf ein # anonymes Array geworden ist, das über keine andere # Variable mehr erreichbar ist. # Die ursprüngliche Referenz auf @array wurde mit dieser # neuen Zuweisung an $arrayRef aufgehoben.

Grundlagen

Variablen

87

Dereferenzierung von Hash-Variablen Hash-Referenzvariablen werden dereferenziert, indem ein % vor die Referenzvariable (einschließlich Typkennzeichen $) gestellt wird. Auf Hash-Elemente wird über die Referenzvariable zugegriffen, indem der Operator -> zwischen den Namen der Referenzvariablen und die öffnende geschweifte Klammer gesetzt wird: # Definition und Initialisierung # einer normalen Hash-Variable my %hash = ( "age" => 35, "gender" => "w", ); # Definition der Referenzvariable, die # auf %hash zeigt. # (Die Referenzvariable wird mit der Adresse # von "%hash" initialisiert.) my $hashRef = \%hash; # Zugriff auf einzelne Hash-Elemente # über die Hash-Variable: my $value = $hash{ "age" }; # $value enthält 35 # Über die Referenzvariable: $value = $hashRef->{ "age" }; # $value enthält 35 # Setzen eines neuen Elements in %hash # über die Hash-Variable: $hash{ "firstname" } = "Egon"; # Dasselbe über die Referenzvariable: $hashRef->{ "firstname" } = "Egon"; # Schleife über alle Hash-Elemente # über die Hash-Variable: foreach my $key ( keys( %hash ) ) { my $val = $hash{ $key }; print( "key = $key, value = $val\n" ); } # Dasselbe über die Referenzvariable: foreach my $key ( keys( %{ $hashRef } ) ) { my $val = $hashRef->{ $key }; print( "key = $key, value = $val\n" ); } # Hash leeren # über die Hash-Variable: %hash = (); # %hash ist nun leer

88

2

Grundlagen

# Dasselbe über die Referenzvariable: %{ $hashRef } = (); # %hash ist nun leer # Abfrage auf Original-Hash-Variable: if ( %hash ) { print( "Hash ist nicht leer\n" ); } # Gleiche Abfrage über Referenzvariable: if ( %{ $hashRef } ) { print( "Hash ist nicht leer\n" ); } # VORSICHT: # Die folgende Abfrage if ( $hashRef ) # prüft, ob der Inhalt der Referenzvariable # logisch TRUE ist. # Der Inhalt der Referenzvariable zeigt aber auf eine # Speicheradresse # und nicht auf den Inhalt der Speicheradresse. # Adressen sind aber normalerweise ungleich 0x00 # (was logisch FALSE wäre), # d.h. die Abfrage liefert in der Regel immer TRUE. # Noch mal VORSICHT: $hashRef = { "a" => 1, "b" => 2, }; # Diese Zuweisung legt ein anonymes Hash an, # da nur noch über die Referenzvariable "$hashRef" # angesprochen werden kann. $hashRef hat nun nichts # mehr mit der Hash-Variable "%hash" zu tun, # diese bleibt unverändert!

Dereferenzierung von Referenzvariablen auf Funktionen Referenzvariablen auf Funktionen werden dereferenziert, indem man das Zeichen & vor die Referenzvariable (einschließlich Typkennzeichen $) stellt. Beispiel: # Definition einer Funktion "myFunc" # Sie gibt das erste Argument aus. sub myFunc { my ( $arg ) = @_; print( "$arg\n" ); } # Definition einer Referenzvariable, die auf # die Funktion "myFunc" zeigt. my $funcRef = \&myFunc; # Normaler Aufruf der Funktion

Variablen

89

myFunc( "hallo" ); # Aufruf über die Referenzvariable &{ $funcRef }( "Welt" ); # In diesem Fall ginge auch &$funcRef( "Welt" ), # aber das wollen wir uns erst gar nicht # angewöhnen. # Völlig anonyme Funktion, da sie gar keinen Namen # hat. Der Funktionsrumpf wird als Ganzes der # Referenzvariable zugewiesen. # Hinweis: Da es sich um eine Zuweisung handelt, # muss nach der schließenden geschweiften Klammer # unbedingt ein Semikolon stehen! # Aufruf nur über die Referenzvariable möglich. my $anonymousFunc = sub { my ( $arg ) = @_; print( "$arg\n" ); }; # Aufruf der Funktion &{ $anonymousFunc }( "Hi" );

Anonyme Funktionen mit Hilfe von Referenzvariablen werden meist in Zusammenhang mit Objektorientierter Programmierung verwendet, um private Funktionen zu definieren, die nur von Programmcode innerhalb derselben Datei aufgerufen werden können. Hinweis: Die Definition von Referenzvariablen für anonyme Funktionen muss vor dem Aufruf stehen, sonst erscheint eine Fehlermeldung. Beispiel: # So ist es richtig: erst definieren, dann benutzen. my $ref = sub { print( "hallo\n" ); }; &{ $ref }(); # So handelt man sich Ärger ein: &{ $ref }(); ... my $ref = sub { print( "hallo\n" ); };

Prüfen des Typs einer Referenzvariable Man kann mit Hilfe der Funktion ref() feststellen, ob es sich bei der angegebenen Variable um eine normale Variable oder aber um eine Referenzvariable handelt. Beispiel: # Definition einer normalen Array-Variable my @array = ( 1, 2, ); # Definition einer Referenzvariable auf @array

90

2

Grundlagen

my $aref = \@array; # Definition einer normalen Hash-Variable my %hash = ( "k1" => 1, "k2" => 2, ); # Definition einer Referenzvariable auf %hash my $href = \%hash; # Definition einer Referenzvariable auf $href my $refRef = \$href; # Definition einer Referenzvariable auf # eine anonyme Funktion my $func = sub { print( "hallo\n" ); }; # Für jede der oben stehenden Variablen wird nun # die Funktion ref() benutzt, um deren Typ # auszugeben: print( 'ref( @array ) = ', ref( @array ), "\n" ); print( 'ref( $aref ) = ', ref( $aref ), "\n" ); print( 'ref( %hash ) = ', ref( %hash ), "\n" ); print( 'ref( $href ) = ', ref( $href ), "\n" ); print( 'ref( $refRef ) = ', ref( $refRef ), "\n" ); print( 'ref( $func ) = ', ref( $func ), "\n" );

Die Ausgabe des Perl-Codes ist: ref( ref( ref( ref( ref( ref(

@array ) = $aref ) = ARRAY %hash ) = $href ) = HASH $refRef ) = REF $func ) = CODE

Wie man bereits durch Überlegen richtig vermuten kann, sind die Variablen @array und %hash keine Referenzvariablen. Für die Referenzvariable auf @array wird der String »ARRAY« zurückgeliefert, für die Hash-Referenz der String »HASH«. Bildet man eine Referenz auf eine Variable, die wiederum eine Referenz ist, wird der String »REF« von der Funktion ref() zurückgegeben. Handelt es sich bei der Variable um eine Referenz auf eine Funktion, dann gibt die Funktion den String »CODE« zurück. Damit lassen wir es fürs Erste gut sein. Später, wenn wir die Objektorientierte Programmierung kennen lernen, werden wir sehen, dass die Funktion ref() auch noch andere Rückgabewerte haben kann. Hier wollen wir uns erst einmal dem Thema »mehrdimensionale Arrays« widmen:

Variablen

91

Mehrdimensionale Arrays Mit Hilfe von Referenzen lassen sich in Perl auf effiziente Art und Weise mehrdimensionale Arrays aufbauen. Sehen wir uns einmal ein Beispiel für ein zweidimensionales Array an: # Direkte Definition einer zweidimensionalen # Array-Variable mit Hilfe von Referenzen my @ar2d = ( [ 1, 2, ], [ "a", 7, "hallo", ], );

Das erste Element des Arrays @ar2d, das mit $ar2d[ 0 ] angesprochen wird, enthält eine Referenz auf ein anonymes Array, das aus zwei Elementen besteht. Das zweite Element von @ar2d ist ebenfalls eine Referenz auf ein anonymes Array, diesmal mit drei Elementen. Wie man sieht, darf die Länge der anonymen Arrays in der zweiten Dimension getrost unterschiedlich sein. Wenn wir uns die Anzahl der Elemente von @ar2d mit print( scalar( @ar2d ), "\n" ); # Alternativ geht natürlich auch: print( $#ar2d + 1, "\n" );

ausgeben lassen, erhalten wir, wie bereits vermutet: 2

Mit folgendem Code sehen wir uns an, was sich in den einzelnen Elementen von @ar2d verbirgt: for ( my $i = 0; $i benutzen, um an die Elemente des Arrays heranzukommen: # Ausgabe der Anzahl von Elementen: print( scalar( @{ $ar2d } ), "\n" ); # oder print( $#{ $ar2d } + 1, "\n" ); # Ausgabe der Elemente: for ( my $i = 0; $i [ 0 ] zugreifen (das ist das erste Element des übergeordneten Arrays @ar2d): # Hier ist der Index für das letzte Element des # untergeordneten Arrays in der zweiten Dimension # fest verdrahtet mit 1 angegeben. print( $ar2d->[ 0 ]->[ 1 ], "\n" ); # Und nun machen wir deutlich, dass wir das letzte # Element wollen, egal, wie viele Elemente im # untergeordneten Array der zweiten Dimension # vorhanden sind: print( $ar2d->[ 0 ]->[ $#{ $ar2d->[ 0 ] } ], "\n" );

Alles klar? Wenn nicht, dann wollen wir die Sache schrittweise vereinfachen: In Worten wollen wir Folgendes: den Inhalt des letzten Elements vom ersten untergeordneten Array der zweiten Dimension. Verwenden wir Hilfsvariablen zur Vereinfachung, dann wird alles schon ein bisschen klarer: # Unser Array in Form von Referenzen my $ar2d = [ [ 1, 2, ], [ "a", 7, "hallo", ], ]; # Hilfsvariable für die Referenz auf das erste Element # in der ersten Dimension my $aref = $ar2d->[ 0 ];

Variablen

93

# Hilfsvariable für den Index des letzten Elements # im untergeordneten Array der zweiten Dimension my $lastInd = $#{ $aref }; # Und nun noch eine Hilfsvariable für den Wert # des so ermittelten Elements my $ele = $aref->[ $lastInd ]; # Nun sieht der Code doch schon ganz lesbar aus, oder? print( "ele[ 0 ][ $lastInd ] = '$ele'\n" );

Noch ein Wort zum Dereferenzierungs Operator ->:

Die Pfeilregel für mehrdimensionale Arrays Perl erlaubt, dass man bei mehreren hintereinander stehenden ->[]->[] in mehrdimensionalen Arrays nur den ersten hinschreibt. Damit sieht ein mehrdimensionales Array auch so aus: # Ausführliche Angabe aller Operatoren zur # Dereferenzierung: my $lastInd = $#{ $ar2d->[ 0 ] }; print( $ar2d->[ 0 ]->[ $lastInd ], "\n" ); # Erlaubte Abkürzung: print( $ar2d->[ 0 ][ $lastInd ], "\n" );

Wenn wir für das Array in der ersten Dimension keine Referenz, sondern eine normale Array-Variable verwenden, dann wird die Sache noch einfacher, denn in diesem Fall benötigt man gar keine Operatoren für die Dereferenzierung: # Direkte Deklaration einer zweidimensionalen # Array-Variable mit Hilfe von Referenzen my @ar2d = ( [ 1, 2, ], [ "a", 7, "hallo", ], ); # Verwendung der Pfeilregel zur Abkürzung my $lastInd = $#{ $ar2d[ 0 ] }; print( $ar2d[ 0 ][ $lastInd ], "\n" );

Ich habe sowohl für die Array-Variable @ar2d als auch für die Referenzvariable $ar2d denselben Namen gewählt (ar2d), um Ihnen ein Problem zu zeigen, das Novizen in Perl oft haben: In den Ausdrücken $ar2d[ 0 ][ $lastInd ] # und $ar2d->[ 0 ][ $lastInd ]

handelt es sich bei $ar2d um zwei völlig verschiedene Variablen!

94

2

Grundlagen

Man erkennt es allerdings nur am ersten Dereferenzierungsoperator -> nach dem Variablennamen, der auf eine skalare Referenzvariable $ar2d hindeutet, während der obere Ausdruck eine Array-Variable @ar2d darstellt. Leider erkennt man das nicht am Typkennzeichen, das vor dem Variablennamen steht, denn dieses ist in beiden Fällen das Dollarzeichen $, und dieses kennzeichnet, wie wir nun wissen, eine skalare Variable. Wir wollen ja schließlich auf ein Element eines Arrays zugreifen. Dieses ist immer, gleichgültig, ob das Array über eine normale Array-Variable oder über eine Referenzvariable angesprochen wird, ein Skalar.

n-dimensionale Arrays n-dimensionale Arrays funktionieren im Prinzip genauso wie zweidimensionale. Man muss einfach nur mehr schreiben: # Beispiel für ein dreidimensionales Array # mit Referenzen my $ar3d = [ # erste Dimension [ # zweite Dimension [ 0, 1, 2, ], # dritte Dimension ], [ # zweite Dimension [ 3, 4, 5, ], # dritte Dimension ], ]; # Anhängen eines Elements push( @{ $ar3d->[ 0 ][ 0 ] }, "2.5" );

Oft werden mehrdimensionale Arrays nicht mit allen Werten in der Deklaration einer Variable initialisiert, sondern wachsen zur Laufzeit des Programms (zum Beispiel, wenn Tabellen aus Dateien eingelesen und verarbeitet werden). Zuerst deklariert man eine Array-Variable (oder auch eine Referenzvariable) und initialisiert diese mit einer leeren Liste: my @array = (); # oder als Referenz my $aref = [];

In einer Schleife werden nun Daten eingelesen. Als Beispiel wollen wir das zeilenweise Einlesen einer Datei nehmen. Jede Zeile der Datei soll ein neues Array werden, das wiederum alle Zeichen der eingelesenen Zeile als Elemente besitzt (keine Sorge, das Einlesen von Dateien werden wir weiter unten ausführlich kennen lernen). Wir bauen also ein zweidimensionales Array zur Laufzeit auf:

Variablen 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 }

95

my @array = (); # Zeilenweises Einlesen von der Standardeingabe # (meist ist das die Tastatur) while ( defined( my $line = ) ) { # Zeilenende-Zeichen entfernen (dieses ist # nach dem Einlesen der Zeile # Bestandteil von $line) chomp( $line ); # Unser oberstes Array, das die Zeilen enthalten # soll, um ein Element erweitern $#array++; # Hinweis: es wurde nur das Array um ein # Element erweitert, das ist jedoch noch nicht # initialisiert. Wir müssen nun dafür sorgen, # dass aus diesem Element eine Array Referenz # wird, die alle eingelesenen Zeichen der Zeile # als Elemente besitzt $array[ $#array ] = [ split( "", $line ) ];

Die interessanten Zeilen des Quellcodes sind Zeile 12 und Zeile 19. In Zeile 12 wird die Anzahl des Arrays @array um ein Element erhöht (mit Hilfe des Autoincrement-Operators, den wir später noch kennen lernen werden). Zu diesem Zeitpunkt erfolgt aber noch keine Initialisierung des neu hinzugekommenen Elements, es ist also noch undef. Es wird also zunächst nur ein Element am Ende des Arrays hinzugefügt. Erst in Zeile 19 erfolgt die Initialisierung. Den Ausdruck $array[ $#array ]

müssten wir bereits kennen, er greift auf das letzte Element des Arrays @array zu. Mit dem Funktionsaufruf split( "", $line )

wandeln wir die Zeichenkette der eingelesenen Zeichen in eine Liste um, deren Elemente die Zeichen der Zeile sind. Dadurch, dass wir den Funktionsaufruf in eckige Klammern setzen, teilen wir dem Interpreter mit: »Mach bitte aus der Liste eine Array-Referenz!«: [ split( "", $line ) ]

Wenn wir keine eckigen Klammern setzen, erhalten wir, wie könnte es anders sein, natürlich eine Fehlermeldung: $array[ $#array ] = split( "", $line );

96

2

Grundlagen

Die Fehlermeldung sieht in etwa so aus: Use of implicit split to @_ is deprecated at - line 19.

Im Moment können wir mit dieser Fehlermeldung noch nicht viel anfangen, weil wir nicht wissen, was für eine Variable @_ ist (keine Sorge, die Erklärung kommt noch). Die Funktion split() liefert, wie gesagt, eine Liste zurück. Wir aber weisen diese Liste einer skalaren Variable zu, nämlich dem letzten Element des Arrays @array. Das kann nur schief gehen. Erst durch die eckigen Klammern wird die Liste zu einer skalaren Referenz gemacht.

2.7 Operatoren 2.7.1 Was sind Operatoren? Ich will Ihnen den Begriff »Operator« anhand einiger einfacher Beispiele näher bringen: 5 + 3 $i++; my $diff = $op1 - $op2;

Ein Operator dient der Verknüpfung von Variablen oder Ausdrücken (englisch: »expressions«). Ein Ausdruck kann ein beliebiger Wert oder eine Verknüpfung von Werten und weiteren Ausdrücken (inklusive Funktionsaufrufen) sein. Das erste Beispiel zeigt den Operator + für die arithmetische Addition. Im zweiten Beispiel wird der aktuelle Zahlenwert der Variable $i um eins erhöht; der dafür vorgesehene Operator ++ wird Autoincrement-Operator genannt. Im letzten Beispiel sehen wir gleich zwei Operatoren: Zunächst wird die Differenz zweier numerischer Variablen mit Hilfe des Operators - gebildet. Das Ergebnis dieser Aktion wird dann über den Zuweisungsoperator = an die Variable $diff übergeben. Die Variablen bzw. die Ausdrücke, welche durch den Operator miteinander verknüpft werden, nennt man »Operanden«. Das letzte Beispiel hieße also in Worten: »Bilde zunächst die Differenz, indem der Operand $op1 und der Operand $op2 durch den Operator - verknüpft werden, und weise das Ergebnis dieser Operation mit Hilfe des Operators - der Variable $diff zu«.

Operatoren

97

Evaluierung Nahezu jeder Operator liefert einen Wert zurück, auf neudeutsch sagt man, er »evaluiert« die Operanden zu einem Wert. So evaluiert der Operator + im ersten Beispiel zum Zahlenwert 8, während der Operator ++ des zweiten Beispiels zu dem um 1 erhöhten aktuellen Zahlenwert der Variable $i evaluiert.

Binäre Operatoren Binäre Operatoren kennzeichnen sich dadurch aus, dass sie zwei Operanden besitzen, einen linken und einen rechten, die vom Operator miteinander verknüpft werden und einen neuen Wert ergeben (evaluieren).

Unäre Operatoren Ein unärer Operator besitzt nur einen einzigen Operanden. So ist zum Beispiel der Operator ++ unär. Je nach Art von Operator kann dabei der Operand entweder links oder rechts vom Operator stehen.

Rangfolge (Prioritäten) von Operatoren Bei der Abarbeitung mehrerer Operatoren gilt eine festgelegte Rangfolge, die bestimmt, in welcher Reihenfolge die Operatoren bearbeitet werden. Haben zwei Operatoren dieselbe Priorität, dann arbeitet der Interpreter diese von links nach rechts ab. Man kann die Reihenfolge der Abarbeitung auch gezielt durch Setzen von runden Klammern verändern. Beispiele: # Alle Operatoren haben dieselbe Priorität # Sie werden von links nach rechts abgearbeitet 1 + 2 - 3 + 4 # Der Operator "*" hat eine höhere Priorität als "+", # "/" hat eine höhere Priorität als "-" 2 * 3 + 4 / 2 - 4 # Ergebnis: 4 # Dasselbe mit expliziten Klammern ( 2 * 3 ) + ( 4 / 2 ) - 4 # Verändern der Reihenfolge mit Klammern 2 * ( 3 + 4 ) / ( 2 - 4 ) # Ergebnis: -7

Übersicht der Operatorprioritäten. In der linken Spalte ist der Operator angegeben, in der rechten Spalte der Assoziationstyp (d.h., ob der links vom Operator stehende Operand evaluiert wird oder der rechts stehende). Die Tabelle ist so sortiert, dass die Operatoren mit der höchsten Priorität oben stehen, die mit der geringsten Priorität unten.

98

2

Grundlagen

Operator

Assoziationstyp

Variablen, Quote-Operatoren, Ausdrücke in runden Klammern, Funktionsaufrufe mit runden Klammern, List-Operatoren nach links gesehen

links

->

links

++ --

./.

**

rechts

!, ~, \, unäres +, unäres -

rechts

=~, !~

links

*, /, %, x

links

+, -, .

links

links

-f, -d etc.

./.

, =, lt, gt, le, ge

./.

==, !=, , eq, ne, cmp

./.

&

links

|, ^

links

&&

links

||

links

.., ...

./.

?:

rechts

Komma-Operator, =>

links

List-Operatoren (nach rechts gesehen)

./.

not

rechts

and

links

or, xor

links

Tabelle 2.2: Übersicht der Operatorprioritäten

Wie man bereits an der Tabelle sieht, ist es bei der Fülle von Operatoren und Prioritäten nicht leicht, sich das Ganze zu merken. Wir sollten uns deshalb an den Leitsatz halten: »Setze immer Klammern, um die Rangfolge zu zeigen, die du meinst, wenn mehrere Operatoren mit unterschiedlichen Prioritäten verwendet werden!«. Das verbessert die Lesbarkeit von Programmen ganz ungemein. In Perl ist nicht immer genau definiert, wann etwas ein Operator oder eine Funktion ist. Diese Tatsache spielt allerdings in der Praxis kaum eine Rolle.

Operatoren

99

Perl stellt für alle möglichen Anforderungen die verschiedensten Operatoren zur Verfügung, die wir nun in einzelne Gruppen einteilen wollen.

2.7.2 Arithmetische Operatoren Für die Verarbeitung von Zahlen stellt Perl die folgenden Operatoren zur Verfügung:

Operator + Mit diesem Operator wird die Summe zweier Operanden gebildet. Beispiel: my $o1 = 1; my $o2 = 5; my $sum = $o1 + $o2; # $sum enthält 6 # Auch Zahlen als Strings mit "white space" am Ende # oder am Anfang # (white space ist ein Blank, ein Tab, "\n" und "\r") # funktionieren: my $o1 = "1\n"; my $o2 = " 2 "; my $sum = $o1 + $o2

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung: my my my my my

$i = 5; $k = "10"; $sum = $i + $k; $m = "a"; $diff = $m - $i;

Die letzte Zeile liefert: Argument "a" isn't numeric in subtraction (-) at - line 5.

Die Summenbildung von $i und $k geht jedoch in Ordnung, obwohl $k als String deklariert wurde, weil der Interpreter den Wert 10 in eine gültige Zahl umwandeln kann. Der Fehler ist in unserem Beispiel zwar leicht erkennbar, weil dort die Variablenwerte als Konstanten angegeben sind. In der Praxis jedoch werden die Variablen zur Laufzeit des Programms dynamisch mit Werten versorgt (z.B. durch das Einlesen von Daten aus einer Datenbank). Dann ist der Fehler aus dem Quellcode nicht mehr direkt ersichtlich. Beispiel: # Einlesen der Variablenwerte von der Standardeingabe # (Tastatur) my $i = ;

100

2

Grundlagen

my $k = ; my $sum = $i + $k;

Wenn wir den Beispielcode mit folgenden Eingaben ausführen: D:\>perl -w my $i = ; my $k = ; print( $i + $k ); ^Z 1 2 3 D:\>

dann erhalten wir, was schon zu vermuten war: die Ausgabe der Zahl 3 (das ist die letzte Zeile, alle vorhergehenden Zeilen haben wir über die Tastatur eingegeben). Das ist insofern bemerkenswert, als durch das Einlesen einer Zeile zunächst ein String entsteht, der das Zeilenende-Zeichen enthält. (Die Variable $i hat also den Wert 1\n. Wie wir sehen, entfernt der Interpreter implizit alle Zeichen, die man »white space« nennt. Dazu gehören das Blank, das TAB-Zeichen sowie \n und \r.) Erst wenn wir eine falsche Eingabe machen, sehen wir, was wirklich an die Variablen übergeben wurde: D:\>perl -w my $i = ; my $k = ; print( $i + $k ); ^Z 1 a Argument "a\n" isn't numeric in addition (+) at - line 3, line 2. 1

Operator Dieser Operator bildet die Differenz zweier Operanden. Beispiel: my $o1 = 1; my $o2 = 5; my $diff = $o1 - $o2; # $diff enthält -4

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung (siehe hierzu den Operator +).

Operatoren

101

Als unärer Operator mit nur einem Operanden liefert er den Wert mit umgekehrtem Vorzeichen: my $o = -1; my $result = -$o1; # $result enthält 1

Operator * Dieser Operator bildet das Produkt zweier Operanden. Beispiel: my $o1 = 1; my $o2 = 5; my $prod = $o1 * $o2; # $prod enthält 5

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung.

Operator / Dieser Operand bildet den Quotienten zweier Operanden. Das Ergebnis ist eine Gleitkommazahl. my $o1 = 1; my $o2 = 5; my $quot = $o1 / $o2; # $quot enthält 0.2

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung. Dasselbe passiert natürlich, wenn der Nenner (das ist der rechte Operand von »/«) die Zahl 0 enthält (Division durch Null).

Operator % Dieser Operator liefert den Rest einer Division (modulo Division). Das Ergebnis ist eine Integerzahl, die zwischen 0 und rechter operand - 1 liegt. Beispiel: my $o1 = 5; my $o2 = 3; my $remainder = $o1 % $o2; # $remainder enthält 2, denn: # 5 / 3 = 1 Rest 2

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung.

102

2

Grundlagen

Operator ** Dieser Operator führt eine Exponentialfunktion durch. Beispiel: my $o1 = 2; my $o2 = 3; my $result = $o1 ** $o2; # $result enthält 8 (2^3)

Enthält ein Operand keine Zahl, erfolgt eine Fehlermeldung. Vorsicht: -2 ** 4 # # # ( -2 ) **

ist nicht etwa 16, sondern -16, da der binäre Operator ** stärker bindet als der unäre Operator 4 # liefert 16

2.7.3 String-Operatoren Operator . (Punkt-Operator) Der Punkt-Operator wird verwendet, um mehrere Strings aneinander zu hängen. Beispiel: my $s1 = " balla"; my $result = "abc" . $s1 . " hallo"; # $result enthält "abc balla hallo"

Operator X (Vervielfältigungsoperator) Mit dem Vervielfältigungsoperator lässt sich ein beliebiger String (das kann auch ein einzelnes Zeichen sein) vervielfältigen. Beispiel: # Rechtsbündige Ausgabe mit fester Zeilenbreite # Auszugebender String my $output = "Hi there"; # Feste Zeilenbreite (hier 80 Zeichen) my $colCount = 80; # Länge des Strings bestimmen, der aus Leerzeichen # besteht und eine Füllfunktion hat my $fillerLen = $colCount - length( $output ); # Der String " " wird so oft vervielfältigt, # dass sich genau 80 Zeichen je Zeile ergeben print( " " x $fillerLen, "$output\n" );

Operatoren

103

Ist der rechte Operand (die Anzahl für die Vervielfältigung) negativ, dann wird vom Operator ein leerer String ausgegeben: "a" x 0 # ergibt ""

2.7.4 Zuweisungsoperatoren Operator = Der einfachste Zuweisungsoperator ist das Gleichheitszeichen, bei dem der Ausdruck rechts vom Operator dem linken Operanden zugewiesen wird. Er kann auch mit vielen anderen Operatoren verknüpft werden, so dass sich abkürzende Schreibweisen ergeben. Beispiele: my $result = 7 + 4; # $result um 10 erhöhen $result += 10; # Dasselbe in Langform: $result = $result + 10; # $result mit 10 multiplizieren $result *= 10 # $result durch 2 teilen $result /= 2; # 5 von $result abziehen $result -= 5;

Neben den gezeigten Kombinationen lässt sich der Zuweisungsoperator mit folgenden Operatoren verknüpfen (der Zuweisungsoperator = steht immer rechts vom verknüpften Operator): 왘 ** (Exponentialfunktion) 왘 . (String Verkettung) 왘 x (Vervielfältigung) 왘 & (Bitweises UND) 왘 | (Bitweises ODER) 왘 ^ (Bitweises EXOR) 왘 > (Bitweises Rechtsschieben) 왘 && (logisches UND) 왘 || (logisches ODER)

2.7.5 Autoincrement- und Autodecrement-Operatoren Mit den Operatoren ++ sowie -- lassen sich Variablen nach Gebrauch bzw. vor Gebrauch automatisch um eins hochzählen (inkrementieren) oder verringern (dekrementieren). Der Autoincrement-Operator weist zudem die Besonderheit auf, dass er auch bei Strings funktioniert. Beispiele: my $var1 = 1; my $var2 = $var1++ + 1; # Zuerst wird der aktuelle Wert von "$var1" mit 1 # addiert, und das Ergebnis wird "$var2" zugewiesen. # $var2 enthält also 2. # $var1 enthält nach der Operation den Wert 2. my $var3 = ++$var1 + 1; # Diesmal wird zuerst der Wert von "$var1" um 1 # erhöht, erst danach wird die Addition durchgeführt. # $var3 enthält also 4. # Besonderheit bei Strings, die nur für den # ++ Operator gilt! my $str = "A"; $str++; print( "$str\n" ); # Es wird "B" ausgegeben. $str = "Z"; $str++; print( "$str\n" ); # Es wird "AA" ausgegeben. # Der Operator -- hingegen funktioniert nicht # bei Strings. $str = "Z"; $str--; print( "$str\n" ); # Es wird -1 ausgegeben.

Operatoren

105

2.7.6 Logische Operatoren Mit logischen Operatoren lassen sich Verknüpfungen der booleschen Algebra durchführen. Die booleschen Verknüpfungen sind: 왘 UND Zwei logische Werte werden mit der UND-Funktion miteinander verknüpft. Das Ergebnis ist nur dann TRUE, wenn beide Operanden TRUE sind, in allen anderen Fällen ist das Ergebnis FALSE. Dazu ein kleines Beispiel aus dem Alltagsleben von Kinogängern: »Wir gehen nur dann ins Kino, wenn noch Plätze frei sind UND das Wetter schlecht ist.« 왘 ODER Zwei logische Werte werden mit der ODER-Funktion miteinander verknüpft. Das Ergebnis ist nur dann FALSE, wenn beide Operanden FALSE sind, in allen anderen Fällen ist das Ergebnis TRUE. Auch hier ein kleines Beispiel: »Wenn ich heute einkaufe ODER meine offenen Rechnungen bezahle, dann habe ich morgen kein Geld mehr.« (Vor allem seit der Einführung des Euro ist besonders der erste Fall sehr wahrscheinlich.) 왘 NICHT Mit diesem unären Operator wird der aktuelle logische Wert des Operanden umgekehrt (negiert). Aus TRUE wird FALSE und umgekehrt. 왘 EXOR Diese wohl beliebteste Verknüpfung der booleschen Algebra liefert TRUE, wenn die beiden Operanden ungleich sind, FALSE bei Gleichheit der Operanden. Die UND-Funktion hat eine höhere Priorität als die ODER-Funktion. Werden beide Funktionen in einem Ausdruck verwendet, sollte man der besseren Lesbarkeit und Verständlichkeit halber Klammern setzen: a # ( #

UND b ODER c UND d ist dasselbe wie a UND b ) ODER ( c UND d ) aber mit Klammern wird es besser ersichtlich

# Sollen zuerst b und c verknüpft werden, dann # muss man sowieso Klammern setzen: a UND ( b ODER c ) UND d

Die Operanden werden von links nach rechts evaluiert. Das bedeutet im Falle der UND-Funktion, dass der zweite Operand überhaupt nicht evaluiert wird, wenn der erste Operand bereits FALSE ist.

106

2

Grundlagen

Umgekehrt wird bei der ODER-Funktion der zweite Operand ebenfalls nicht evaluiert, wenn der erste Operand bereits TRUE ist. Diese Tatsache kann man beim Kodieren von hoch performantem Code berücksichtigen, indem man die Reihenfolge der Operanden je nach Lage der Dinge vertauscht.

Die Operatoren && und and Sowohl && als auch and führen eine logische UND-Verknüpfung zweier Operanden durch. and hat jedoch niedrigere Priorität. Beispiele: my $flag1 = 0; my $flag2 = 1; my $result = $flag1 && $flag2; # Beachte: Hier wird $flag2 nicht evaluiert, # weil $flag1 bereits logisch FALSE ist. Der Interpreter # arbeitet die Operation von links nach rechts ab # und hört in dem Moment auf, in dem sich ein FALSE-Wert # ergibt. # Das Ergebnis ist hier FALSE. # Vorsicht: Hier wird zuerst die Zuweisung von # $flag1 an $result durchgeführt # und dieses erst mit $flag2 # "verundet", da der Operator "and" # eine niedrigere Priorität besitzt als # der Zuweisungsoperator. $result = $flag1 and $flag2; # So funktioniert es wie erwartet: $result = ( $flag1 and $flag2 );

Die Operatoren || und or Sowohl || als auch or führen eine logische ODER-Verknüpfung zweier Operanden durch. or hat jedoch niedrigere Priorität. Beispiele: my $flag1 = "yes"; my $flag2 = ""; # Beachte: Hier wird $flag2 nicht evaluiert, # weil $flag1 bereits logisch TRUE ist. # Das Ergebnis ist TRUE.

Operatoren

107

my $result = $flag1 || $flag2; # Vorsicht: Hier wird zuerst die Zuweisung von # $flag1 an $result durchgeführt und dieses # erst anschließend mit $flag2 "verodert", da der # Operator "or" # eine niedrigere Priorität besitzt als # der Zuweisungsoperator. $result = $flag1 or $flag2; # So funktioniert es wie erwartet: $result = ( $flag1 or $flag2 );

Die Operatoren ! und not Sowohl der Operator ! als auch not führen eine logische Negation des Operanden durch. not hat jedoch eine niedrigere Priorität. Beispiele: my $flag = !1; $flag = not 1; # $flag enthält nach der Zuweisung den leeren String ''. # Jeder beliebige Wert, der im Sinne von Perl zu TRUE # evaluiert, ergibt in negierter Form den leeren String. $flag = !0; $flag = not 0; # $flag enthält nach der Zuweisung die Zahl "1". # Jeder beliebige Wert, der im Sinne von Perl zu FALSE # evaluiert, ergibt in negierter Form die Zahl "1".

Operator xor Der xor-Operator führt eine logische Exklusiv-Oder-Verknüpfung der beiden Operanden durch. Beispiele: my $flag1 = "yes"; my $flag2 = ""; # Vorsicht: Hier wird zuerst die Zuweisung von # $flag1 an $result durchgeführt, # anschließend dieses mit $flag2 # exklusiv-oder verknüpft, da der Operator "xor" # eine niedrigere Priorität besitzt als der # Zuweisungsoperator. $result = $flag1 xor $flag2;

108

2

Grundlagen

# So funktioniert es wie erwartet: $result = ( $flag1 xor $flag2 ); # $result ist TRUE, da $flag1 einen anderen # logischen Zustand hat als $flag2 # (die beiden Operanden sind ungleich).

2.7.7 Vergleichsoperatoren Mit Vergleichsoperatoren lassen sich zwei Operanden miteinander vergleichen. Das Ergebnis des Vergleichs ist ein logischer Wert. Perl bietet für Zahlen andere Vergleichsoperatoren an als für Strings. Während Zahlen numerisch verglichen werden, findet bei Strings ein lexikalischer Vergleich statt. Dies hat insbesondere dann oft unerwünschte Auswirkungen, wenn Zahlen mit Vergleichsoperatoren für Strings verglichen werden. Beispiele: my $v1 = 10; my $v2 = 5;

Numerisch gesehen kommt »5« vor »10«. Lexikalisch jedoch kommt »10« vor »5«.

2.7.8 Vergleichsoperatoren für Zahlen Operator == Mit dem ==-Operator wird die Gleichheit zweier numerischer Operanden geprüft. Das Ergebnis ist TRUE, wenn beide Operanden denselben Wert haben, sonst evaluiert der Operator zu FALSE. Ist einer der Operanden keine gültige Zahl, erfolgt eine Fehlermeldung. Beispiele: my $op1 = 5; my $op2 = -17.4; my $equals = ( $op1 == $op2 ); # Hinweis: Ich habe der besseren Lesbarkeit halber # Klammern gesetzt. Da der Zuweisungsoperator "=" # eine geringere Priorität besitzt als "==", könnte # man die Klammern hier auch weglassen: $equals = $op1 == $op2; # liefert also dasselbe Ergebnis, würde ich aber # nicht empfehlen. # $equals ist FALSE. $op1 = "a"; $equals = ( $op1 == $op2 );

Operatoren

109

# Dies führt zu einer Fehlermeldung, # da $op1 keine Zahl enthält. # Dasselbe, aber nicht direkt ersichtlich: # Einlesen zweier Werte von der Tastatur my $op10 = ; my $op11 = ; $equals = ( $op10 == $op11 ); # Der Code führt dann zu einer Fehlermeldung, # wenn entweder $op10 oder $op11 keine gültige Zahl ist.

Operator != Der != Operator evaluiert zu TRUE, wenn beide Operanden ungleich sind, andernfalls liefert er FALSE. Ist einer der Operanden nicht numerisch, erfolgt eine Fehlermeldung. Beispiele: my $op1 = 5; my $op2 = -17.4; my $notEquals = ( $op1 != $op2 ); # Hinweis: Ich habe der besseren Lesbarkeit halber # Klammern gesetzt. Da der Zuweisungsoperator "=" # eine geringere Priorität besitzt als "!=", könnte # man die Klammern hier auch weglassen: $equals = $op1 != $op2; # $notEquals ist TRUE. $op1 = "a"; $notEquals = ( $op1 != $op2 ); # Dies führt zu einer Fehlermeldung, da $op1 keine Zahl # enthält. # Dasselbe, aber nicht direkt ersichtlich: # Einlesen zweier Werte von der Tastatur my $op10 = ; my $op11 = ; $notEquals = ( $op10 != $op11 ); # Der Code führt dann zu einer Fehlermeldung, wenn # entweder $op10 oder $op11 keine gültige Zahl ist.

Operator > Der >-Operator evaluiert zu TRUE, wenn der linke Operand größer ist als der rechte Operand, andernfalls liefert er FALSE. Ist einer der Operanden nicht numerisch, erfolgt eine Fehlermeldung.

110

2

Grundlagen

Beispiele: my $op1 = 5; my $op2 = -17.4; my $greater = ( $op1 > $op2 ); # $greater ist TRUE, da $op2 zwar zahlenmässig grösser, # aber negativ ist. # Hinweis: Ich habe der besseren Lesbarkeit halber # Klammern gesetzt. Da der Zuweisungsoperator "=" # eine geringere Priorität besitzt als ">", könnte # man die Klammern hier auch weglassen: $equals = $op1 > $op2; $op1 = "a"; $greater = ( $op1 > $op2 ); # Dies führt zu einer Fehlermeldung, da $op1 # keine Zahl enthält. # Dasselbe, aber nicht direkt ersichtlich: # Einlesen zweier Werte von der Tastatur my $op10 = ; my $op11 = ; $greater = ( $op10 > $op11 ); # Der Code führt dann zu einer Fehlermeldung, wenn # entweder $op10 oder $op11 keine gültige Zahl ist.

Operator < Der = und und perl -w use strict; # Einlesen zweier Werte von der Tastatur my $v1 = ; my $v2 = ; print( "v1 v2 = ", $v1 $v2, "\n" ); ^Z 3 a Argument "a\n" isn't numeric in numeric comparison () at - line 6, line 2. v1 v2 = 1

Der -Operator wird häufig in Sortierfunktionen verwendet, um numerisch zu sortieren. Per Default verwendet die sort()-Funktion die lexikalische Sortierung. Wir werden später noch intensiv auf die Verwendung der Funktion zu sprechen kommen.

112

2

Grundlagen

2.7.9 Vergleichsoperatoren für Strings Alle Vergleichsoperatoren für Strings können auch auf Zahlen und numerische Werte in Variablen benutzt werden. Die Zahlenwerte werden vor dem Vergleich implizit in Strings umgewandelt. Allerdings ist zu beachten, dass alle Stringvergleiche lexikalisch und case-sensitive sortieren (»10« kommt lexikalisch vor »5«). Deutsche Umlaute werden per Default bei einer Sortierung als Sonderzeichen behandelt, die vom Zeichencode her hinter allen normalen Zeichen kommen. Das hat folgende Auswirkung: D:\>perl -w use strict; my $s1 = "außen"; my $s2 = "äußern"; print( join( ", ", sort( $s1, $s2 ) ), "\n" ); ^Z außen, äußern

Eigentlich müsste das Wort »äußern« vor »außen« kommen. Das Problem kann man mit der Direktive use locale;

jedoch beheben: D:\>perl -w use strict; use locale; my $s1 = "außen"; my $s2 = "äußern"; print( join( ", ", sort( $s1, $s2 ) ), "\n" ); ^Z äußern, außen

Nun stimmt die Welt wieder. Eine genaue Beschreibung für die Benutzung von Locales erhält man mit dem Aufruf: perldoc perllocale

Operator eq Der eq-Operator entspricht dem ==-Operator für Zahlen, jedoch werden die Operanden lexikalisch verglichen. Im Gegensatz zum ==-Operator funktioniert der eq-Operator durch die automatische Umwandlung sowohl für Zahlen als auch für Strings. Der lexikalische Vergleich ist case-sensitive.

Operatoren

113

Beispiele für die Benutzung des eq-Operators: # Normaler Vergleich zweier Strings if ( $stringVar eq "hallo" ) # Vergleich mit einer Zahl als String if ( $stringVar eq "1" ) # eq kann man auch für numerische Variablen benutzen, # diese werden automatisch in Strings umgewandelt. if ( $numericVar eq 17 )

# Einlesen der Werte für $v1 und $v2 von der Tastatur my $v1 = ; my $v2 = ; my $equalFlag = ( $v1 eq $v2 ); # Hier kommt keine Fehlermeldung wie beim numerischen # Vergleich mit "==", wenn man keine Zahlen eingibt.

Operator ne Der ne-Operator entspricht dem !=-Operator für Zahlen, jedoch werden die Operanden lexikalisch verglichen. Im Gegensatz zum !=-Operator funktioniert der ne-Operator sowohl für Zahlen als auch für Strings. Der lexikalische Vergleich ist case-sensitive. Beispiel: # Einlesen der Werte für $v1 und $v2 von der Tastatur my $v1 = ; my $v2 = ; my $notEqualFlag = ( $v1 ne $v2 ); # Hier kommt keine Fehlermeldung wie beim numerischen # Vergleich mit "!=", wenn man keine Zahlen eingibt.

Operator gt Der gt-Operator entspricht dem >-Operator für Zahlen, jedoch werden die Operanden lexikalisch verglichen. Der lexikalische Vergleich ist case-sensitive. Beispiel: "hallo" gt "Hallo" # # "300" gt "4" # # 300 gt 4 # #

Evaluiert zu true, weil 'hallo' nach 'Hallo' kommt. Evaluiert zu false, da 3 im Alphabet vor 4 kommt. Dito, Zahlen werden automatisch in Strings konvertiert.

114

2

Grundlagen

Vorsicht ist geboten, wenn man Zahlen mit dem gt-Operator vergleichen möchte: D:\>perl -w use strict; # Einlesen zweier Werte von der Tastatur my $v1 = ; my $v2 = ; if ( $v1 gt $v2 ) { print( "$v1 groesser als $v2\n" ); } else { print( "$v2 kleiner als $v2\n" ); } ^Z 3 10 3 groesser als 10 D:\>

Wie wir sehen, hat da irgendjemand einen Fehler gemacht. Wie so häufig muss man diesen beim Programmierer suchen und nicht den Interpreter beschimpfen. Die Ausgabe ist lexikalisch sortiert richtig, numerisch sortiert aber falsch, also haben wir im Programmcode einen Denkfehler gemacht. Abhilfe für das Problem schafft folgender Code, der zum einen auch Zahlen richtig vergleicht, auf der anderen Seite aber beliebige Strings vergleicht (natürlich nur bis zur angegebenen maximalen Länge): my $v1 = ; my $v2 = ; $v1 = sprintf( "%10d", $v1 ); $v2 = sprintf( "%10d", $v2 );

Im Moment müssen wir die Funktion sprintf() noch nicht verstehen, es reicht zu wissen, dass die Variablen mit führenden Nullen aufgefüllt werden, so dass sie immer eine Länge von 10 Zeichen haben.

Operator lt Der lt-Operator entspricht dem perl -w use strict; # Einlesen zweier Werte von der Tastatur my $v1 = ; my $v2 = ; if ( $v1 lt $v2 ) { print( "$v1 kleiner als $v2\n" ); } else { print( "$v2 groesser als $v2\n" ); } ^Z 3 10 3 groesser als 10 D:\>

Es tritt wie schon beim gt-Operator ein Fehler auf, da die Ausgabe lexikalisch sortiert richtig ist, numerisch sortiert aber falsch. Auch hier kann man mit der sprintf()-Funktion Abhilfe schaffen: my $v1 = ; my $v2 = ; $v1 = sprintf( "%10d", $v1 ); $v2 = sprintf( "%10d", $v2 );

Damit werden alle Eingaben mit führenden Nullen aufgefüllt, und die Sache ist im Lot.

Operator ge Der ge-Operator entspricht dem >=-Operator für Zahlen, jedoch werden die Operanden lexikalisch verglichen. Der lexikalische Vergleich ist case-sensitive. Man kann bei Zahlen in die gleiche Falle wie bei gt und lt stolpern.

Operator le Der le-Operator entspricht dem 0 ) { print( "$w1 kommt nach $w2\n" ); } else { print( "$w1 kommt vor $w2\n" ); }

Wenn wir den gezeigten Programmcode ausführen, erhalten wir die Ausgabe: augen kommt vor äugen

Das ist natürlich falsch, da das Wort »äugen« lexikalisch wie das Wort »aeugen« behandelt werden und damit lexikalisch vor »augen« kommen muss. Die Direktive use locale; löst das Problem, da nun deutsche Umlaute richtig behandelt werden (wir werden noch öfter auf dieses Problem zu sprechen kommen):

Operatoren

117

use strict; use locale; my $w1 = "augen"; my $w2 = "äugen"; if ( ( $w1 cmp $w2 ) > 0 ) { print( "$w1 kommt nach $w2\n" ); } else { print( "$w1 kommt vor $w2\n" ); }

Nun gibt unser Programmcode richtigerweise aus: augen kommt nach äugen

Die Direktive use locale; in Verbindung mit deutschen Umlauten funktioniert natürlich nur, wenn der Computer auf deutsche Sprache eingestellt ist! Andernfalls nimmt Perl an, es handelt sich um amerikanische Computer, und die können nun mal keine deutschen Umlaute (obwohl sie mindestens 5 Sprachen sprechen: Englisch, Amerikanisch, Kanadisch, Hawaiianisch, Australisch).

2.7.10 Bit-Operatoren Bisher haben wir Operatoren kennen gelernt, die sich mit Strings, Zahlen und booleschen Werten beschäftigen. Nun wollen wir ans Eingemachte gehen und Operatoren besprechen, die ihren Wirkungsbereich mehr in den Registern der CPU haben, die so genannten Bit-Operatoren. Dort existieren nur »Nullen« und »Einsen«. A propos, da wir gerade von CPU-Registern sprechen, sei ein kleiner Hinweis erlaubt: Alle Bit-Operatoren sind auf die Breite der CPU-Register beschränkt, die im Moment auf nahezu allen Rechnern noch 32 Bit beträgt. Das bedeutet, dass Sie keine Zahlen oder Strings (mit Strings macht man in der Regel jedoch sowieso keine Bit-Operationen) für BitOperatoren verwenden sollten, die länger als 32 Bit sind. In naher Zukunft, wenn wir in der 64-Bit-Welt leben werden, haben wir immerhin die doppelte Länge (was spätestens 2038 der Fall sein wird, weil dann auf 32-Bit-Rechnern kein Datum mehr funktioniert).

Operator & Mit dem &-Operator wird eine bitweise UND-Verknüpfung beider Operanden durchgeführt. Beispiel: my $op1 = 0x7f; my $op2 = 0x3; my $result = $op1 & $op2;

118

2

Grundlagen

# $result enthält 0x03 0b0111 1111 (0x7f) 0b0000 0011 (0x03) ----------0b0000 0011 (0x03)

Operator | Mit dem |-Operator wird eine bitweise ODER Verknüpfung beider Operanden durchgeführt. Beispiel: my $op1 = 0x7f; my $op2 = 0x3; my $result = $op1 | $op2; # $result enthält 0x7f 0b0111 1111 (0x7f) 0b0000 0011 (0x03) ----------0b0111 1111 (0x7f)

Operator ^ Mit dem ^-Operator wird eine bitweise EXOR Verknüpfung beider Operanden durchgeführt. Beispiel: my $op1 = 0x7f; my $op2 = 0x3; my $result = $op1 ^ $op2; # $result enthält 0x7c 0b0111 1111 (0x7f) 0b0000 0011 (0x03) ----------0b0111 1100 (0x7c)

Operator >= 4; # $i enthält 1 0b0001 0010 Der Dereferenzierungsoperator -> dient dazu, auf ein Hash- oder Array-Element über eine Referenzvariable zuzugreifen. Bei Objekten wird er verwendet, um Objektmethoden aufzurufen oder auf Objektattribute zuzugreifen. Damit werden wir uns im Kapitel über Objektorientierte Programmierung noch ausführlich befassen. Auch normale Funktionen von Perl-Modulen können über diesen Operator aufgerufen werden. Beispiele: my @array = ( 1, 2, 3, ); # Array-Referenzvariable: my $arrayRef = \@array; my %hash = ( "type" => "tree", "subtype" => "palm", ); # Hash-Referenzvariable: my $hashRef = \%hash;

120

2

Grundlagen

# Array-Referenz auf anonymes Array: my $refArray = [ 4, 5, 6, ]; # Hash-Referenz auf anonymes Hash my $refHash = { "gender" => "f", "age" = 17, }; $arrayRef->[ 2 ]

# 3. Element von @array über # Referenzvariable

$hashRef->{ "type" } # Element mit dem Key "type" von # %hash über Referenzvariable $refArray->[ 1 ]

# 2. Element des anonymen Arrays # $refArray

$refHash->{ "age" }

# Element mit dem Key "age" des # anonymen Hash $refHash

Verwendung bei Objekten: use IO::Handle; # Aufruf der Methode autoflush() des Objekts # IO::Handle::STDOUT STDOUT->autoflush( 1 ); # Prozedurale Verwendung bei Perl-Modulen # nicht-objektorientierte Programmierung: my $dbh = DBI->connect(...);

Adressoperator \ Der Adressoperator \ wird verwendet, um die Speicheradresse eines Arrays, eines Hashs oder einer Funktion einer Referenzvariable zuzuweisen. Beispiele siehe Dereferenzierungsoperator. # Referenz auf eine Funktion, die das erste Argument # ausgibt. sub myFunc { my $arg = shift; $arg = "undef" unless ( defined( $arg ) ); print( "$arg\n" ); } my $funcRef = \&myFunc; # Aufruf der Funktion über die Referenzvariable &{ $funcRef }( "hello" );

Operatoren

121

I/O-Operator Der -Operator liest aus einer Datei zeilenweise Daten ein. Wenn kein FileHandle angegeben ist, dann wird das vordefinierte FileHandle STDIN verwendet. Der Operator arbeitet unterschiedlich, je nachdem, ob er im skalaren oder im List-Kontext verwendet wird. Zeilenende-Zeichen sind Bestandteil der eingelesenen Zeilen. An dieser Stelle sollte ich ein paar Sätze zu FileHandles sagen: Unter einem FileHandle versteht man eine Systemressource, die das Betriebssystem zur Verfügung stellt, damit man unter anderem auf Dateien im Filesystem der Festplatte zugreifen kann (Lesen, Schreiben, Ändern, Erzeugen). Die Verwaltung solcher FileHandles erfolgt durch das so genannte »I/O-Subsystem« (ins Deutsche übersetzt: »Eingabe/Ausgabesystem«) des Betriebssystemkerns, der neudeutsch auch »Kernel« genannt wird. Zu den Daten, die über FileHandles des I/O-Systems verarbeitet werden können, gehören auch Tastatureingaben, Bildschirmausgaben sowie Datenströme über so genannte »Pipes« oder Netzwerkverbindungen in Form von »Sockets«. Unter einer »Pipe« muss man sich eine Datenverbindung zwischen zwei Prozessen vorstellen, die wie ein Wasserrohr funktioniert: Der Prozess am linken Rohrende schiebt Daten durch das Rohr auf die rechte Seite, wo ein anderer Prozess die ankommenden Daten in Empfang nimmt, um sie weiterzuverarbeiten. Das Betriebssystem stellt jedem Prozess drei vordefinierte FileHandles zur Verfügung: 왘 STDIN (Standard Eingabe) 왘 STDOUT (Standard Ausgabe) 왘 STDERR (Standard Fehlerausgabe) Die Daten von STDIN werden in der Regel von der Tastatur eingelesen (das muss aber nicht immer der Fall sein, zum Beispiel dann nicht, wenn man die Standardeingabe mit Hilfe des -Zeichens die Standard Ausgabe umgelenkt wurde, siehe weiter unten). Die Daten nach STDERR gehen in der Regel auf das gleiche Ausgabemedium wie diejenigen nach STDOUT. Man kann jedoch auch die Standardfehlerausgabe umlenken. Mehr zu FileHandles finden Sie im Kapitel über Ein-/Ausgabe. Beispiel für das zeilenweise Einlesen von Daten über das vordefinierte FileHandle STDIN: # Einlesen von Daten mit "" in skalarem Kontext # und Standard-FileHandle STDIN while ( defined( my $line = ) ) {

122

2

Grundlagen

# Zeilenende-Zeichen entfernen chomp( $line ); print( "$line\n" ); }

Der Operator erlaubt, dass man das FileHandle weglässt: while ( defined( my $line = ) ) { # Zeilenende-Zeichen entfernen chomp( $line ); print( "$line\n" ); }

Ich bevorzuge jedoch die erste Variante, bei der das FileHandle explizit angegeben ist, auch wenn es entfallen darf, weil es die Standardeinstellung ist. Das Programm wird dadurch verständlicher. Es folgt ein Beispiel für das Einlesen aller Zeilen einer Datei. Hier wird der Operator nicht im skalaren Kontext, sondern im List-Kontext verwendet, weil die eingelesenen Zeilen einer Array-Variable zugewiesen werden: # Wir verwenden das Modul "FileHandle", das in der # Standard-Distribution von Perl enthalten ist. use FileHandle; # Öffnen der Datei "C:\temp\data.txt" für lesenden # Zugriff my $fh = new FileHandle( "C:/temp/data.txt" ); # Prüfung, ob die Datei zum Lesen geöffnet werden # konnte unless ( $fh ) { print( STDERR "Fehler\n" ); exit( 1 ); } # Einlesen aller Zeilen der Datei # Hinweis: Wenn die Datei kein Zeilenende-Zeichen # enthält, dann steht der gesamte Datei-Inhalt im # ersten Element von @lines (z.B. bei Binärdateien). my @lines = ; # @lines enthält alle Zeilen der Datei als Elemente # (inklusive Zeilenende-Zeichen). # NICHT vergessen: Die Datei muss wieder geschlossen # werden, damit die damit verbundene Systemressource # im Betriebssystem freigegeben wird. undef( $fh );

Operatoren

123

Beispiel für das Einlesen des gesamten Datei-Inhalts in eine skalare Variable: # Wir verwenden das Modul "FileHandle", das in der # Standard-Distribution von Perl enthalten ist. use FileHandle; # Öffnen der Datei "C:\temp\data.txt" für lesenden # Zugriff my $fh = new FileHandle( "C:/temp/data.txt" ); # Prüfung, ob die Datei zum Lesen geöffnet werden # konnte unless ( $fh ) { print( STDERR "Fehler\n" ); exit( 1 ); } # Einlesen des gesamten Inhalts der Datei my $data = join( "", ); # $data enthält den Inhalt der gesamten Datei als String # NIEMALS vergessen: FileHandles müssen sofort nach # Gebrauch wieder geschlossen werden. undef( $fh );

Bereichsoperator .. Der Bereichsoperator .. definiert einen aufsteigenden Bereich von Zahlen oder Buchstaben bzw. Ziffern (und sogar Strings) und gibt eine Liste zurück. Wir haben ihn schon bei der Vorstellung von Arrays gesehen. Er ist deshalb sehr praktisch, weil man sich damit eine Menge Schreibarbeit sparen kann. Beispiele: # Ausgabe der Zahlen 1 bis 100 foreach my $i ( 1 .. 100 ) { print( "i = $i\n" ); } my @hexDigits = ( "0" .. "9", "a" .. "f" ); # @hexDigits enthält # ( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f )

Wichtig beim Bereichsoperator ist, dass alle Zeichen im angegebenen Bereich monoton aufsteigend sein müssen. Beispiele: my @chars = ( "A" .. "z" ); # Liefert nicht etwa ( "A" bis "Z" und "a" bis "z" ), # sondern nur "A" bis "Z", weil "z" nicht in der

124 # # # # #

2

Grundlagen

aufsteigenden Zeichenklasse der Großbuchstaben enthalten ist. Es wird die gesamte Zeichenklasse, in der "A" enthalten ist, beginnend bei "A" in das Array gestellt.

my @list = ( "y" .. "Z" ); # Liefert nur ( "y", "z" ), # weil "Z" nicht in der aufsteigenden Zeichenklasse # der Kleinbuchstaben enthalten ist. # Es wird, beginnend bei "y", der Rest der Zeichenklasse # der Kleinbuchstaben in das Array gestellt. # Das Ganze funktioniert auch bei Strings, die # Zahlen mit führenden Nullen enthalten: my @mdayNums = ( "01" .. "31" ); # @mdayNums enthält die Nummern der Monatstage # in zweistelligem Format mit führender Null.

Außerdem kann der Bereichsoperator noch bei Arrays verwendet werden, um Teile aus dem Array zu erhalten: my @array = ( 1, 2, 3, 4, 5, 6, 7, ); my @array1 = @array[ 1 .. 4 ]; # @array1 enthält ( 2, 3, 4, 5 )

Um Bereiche aus dem Array @array anzusprechen, muss als Typkennzeichen das AtZeichen »@« vor dem Variablennamen array stehen, nicht das Dollarzeichen $. Der Bereichsoperator hat im skalaren Kontext eine andere Bedeutung als im List-Kontext und wirkt dann wie ein Flipflop. In diesem Kontext kann man zusätzlich den Operator mit 3 statt 2 Punkten verwenden. Mehr Informationen hierzu erhalten Sie mit: perldoc perlop

Bedingungsoperator ?: Der Bedingungsoperator ?: kann als Abkürzung einer if/else-Abfrage verwendet werden (if und else werden weiter unten erläutert). Beispiel: my $v1 = 5; my $v2 = 3; my $flag = undef;

Operatoren

125

if ( $v1 > $v2 ) { $flag = 1; } else { $flag = 0; } # Abkürzung mit dem ?:-Operator my $flag = ( $v1 > $v2 ) ? 1 : 0;

Zunächst wird der Ausdruck links vom Fragezeichen ? evaluiert. Ist das Ergebnis TRUE, dann wird der Ausdruck links vom Doppelpunkt evaluiert, ansonsten der Ausdruck rechts vom Doppelpunkt. Ich setze grundsätzlich Klammern um den Ausdruck vor dem Fragezeichen, damit wird der Code besser lesbar und verständlicher, weil die Prioritäten der Operatoren dann keine Rolle spielen.

Operator path" ) # Datei zum Schreiben öffnen. Der Inhalt einer # bereits evtl. existierenden Datei wird allerdings # nicht gelöscht, Schreiboperationen fügen die # Daten am Ende der Datei an. Existiert die Datei # vorher noch nicht, dann wird sie neu angelegt. new FileHandle( path, "a" ) new FileHandle( path, "a+" ) new FileHandle( ">> path" ) oder: # Die "use"-Direktive wird nun ohne leere Liste # verwendet, d.h., alle exportierten Identifier # werden in den aktuellen Namespace übernommen. # Wir benötigen sie, um die Konstanten angegeben # zu können. use FileHandle; # Nur zum Lesen öffnen new FileHandle( path, O_RDONLY ) # Nur zum Schreiben öffnen new FileHandle( path, O_WRONLY ) # Nur zum Schreiben (Anhängen ans Ende) öffnen new FileHandle( path, O_APPEND )

Ein-/Ausgabe (File I/O)

179

# Zum Lesen und Schreiben öffnen new FileHandle( path, O_RDWR ) # Alle Möglichkeiten noch einmal, diesmal wird # aber die Datei neu angelegt, falls sie vorher # noch nicht existiert. # Funktioniert auch für nur lesendes Öffnen! new FileHandle( path, O_WRONLY | O_CREAT ) new FileHandle( path, O_APPEND | O_CREAT ) new FileHandle( path, O_RDWR | O_CREAT )

Hinweis zu den Konstanten O_RDONLY etc.: Diese Konstanten sind im Perl-Modul Fcntl.pm definiert und werden per Default exportiert, wenn man die Direktive use FileHandle; ohne Liste verwendet. Man kann sie auch gezielt in der Liste der zu importierenden Identifier angeben: use FileHandle qw( O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT );

Die Werte der Konstanten sind binäre Flags, die durch eine ODER-Verknüpfung kombiniert werden können. So besagt die Kombination O_RDWR | O_CREAT # Achtung O_RDWR || O_CREAT # ist falsch, da "||" eine logische Verknüpfung ist, # wir aber eine binäre Verknüpfung benötigen.

zum Beispiel, dass die Datei für schreibenden und lesenden Zugriff geöffnet werden soll. Außerdem soll die Datei neu angelegt werden, falls sie noch nicht existiert. Beachte: Die ODER-Verknüpfung muss mit dem Bit-Operator »|« erfolgen, nicht mit dem logischen Operator »||«. Weiterführende Informationen gibt es mit dem Kommando perldoc Fcntl

und natürlich über die grafische Dokumentationsoberfläche der Perl-Distribution. Die Packages FileHandle und DirHandle bilden nur Wrapper für die internen PerlFunktionen wie zum Beispiel open() ab und sind nicht als Ersatz der internen Funktionen gedacht. Sie bieten dem Programmierer aber ein freundliches API (Application Programming Interface) an.

180

2

Grundlagen

Der Konstruktor new() des Perl-Moduls FileHandle liefert entweder eine Objektreferenz auf ein gültiges FileHandle zurück oder undef, wenn ein Fehler aufgetreten ist. In letzterem Fall enthält die vordefinierte Variable $! die Fehlermeldung bzw. den Fehlercode. Alle wichtigen vordefinierten Variablen sind übrigens im Anhang beschrieben. Beispiele für das Öffnen einer bereits existierenden Datei: use FileHandle; # Öffnen für ausschließlichen Lesezugriff # (Schreibzugriffe führen zu einem Fehler) # Leeres FileHandle-Objekt instanzieren my $fh = new FileHandle(); # Datei 'bla.txt' zum Lesen öffnen $fh->open( "< bla.txt" ); # Es geht auch nur mit dem Konstruktor my $fh = new FileHandle( "bla.txt", "r" ); # oder my $fh = new FileHandle( "[ 1 ] # Es geht aber auch so: $ar[ 0 ][ 1 ]

Streng genommen muss der Dereferenzierungsoperator -> verwendet werden, da jedes Element von »@ar« eine Array-Referenz ist. Perl macht aber bei aufeinander folgenden eckigen Klammern eine Ausnahme. In diesem Fall darf der Operator -> fehlen. Diese Ausnahme gilt übrigens auch bei geschweiften Klammern (Hashes), wie wir weiter unten noch sehen werden. Bei der folgenden Definition sind alle Ebenen des Arrays Referenzen (auch die Variable ar für die erste Ebene der Arrayhierarchie wird nun als skalare Referenzvariable auf ein anonymes Array deklariert): # Definition eines zweidimensionalen Arrays mit Hilfe # einer Referenzvariable my $ar = [ [ 1, 2, 3, ], [ 10, 20, 30, ] ]; # Hier muss in der obersten Ebene der Operator -> # verwendet werden, aber in allen weiteren Dimensionen # darf man den Operator weglassen. $ar->[ 1 ][ 2 ] # Dasselbe nochmal in ausführlicher Schreibweise $ar->[ 1 ]->[ 2 ]

Mehrdimensionale Arrays

263

Zur Veranschaulichung ein Beispiel für mehrdimensionale Arrays. Es werden Zeilen von STDIN gelesen (Beenden der Eingabe unter Windows mit ^Z und Zeilenende-Zeichen, unter UNIX mit ^D). Jede einzelne Zeile wird zu einem Array-Element, ist aber zugleich ebenfalls ein Array, dessen Elemente alle in der Zeile eingegebenen Wörter sind. Ein kleines Beispiel erleichtert das Verständnis: Sie geben ein: Perl ist so schön, dass ich eigentlich nur noch in Perl programmieren möchte. Java ist zwar heutzutage das "Non-plus-Ultra", aber ich glaube, dass es gegenüber Perl immer noch gravierende Nachteile hat.

Aus diesen Eingaben soll die folgende Arraystruktur gebildet werden: [ "Perl", "ist", "so", "schön" ], [ "dass", "ich", "eigentlich", "nur", "noch", "in", "Perl", "programmieren" ], [ "möchte", ], [ "Java", "ist", "zwar", "heutzutage", "das", "\"Non-plus-Ultra\"", ], [ "aber", "ich", "glaube,", "dass", "es", "gegenüber", "Perl", ], [ "immer", "noch", "gravierende", "Nachteile", "hat", ]

Der gesamte Eingabestrom wird in ein Array gegliedert. Jede Zeile entspricht einem Array-Element und ist ihrerseits wiederum ein Array, dessen Elemente alle Wörter der Zeile sind. Hier der Programmcode: #!D:/Perl/bin/perl.exe -w use strict; # Array, das die eingegebenen Zeilen aufnimmt my @lines = (); # Eingabeschleife, die so lange durchlaufen wird, # bis man auf der Tastatur Strg-D (UNIX) # bzw. Strg-Z (Windows) eingibt. while ( defined( my $line = ) ) { # Zeilenende-Zeichen entfernen chomp( $line ); # Die nächste Anweisung erzeugt zunächst eine # Wortliste,

264

4

Komplexe Datentypen

# hängt an das Array ein Element an und erzeugt # daraus eine Array-Referenz, # welche die Wortliste aufnimmt. ${ $lines[ $#lines + 1 ] } = [ split( /[,.\s]+/, $line ) ]; }

Hier noch einmal die Langfassung der Zuweisung: ${ $lines[ $#lines + 1 ] } = [ split( /[,.\s]+/, $line ) ];

Die split()-Funktion erzeugt aus dem skalaren String einer Eingabezeile eine Liste aller Wörter. Als Kennzeichen für die Umwandlung dient der Pattern Matching-Ausdruck /[,.\s]+/. Ins Deutsche übersetzt bedeutet dies: Erzeuge eine Liste von Zeichenketten. Die Sonderzeichen »,«, ».« und alle »White Space«-Zeichen dienen als Trennzeichen und werden nicht mit in die Liste aufgenommen. Sie können auch mehrfach hintereinander folgen. Der String »hallo du« wird also in die Liste »( 'hallo', 'du' )« umgewandelt. Aber auch »hallo,..., ,.,,,du« erzeugt genau dieselbe Liste. Durch die eckigen Klammern wird aus der Liste eine anonyme Array-Referenz gebildet. Diese nun weist man mit dem Zuweisungsoperator = dem Ausdruck ${ $lines[ $#lines + 1 ] }

zu. Der Ausdruck $#lines + 1 bewirkt, dass das Array @lines um ein Element am Ende erweitert wird, dem dann die Arrayreferenz zugewiesen wird. Manchmal begegnet man Arrayreferenzen, die gar nicht danach aussehen. Zur Veranschaulichung schreibe ich die Umwandlung der Zeile in eine Wortliste ein bisschen anders: @{ $lines[ $#lines + 1 ] } = split( /\s+/, $line );

Der Code scheint falsch zu sein, weil Array-Element Skalare sein müssen. Wir erzeugen mit dem Code aber ein normales Array mit dem Typkennzeichen @. In Wirklichkeit jedoch handelt es sich um eine Referenz, die vom Perl-Interpreter automatisch erzeugt wird. Dies lässt sich mit Hilfe der Funktion ref() leicht beweisen: print( ref( $lines[ $#lines ] ) || "keine Referenz" );

Der Code gibt den String »ARRAY« aus, wenn das letzte Element von @lines eine ArrayReferenz ist, andernfalls wird »keine Referenz« ausgegeben. Wenn wir den Code ausführen, erhalten wir als Ausgabe ARRAY

Mehrdimensionale Arrays

265

Sie werden häufig in Programmen die eine oder die andere Schreibweise finden, je nachdem, welche Vorliebe der Autor des Codes hat. Wir hätten den Code auch wie folgt schreiben können: # Es geht auch so: while ( defined( my $line = ) ) { chomp( $line ); push( @lines, [ split( /[,.\s]+/, $line ) ] ); }

Wenn man den Code wie folgt ändert: original: push( @lines, [ split( /\s+/, $line ) ] ); falsch: push( @lines, split( /\s+/, $line ) );

dann hat man einen Fehler gemacht. In der zweiten Anweisung fehlen die eckigen Klammern, mit denen man die Liste in eine Arrayreferenz umwandelt. Die Folge: Alle Wörter der Liste werden in der Reihenfolge, in der sie eingegeben wurden, ans Ende des Arrays @lines angehängt. Dieses wird also mit jeder Zeile größer, und es entstehen keine untergeordneten Arrays für die Zeilen, sondern nur ein eindimensionales Array, das alle Wörter enthält. Ausgegeben wird das Zeilen-Array des Beispiels mit folgendem Code: foreach my $line ( @lines ) { # $line ist eine Array-Referenz! print( join( " ", @{ $line } ), "\n" ); } # Oder mit einer "for" Schleife for ( my $i = 0; $i { $age }->{ $ln }->{ $fn }++;

da bis auf die erste Ebene alle Hash-Keys Referenzen auf Hashes sind. Aber wie bei Arrays kann der Operator -> entfallen, wenn zwei verschiedene geschweifte Klammern aufeinander folgen. Wer nicht weiß, welche Priorität höher ist (-> oder ++), der kann Prioritäten auch explizit durch runde Klammer setzen: ( $pers{ $ge }{ $age }{ $ln }{ $fn } )++;

Als Gegenüberstellung hier der Code für ein flaches Design, bei dem es nur ein Hash gibt: my %pers = (); while ( defined( my $line = ) ) { chomp( $line ); my ( $ln, $fn, $ge, $age ) = split( /\s+/, $line ); # Als Trennzeichen der einzelnen Bestandteile # im Hash-Key wird das TAB-Zeichen "\t" verwendet unless ( exists( $pers{ "$ge\t$age\t$ln\t$fn" } ) ) { $pers{ "$ge\t$age\t$ln\t$fn" } = 0; } $pers{ "$ge\t$age\t$ln\t$fn" }++; # Oder auch # ( $pers{ "$ge\t$age\t$ln\t$fn" } )++; }

Und hier der Code zum Ausgeben der Datensätze für das geschachtelte Design: 01 foreach my $ge ( sort( keys( %pers ) ) ) { 02 # Hilfsvariable $href, sie dient der Abkürzung 03 my $href = $pers{ $ge }; 04

270 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 }

4

Komplexe Datentypen

foreach my $age ( sort( { $a $b } keys( %{ $href } ) ) ) { # Hilfsvariable $hr, sie dient der Abkürzung my $hr = $href->{ $age }; foreach my $ln ( sort( keys( %{ $hr } ) ) ) { # Und noch einmal eine Abkürzung my $h = $hr->{ $ln }; # Dies ist wesentlich besser lesbarer als: # $pers{ $ge }{ $age }{ $ln } foreach my $fn ( sort( keys( %{ $h } ) ) ) { my $cnt = $h->{ $fn }; print( "$ln $fn $ge $age = $cnt\n" ); } } }

Erläuterungen: Im Beispiel für die Ausgabe der Datensätze wurden Hilfsvariablen benutzt, um eine abkürzende Schreibweise zu erreichen: Aus: $pers{ $ge } $pers{ $ge }{ $age } $pers{ $ge }{ $age }{ $ln }

wird jetzt: $href $hr $h

Bei der abkürzenden Schreibweise muss der Operator -> hinter dem Variablennamen verwendet werden, da $href, $hr und $h Referenzvariablen sind. Die Ausgabe der Datensätze erfolgt sortiert (in Zeile 06 werden die Keys mit dem Operator numerisch sortiert, in Zeile 11 wird die Standardsortierung verwendet, bei der die Keys lexikalisch aufsteigend sortiert sind). Beim flachen Design kann man dies nicht auf einfache Art erreichen: foreach my $key ( sort( keys( %pers ) ) ) { my ( $ge, $age, $ln, $fn ) = split( /\t/, $key ); my $cnt = $pers{ $key }; print( "$ln $fn $ge $age = $cnt\n" ); }

Mehrdimensionale Hashes

271

Hier ist auch das Alter Bestandteil der lexikalischen Sortierung, damit kommt jemand, der 7 Jahre alt ist, nach jemandem, der 12 Jahre alt ist, und nicht vorher. Dieses Problem könnte man beseitigen, indem man beim Erstellen der Keys das Alter mit führenden Nullen erzeugt und diese vor der Ausgabe wieder entfernt: my %pers = (); while ( defined( my $line = ) ) { chomp( $line ); my ( $ln, $fn, $ge, $age ) = split( /\s+/, $line ); # Alter dreistellig mit führenden Nullen $age = sprintf( "%03d", $age ); # Es gibt wohl kaum einen Menschen, # der älter als 999 Jahre ist. unless ( exists( $pers{ "$ge\t$age\t$ln\t$fn" } ) ) { $persons{ "$ge\t$age\t$ln\t$fn" } = 0; } $pers{ "$ge\t$age\t$ln\t$fn" }++; # Oder auch # ( $pers{ "$ge\t$age\t$ln\t$fn" } )++; } ... foreach my $key ( sort( keys( %pers ) ) ) { my ( $ge, $age, $ln, $fn ) = split( /\t/, $key ); $age =~ s/^0+//; my $cnt = $pers{ $key }; print( "$ln $fn $ge $age = $cnt\n" ); }

Mehrdimensionale Hashes können unter anderem für mehrsprachige Meldungen verwendet werden. Beispiel: my %messages = ( "de" => { "success" => "Erfolg", "error" => "Fehler", }, "en" => { "success" => "Success",

272

4

Komplexe Datentypen

"error" => "Error", }, ); # Oder, nach einer anderen Hierarchie-Philosophie my %messages = ( "success" => { "de" => "Erfolg", "en" => "Success", }, "error" => { "de" => "Fehler", "en" => "Error", }, );

Die Variable %messages ist eine normale Hash-Variable (obwohl wir sie natürlich auch als Referenzvariable definieren könnten; in diesem Fall müssten wir geschweifte Klammern statt der runden verwenden). Die Keys der ersten Ebene sind Referenzen auf anonyme Hashes, die je nach Philosophie entweder auf die Sprachvarianten oder die symbolischen Namen der Meldungstexte zeigen. Die Hashes der untersten Ebene enthalten die eigentlichen Meldungstexte. Auch sie werden über Referenzen angesprochen. Je nach Philosophie enthalten die Keys entweder die symbolischen Namen der Texte oder die Sprachvarianten. Im Programm werden nur die englischsprachigen Identifier für die Meldungstexte verwendet, das Skript selbst ist also sprachunabhängig. Der Mechanismus kann in einem Perl-Skript wie folgt verwendet werden: ... # Hier fest verdrahtet Locale auf "de" eingestellt my $locale = "de"; # Hinweis: Bei CGI-Scripts kann man die Locale auch # dynamisch aus dem URI extrahieren. # Beispiel: # URI: /cgi-bin/de/myScript.pl bzw. # /cgi-bin/en/myScript.pl # myScript.pl ist physisch nur einmal vorhanden # (z.B. unter de), alle weiteren (z.B. unter en) sind # "symbolic links" # Man kann den Webserver auch so konfigurieren, dass # "symbolic links" überflüssig sind. # Das Extrahieren der Sprachvariante aus dem URI # erfolgt mit Pattern Matching.

Mehrdimensionale Hashes # # # # # # # # #

273

Wenn z.B. der URI etwa so aussieht: /cgi-bin/de/myScript.pl oder /cgi-bin/en/myScript.pl dann kann man die Sprachvariante mit der folgenden Anweisung extrahieren: my ( $locale ) = $uri =~ m~/cgi-bin/(\w\w)/~; Voraussetzung: Die Sprachvariante ist immer in Form von 2 Zeichen angegeben.

my $href = $messages{ $locale }; ... if ( Operation erfolgreich } { my $msg = $href->{ "success" }; print( "$msg\n" ); } else { print( "$href->{ 'error' }\n" ); }

Im Skript werden nur die englischsprachigen Identifier für die Meldungen verwendet. Damit ist der Programmcode sprachunabhängig. Im letzten print()-Aufruf muss der Hash-Key »error« in einfache Quotes gestellt werden, da er in einem String verwendet wird, der in doppelten Quotes steht. Stattdessen kann man in neueren Perl-Versionen (> 5.005) auch schreiben: print( "$href->{ error }\n" );

weil Hash-Keys vom Interpreter grundsätzlich immer als Strings behandelt werden, auch wenn sie ohne Quotes als so genanntes Bareword stehen. Ein anderes Beispiel für mehrdimensionale Hashes: Personendaten, die in einer Datei oder in der Datenbank gespeichert sind. Die Sortierung bei der Ausgabe ist nicht vorbestimmt, sondern das Perl-Skript kann die Datensätze beliebig sortieren: # Format der Datensätze: ID\tNachname\tVorname\tAlter # Beispieldaten 1\tHuber\tErwin\t22 2\tHuber\tFranz\t30 ... # Einlesen der Datensätze zum Beispiel aus einer Datei: ... my %pers = (); # fileHandle kann STDIN (Standardeingabe) oder ein

274

4

Komplexe Datentypen

# FileHandle einer geöffneten Datei sein, welche die # Datensätze (pro Zeile ein Datensatz) enthält while ( defined( my $line = ) ) { chomp( $line ); my ( $id, $ln, $fn, $age ) = split( "\t", $line ); $pers{ $id } = { "id" => $id, "ln" => $ln, "fn" => $fn, "age" => $age, }; } ...

In diesem Beispiel ist der Key »id« in der Hash-Referenz redundant, da er bereits als Key des Hashes %pers vorhanden ist. Es ist jedoch guter Programmstil, die Redundanz zugunsten besserer Übersichtlichkeit in Kauf zu nehmen. Weiter unten, wenn Hashes in ein Array gepackt werden, gibt es diese Redundanz nicht. Ausgabe der Datensätze, nach ID sortiert: foreach my $id ( sort( { $a $b } keys( %pers ) ) ) { my $href = $pers{ $id }; print( "$href->{ 'ln' }, ", "$href->{ 'fn' } ", "($href->{ 'age' }): $id\n" ); }

Es wird der Codeblock { $a $b } für die Sortierung verwendet, der die numerischen Keys auch numerisch sortiert, da die Standardsortierung lexikalisch erfolgt. Ausgabe nach Alter sortiert: foreach my $id ( sort( { $pers{ $a }{ "age" } $pers{ $b }{ "age" } } keys( %pers ) ) ) { my $href = $pers{ $id }; print( "$href->{ 'ln' }, ", "$href->{ 'fn' } ($href->{ 'age' }): $id\n" ); }

Mehrdimensionale Hashes

275

Es dürfen jetzt nicht die Keys selbst als Kriterium für die Sortierung verwendet werden, sondern die Values des Keys »age« des Subhashes, der durch die numerische ID referenziert wird. Das Ganze noch einmal anders ausgedrückt: Mit keys( %pers ) erhält man eine unsortierte Liste von numerischen IDs. Diese Liste muss nun so umgewandelt werden, dass sie nach Alter sortiert ist. Auf das Alter kann man aber nur über den Key »age« des jeweiligen Subhashes zugreifen. Der Code sort({ $pers{ $a }{ "age" } $pers{ $b }{ "age" } })

besagt: Nimm aus der ID-Liste jeweils zwei Elemente ($a und $b) und sortiere dann nach dem, was im Key »age« der beiden zu vergleichenden Subhash-Elemente steht. $a und $b sind die numerischen IDs, welche von der Funktion keys() an die sort()-Funk-

tion übergeben werden. Diese beiden Variablen sind in der sort()-Funktion von Perl vordefiniert. Mehr zu diesem Thema findet sich in der Beschreibung der sort()-Funktion. Ausgabe nach Nachname sortiert: foreach my $id ( sort( { $pers{ $a }{ "ln" } cmp $pers{ $b }{ "ln" } } keys( %pers ) ) ) { my $href = $pers{ $id }; print( "$href->{ 'ln' }, ", "$href->{ 'fn' } ($href->{ 'age' }): $id\n" ); }

Obwohl hier die lexikalische Sortierung verwendet wird, die ja die Standardsortierung ist, muss trotzdem der Operator cmp in einem Codeblock angegeben sein. Ausgabe nach Nachname und Vorname sortiert: foreach my $id ( sort( { $pers{ $a }{ "ln" } . " " . $pers{ $a }{ "fn" } cmp $pers{ $b }{ "ln" } . " " . $pers{ $b }{ "fn" } } keys( %pers ) ) ) { my $href = $pers{ $id };

276

4

Komplexe Datentypen

print( "$href->{ 'ln' },", "$href->{ 'fn' } ($href->{ 'age' }): $id\n" ); }

In diesem Beispiel werden für die Sortierung Nachname und Vorname zu einem Namen (mit Leerzeichen getrennt) zusammengefügt.

4.3 Hash-Arrays Hash Arrays bilden Datenstrukturen, bei denen die Elemente eines Arrays Referenzen auf Hashes sind. Sie werden verwendet, wenn man sortierte Datenstrukturen von einer Datei oder aus der Datenbank liest. Ein Beispiel mit Personendaten, die bereits vorsortiert sind: # Format der Datensätze: ID\tNachname\tVorname\tAlter # Beispieldaten 1\tHuber\tErwin\t22 2\tHuber\tFranz\t30 ... # Einlesen der Daten von einer Datei: ... my @pers = (); while ( defined( my $line = ) ) { chomp( $line ); my ( $id, $ln, $fn, $age ) = split( "\t", $line ); # Jedes Array-Element wird zu einer Hash-Referenz. # Nebenbei bemerkt: $#persons + 1 erweitert das # Array automatisch um ein weiteres Element, # das wir als Hash-Referenz definieren, indem # wir geschweifte Klammern benutzen $pers[ $#pers + 1 ] = { "id" => $id, "ln" => $ln, "fn" => $fn, "age" => $age, }; } ...

Jedes einzelne Element des Arrays ist eine Referenz auf ein anonymes Hash und daher eine Referenzvariable.

Hash-Arrays

Beispiel zum Ausgeben der Datensätze: ...

foreach my $href ( @persons ) { # $href ist eine Referenzvariable auf ein anonymes # Hash print( "$href->{ 'ln' }, ", "$href->{ 'fn' } ($href->{ 'age' }): $id\n" ); }

277

5 Objektorientierte Programmierung In diesem Kapitel werden wir lernen, dass gute objektorientierte Programmierung (kurz »OOP«) auch in Perl kein Problem ist. Bei der prozeduralen Programmierung steht die Programmlogik im Vordergrund. Sie definiert Funktionen für das Verarbeiten von Daten. Bei komplexen Daten wird diese Art der Programmierung aber schnell unübersichtlich, und die Skripts sind im Nachhinein nicht einfach zu ändern. Im Gegensatz zur prozeduralen Programmierung stehen bei der objektorientierten Vorgehensweise die Daten selbst im Mittelpunkt. Sie sind nun nicht mehr einfache Parameter von Funktionsaufrufen, die als Kopie oder Referenz übergeben werden, sondern treten selbst in Aktion und sind sozusagen »lebendig«. Objekte besitzen Eigenschaften (so genannte Attribute, englisch »Attributes«) und bieten Methoden an, mit denen diese Eigenschaften gelesen oder verändert werden können. Auch Aktionen, die sich auf das gesamte Objekt beziehen, werden von den Objekten selbst in Form von Methoden zur Verfügung gestellt. Der Vorteil objektorientierter Programmierung liegt unter anderem darin, dass bei einer Änderung der internen Verarbeitung der Daten die Programmierschnittstelle nach außen unverändert bleibt. Dies nennt man »Kapselung«. Auch bei Änderungen im Programmcode hat OOP die Nase gegenüber herkömmlicher Programmierung vorn, weil man eine Änderung an zentraler Stelle durchführen kann, ohne dass sich diese Änderung wie ein Rattenschwanz durch alle Programme zieht. Ein weiterer Begriff, den wir weiter unten noch ausführlich besprechen werden, spielt bei OOP ebenso eine Rolle, das ist die »Vererbung«. Damit kann man zunächst mit einer relativ einfachen Implementierung beginnen und den bereits erstellten Code an weitere, spezialisierte Module weitervererben. Durch die Vererbung von Attributen und Methoden spart man sich oft eine Menge Schreibarbeit. Hier ein Beispiel für Objekte mit Vererbung: Aus der Abbildung wird deutlich, dass der Wagen von Frau Huber ein Auto (KFZ) ist, den die Behörden als »PKW« klassifizieren. Die Marke des PKW heißt »Lamborghini«, und das Modell ist »Typ 1«.

280

5



Objektorientierte Programmierung

     



  

    !





       ! "   #

$ %  "&    '  #

$ %  "&     ('( 

Abbildung 5.1: Autos als Objekte

Herr Müller besitzt ebenfalls einen Lamborghini aus der Modellreihe »Typ 1«. Beide Wagen unterscheiden sich zunächst rein äußerlich durch die verschiedenen Kennzeichen. Natürlich sind auch Fahrgestellnummer, Ausstattung, Motor etc. nicht identisch. Diese individuellen Eigenschaften werden beim Zusammenbauen des Autos in der Fabrik festgelegt, bis am Ende eine physische Ausprägung des Objekts »Modell Typ 1« vom Fließband läuft, die man »Instanz« nennt. Das Zusammenbauen einer solchen Instanz wird als »Instanzierung« bezeichnet. Sehen wir uns noch einmal das Bild an: Jede Objektinstanz ist einer so genannten »Klasse« zugeordnet. Sowohl das Auto von Frau Huber als auch das des Herrn Müller gehören zur Klasse »Typ 1«. In dieser Klasse werden diejenigen Eigenschaften festgelegt, die für alle Instanzen der Klasse »Typ 1« gleichermaßen gelten (z.B. hat jede Instanz zwei Seitentüren). Eine weitere Eigenschaft, nämlich die Gattung »Sportwagen«, ist kein Merkmal, das nur für dieses eine Modell gilt, sondern für alle Wagen der Marke »Lamborghini«. Man kann also sagen, die Klasse »Typ 1« ist eine Unterklasse von »Lamborghini« und erbt das Attribut »Sportwagen«. Gehen wir noch einen Schritt weiter: Jeder Lamborghini, ebenso alle Autos der anderen Marken, werden als »PKW« bezeichnet und haben (meist) 4 Räder. Die Klasse »Lamborghini« kann also auch als Unterklasse von »PKW« angesehen werden und erbt von dieser das Merkmal »4 Räder«. Eine weitere Untergliederung findet man, wenn man zum Beispiel alle PKW und LKW als Unterklassen von »KFZ« definiert, denen das Merkmal »Sie besitzen einen Motor« gemeinsam ist.

281

Nun sollten wir mit den Begriffen »Klasse«, »Instanz« und »Vererbung« keine größeren Probleme mehr haben. In diesem Kapitel will ich mit Ihnen die Vorteile der objektorientierten Programmierung anhand eines einfachen Beispiels erarbeiten. Schreiben wir ein Modul, mit dessen Hilfe Userdaten verarbeitet werden können. Im einfachsten Fall benötigt man hierfür nur den Benutzernamen (englisch »login«) und das Kennwort (englisch »password«, wird meist mit »pwd« abgekürzt). In der folgenden Abbildung möchte ich kurz skizzieren, was auf uns zukommt:         

   

      ! ! "  # !$! "  # 

 

  

   

 

         

Abbildung 5.2: Beispielklasse »User«

Das Modul für unsere Beispielklasse enthält sowohl statische Attribute und Methoden als auch solche, die individuellen Instanzen der Klasse zugeordnet werden. Im Folgenden werden wir auf die einzelnen Bestandteile genauer eingehen.

282

5

Objektorientierte Programmierung

5.1 Klassen Nicht nur in Perl, sondern auch in allen anderen objektorientierten Programmiersprachen werden Objekte durch so genannte Klassen dargestellt. Eine Klasse definiert Eigenschaften (Attribute) und Funktionalitäten (Methoden) für ein bestimmtes Objekt. In Perl ist eine Klasse identisch mit einem Package. Der Programmcode steht in einem Perl-Modul, dessen Dateiname die Endung .pm besitzt und als Prefix den Packagenamen hat. Wenn wir also die Klasse User implementieren, dann schreiben wir den Programmcode in die Datei User.pm. Es hat sich eingebürgert, die Dateinamen von Modulen mit einem Großbuchstaben zu beginnen. Halten wir uns also daran. Beispiel für den grundsätzlichen Aufbau einer Klasse in Form eines Packages: # Datei User.pm package User; use strict; ... 1;

Wie wir deutlich sehen, sieht die Struktur genauso aus wie die eines ganz normalen Perl-Moduls. Am Beginn des Moduls steht die Direktive package, danach folgt die für jeden ordentlichen Programmierer zwingend vorgeschriebene Direktive use strict;, und am Ende unsere obligatorische Zeile 1;. Dazwischen werden wir noch Funktionsdefinitionen implementieren. Eine Klasse definiert zwei Arten von Attributen und Methoden: solche, die für alle Objekte einer Klasse gleichermaßen gelten, und solche, die für jedes einzelne Objekt einer Klasse unterschiedlich sind.

5.1.1 Klassenattribute und Klassenmethoden Diese Art von Attributen und Methoden nennt man auch »statisch«. Klassenattribute sind Eigenschaften der Klasse selbst, die für alle Objekte (Instanzen) der Klasse gleichermaßen gelten. So ist zum Beispiel die maximale Länge des Benutzernamens (nennen wir die Variable $maxNameLen) eines Users für alle Objekte gültig.

Klassen

283

Die Variable, in welcher die Länge gespeichert ist, muss also nur einmal als Klassenvariable definiert werden: package User; ... our $maxNameLen = 64; ...

Die Variable $maxNameLen wird im Package-Scope (englisch für »Geltungsbereich innerhalb eines Moduls«) mit dem Bareword our definiert, sie ist also im gesamten Modul gültig und somit allen Objekten der Klasse gemeinsam. Klassenmethoden bieten Funktionalitäten an, die nicht auf ein einzelnes Objekt der Klasse bezogen sind. Man benötigt also keine individuelle Instanz des Objekts, wenn man eine statische Methode aufruft. Eine Klassenmethode wird wie eine normale Package-Funktion aufgerufen, zum Beispiel User::readUsers(). Die Definition einer Klassenmethode erfolgt genauso wie in normalen prozeduralen, nicht objektorientierten Modulen: sub readUsers { # Code für das Lesen aller Benutzer # Es wird eine Liste der gelesenen Benutzer # an den Aufrufer der Funktion zurückgegeben. # Die einzelnen Elemente der Liste können # dann Instanzen der Klasse "User" sein. }

5.1.2 Konstruktor Den Begriff »Konstruktor« kann man auch mit dem nicht sehr schönen, dafür aber deutschen Wort »Zusammenbauer« übersetzen, denn es sagt genau aus, was damit gemeint ist. Er baut im Hauptspeicher eine Instanz der Klasse zusammen. Erst nach dem Aufruf des Konstruktors kann man auf die individuellen Attribute und Methoden des Objekts zugreifen. In Perl ist der Konstruktor nichts anderes als eine Funktion wie alle anderen auch und sieht deshalb etwa so aus: sub new { # Programmcode }

Der Konstruktor hat in allen Programmiersprachen den Namen new, deshalb heißt der Funktionsname in Perl üblicherweise ebenfalls new. Im Prinzip könnte man sich jeden beliebigen Namen dafür einfallen lassen, bleiben wir aber beim Standard.

284

5

Objektorientierte Programmierung

Der Konstruktor liefert dem Aufrufer eine skalare Referenz auf das individuelle Objekt (Instanz) zurück, die ich im Weiteren »Objektreferenz« nenne. Alle weiteren Aktionen werden dann über Instanzmethoden durchgeführt. In Perl sind dies ebenfalls Funktionen, wie wir weiter unten noch sehen werden. Die vom Konstruktor zurückgelieferte Objektreferenz auf die neu erschaffene Instanz der Klasse ist eine Zwittervariable, denn zum einen ist sie eine Referenzvariable auf das Hash, in dem die Instanzattribute gespeichert sind, zum anderen verhält sie sich wie ein Modul, über das Funktionen aufgerufen werden können. Die Zwittervariable wird fast ausnahmslos $self genannt (obwohl sie jeden beliebigen Namen haben könnte). Bleiben wir bei unserem Beispiel der Klasse User und sehen uns eine Implementierung des Konstruktors für die Klasse an: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

# Modul User.pm package User; # Statische Variablen, die allen Objekten (Instanzen) # der Klasse gemeinsam sind our $maxNameLength = 64; # Konstruktor sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { "login" "pwd" };

=> undef, => undef,

# Überprüfung der Argumente unless ( @_ and $_[ 0 ] and $_[ 1 ] ) { return undef; } if ( length( $_[ 0 ] ) > $maxNameLength ) { return undef; } # Setzen der Instanzattribute "login" und "pwd" $self->{ "login" } = shift( @_ ); $self->{ "pwd" } = shift( @_ ); # Umwandeln der Hash-Referenzvariable zu einer # Objektreferenz

Klassen 33 34 35 36 }

285 bless( $self, $class ); return $self;

Bevor ich näher auf den Programmcode eingehe, möchte ich Ihnen kurz zeigen, wie man den Konstruktor in einem Skript oder in einem anderen Modul benutzt: ... use User; my $user = new User( "Hugo", "secret" ); ...

Der Aufruf des Konstruktors ist bei Perl in vielen Varianten möglich. Häufig werden Sie in Programmen zum Beispiel folgende Version davon sehen: my $user = User->new( "Hugo", "secret" );

Ich persönlich verwende die erste Variante, weil diese Syntax in anderen objektorientierten Programmiersprachen Standard ist. Im Beispiel sehen wir eine Mischung aus statischen Elementen, die allen Instanzen gemeinsam sind, und solchen, die nur für eine einzelne Objektinstanz gelten. Die Variable $maxNameLength ist eine innerhalb des gesamten Moduls gültige Variable und somit ein statisches Attribut, das für alle Objekt Instanzen gleichermaßen gilt, während der Loginname und das Kennwort, die beim Aufruf des Konstruktors übergeben werden, nur für diese eine Objektinstanz gelten. Die Zeilen 10 und 11 my $proto = shift( @_ ); my $class = ref( $proto ) || $proto;

dienen dazu, den Klassennamen für die Instanz zu erhalten, der beim Aufruf der Konstruktorfunktion new() implizit vom Perl-Interpreter als erstes Argument übergeben wird. Diese Eigenart werden wir bei allen Instanzmethoden bei Perl wiedertreffen. Von außen rufen wir den Konstruktor new() so auf: my $user = new User( "Hugo", "secret" );

Der Interpreter fügt aber unsichtbar den Klassennamen als weiteren Parameter an den Anfang der Argumentliste hinzu, als hätten wir den Konstruktor wie folgt aufgerufen: my $user = User::new( "User", "Hugo", "secret" );

286

5

Objektorientierte Programmierung

In der zweiten Zeile wird geprüft, ob das vom Interpreter unsichtbar eingefügte Argument ein einfacher String ist, der den Klassennamen enthält, oder seinerseits eine Referenzvariable auf ein Objekt. Wir hätten nämlich den Konstruktor auch über eine bereits existierende Instanz der Klasse aufrufen können: # Normale Instanzierung einer Objektinstanz my $user = new User( "Hugo", "secret" ); # Instanzierung eines neuen Objekts über eine bereits # existierende Objektreferenz my $user1 = new $user( "Egon", "verysecret" ); # Oder auch so: my $user2 = $user->new( "Willy", "auchsecret" );

Die Variable $class muss aber in jedem Fall den Klassennamen enthalten, da wir diesen weiter unten im Code noch benötigen. Der Klassenname ist in unserem Beispiel der String »User«. Um den Unterschied zu verdeutlichen: Beim Aufruf my $user = new User( "Hugo", "secret" );

enthält die Variable $proto den String User und damit bereits den benötigten Klassennamen, während der Aufruf von ref( $proto ) in diesem Fall einen leeren String zurückliefert und somit einen FALSE-Wert ergibt. Ruft man den Konstruktor aber über eine bereits existierende Instanz der Klasse auf: my $user1 = new $user( "Egon", "verysecret" );

dann liefert der Aufruf ref( $proto ) den Klassennamen User zurück, während die Variable »$proto« selbst eine Objektreferenz ist. Wenn Sie diesen Mechanismus als zu schwierig empfinden, dann empfehle ich Ihnen, gar nicht weiter darüber zu grübeln und einfach immer diese beiden Zeilen zu verwenden, denn damit funktioniert der Code immer. Nicht nachdenken, einfach hinschreiben, heißt hier die Devise. Die Zeilen 13 bis 16 # Alle Attribute einer Objektinstanz sind Elemente # einer Hash-Referenzvariable. # Die Namen der Attribute sind die Hash-Keys, # die Attributwerte sind die Hash-Values. my $self = { "login" => undef, "pwd" => undef, };

Klassen

287

sollten eigentlich keine Probleme bereiten. Sie definieren zunächst nur ein anonymes Hash, das die Objektattribute enthält und nur über die Referenzvariable $self zugänglich ist. Im Prinzip könnte man den Namen der Referenzvariable beliebig wählen, es hat sich bei Perl jedoch eingebürgert, ihn $self zu nennen. Wer andere objektorientierte Programmiersprachen kennt, kann sich unter dem Namen this dasselbe vorstellen. Wir werden weiter unten sehen, dass diese Variable ein »zweites Ich« entwickelt. In den Zeilen 19 bis 25 unless ( @_ and $_[ 0 ] and $_[ 1 ] ) { return undef; } if ( length( $_[ 0 ] ) > $maxNameLength ) { return undef; }

findet ein Check der Argumente des Konstruktors statt. Es wird überprüft, ob deren Anzahl stimmt, ob die Argumente definiert und nicht leer sind und ob die Länge des Benutzernamens in Ordnung ist. Hier sehen wir eine Mischung aus Instanzcode und statischem Code, denn die Variable $maxNameLength ist statisch und für alle Objektinstanzen gleich. Die Zeilen 28 und 29 $self->{ "login" } = shift( @_ ); $self->{ "pwd" } = shift( @_ );

füllen die Instanzattribute »login« sowie »pwd« mit den Argumenten, die beim Aufruf des Konstruktors übergeben wurden. Wie wir weiter unten sehen werden, setzt man Instanzattribute ausschließlich über so genannte »Setter«-Methoden und nicht direkt, wie ich es hier gemacht habe. Der Einfachheit halber wollen wir aber eine Ausnahme von der Regel machen. Eine sehr wichtige Programmzeile ist die Zeile 33 bless( $self, $class );

denn hier wird die Hash-Referenzvariable $self zu einer Objektreferenz gemacht und führt von hier an ein Zwitterdasein, da sie mit dem Aufruf von bless() sowohl eine Referenz auf das Objekt der Klasse User als auch eine Hash-Referenz auf die Attribute der Instanz wird. Erst nachdem man die Referenzvariable an die Klasse gebunden hat, entsteht eine Objektinstanz, die in der nächsten Zeile des Programmcodes an den Aufrufer zurückgegeben wird.

288

5

Objektorientierte Programmierung

Versuchen Sie nicht, in einem Wörterbuch die deutsche Bedeutung des Worts bless zu finden, ich möchte nicht, dass irgendjemand verzweifelt versucht, die Übersetzung »segnen« oder »glückselig machen« mit dem in Einklang zu bringen, was die Funktion bless() wirklich tut: Sie bindet die Hash-Referenz an die Klasse und macht sie zu einer Objektreferenz. Eine Grundregel von OOP lautet: Der Konstruktor sollte so kurz wie möglich sein. Um dieser Regel gerecht zu werden, können wir ein paar Zeilen (19 bis 25) des Konstruktors in eine separate Funktion auslagern, die wir _init() nennen wollen (ich glaube, der Name ist selbsterklärend). sub _init { my ( $login, $pwd ) = @_; unless ( $login and $pwd and ( length( $login ) undef,

Klassen

289

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 }

"pwd"

=> undef,

}; # Überprüfung der Argumente unless ( _init( @_ ) ) { return undef; } # Setzen der Instanzattribute "login" und "pwd" $self->{ "login" } = shift( @_ ); $self->{ "pwd" } = shift( @_ ); # Umwandeln der Hash-Referenzvariable zu einer # Objektreferenz bless( $self, $class ); return $self;

5.1.3 Instanzattribute und Instanzmethoden Im Gegensatz zu statischen Attributen, die für alle Objekte (Instanzen) der Klasse gleichermaßen gelten, repräsentieren Instanzattribute individuelle Eigenschaften einer Objektinstanz und müssen deshalb für jede neue Instanz der Klasse separat in einer Hash-Referenzvariablen gespeichert werden, die beim Anlegen eines neuen Objekts nur in diesem definiert ist. Beispiel für ein Instanzattribut ist das Kennwort der Klasse User, das als Hash-Element mit dem Key »pwd« abgespeichert wird, denn es kann für jedes instanzierte Objekt der Klasse unterschiedlich sein. Zur Rekapitulierung: Bei Perl werden alle Instanzattribute in einer Hash-Referenz abgelegt, wie wir bereits beim Konstruktor gesehen haben: # Alle Attribute einer Objektinstanz sind Elemente # einer Hash-Referenzvariable. # Die Namen der Attribute sind die Hash-Keys, # die Attributwerte sind die Hash-Values. my $self = { "login" => undef, "pwd" => undef, };

Nun wollen wir uns die Methoden näher ansehen, mit denen wir zum einen die Attribute des Objekts lesen oder verändern, zum anderen auch Aktionen wie »Schreibe die Daten des gesamten Objekts in eine Datei« usw. durchführen können.

290

5

Objektorientierte Programmierung

Aufruf von Instanzmethoden Weiter oben haben wir bereits gelernt, wie man statische Funktionen von Perl-Modulen aufruft: # Wir laden ein anderes Perl-Modul use OtherPackage; # Aufruf von Modulfunktionen ohne OOP OtherPackage::func( "a" ); # In der Funktion "func" des Packages "OtherPackage" # lautet die Argumentliste: ( "a" ) # Aufruf mit OOP # (Klassenname bzw. Packagename wird vom # Interpreter heimlich eingefügt) OtherPackage->func( "a" ) # In der Funktion "func" des Packages "OtherPackage" # lautet die Argumentliste: ( "OtherPackage", "a" )

Um es kurz zu machen: Der Aufruf von Instanzmethoden ist fast identisch mit der zweiten Variante, nur schreibt man anstelle des Packagenamens die Objektreferenz: # Wir laden ein anderes Perl-Modul use OtherPackage; # Neue Instanz der Klasse durch Aufruf der # Konstruktorfunktion "new()" erzeugen und in $obj # speichern my $obj = new OtherPackage(); # Aufruf der Instanzmethode "getVersion()" über die # Objektreferenz my $version = $obj->getVersion();

Nach der Variable $obj folgt der Dereferenzierungsoperator ->, den wir schon im Abschnitt über Referenzvariablen kennen gelernt haben. Anschließend gibt man den Namen der aufzurufenden Methode und die Parameterliste an. In unserem Beispiel heißt die Instanzmethode getVersion(), und die Parameterliste ist leer. Die aufgerufene Methode erhält dennoch einen Parameter, den der Interpreter heimlich einfügt, und zwar ist es die Objektreferenz selbst. Somit kann die Funktion getVersion() auf das Hash $self zugreifen, das eigentlich als private Variable in der Konstruktorfunktion new() definiert ist (und normalerweise nur dort gültig wäre). Soweit die allgemeinen Spezialitäten der Instanzmethoden. Die Riege der OOP-Designer hat sich aber noch eine Reihe von Feinheiten einfallen lassen, mit denen ein gewisser Standard erreicht (und bitteschön von allen Programmierern auch eingehalten) werden soll.

Klassen

291

Übrigens: Bei der Namenskonvention von Instanzmethoden gilt wie immer: Funktionsnamen beginnen mit einem Kleinbuchstaben. Ausnahmen sind (wenn auch selten) erlaubt. So werden Instanzmethoden zum Beispiel je nach Einsatzgebiet in verschiedene Kategorien eingeteilt. Diese wollen wir nun näher betrachten.

Getter-Methoden In der objektorientierten Programmierung werden alle Methoden, die nur für das Lesen von Attributen verwendet werden, mit dem Begriff »Getter«-Methoden bezeichnet. Das Wort kommt vom englischen Verb »to get«, das man hier mit »holen« oder »lesen« übersetzen kann. Bei der Namenskonvention für solche Methoden hat sich eingebürgert (und man sollte diesen Standard unbedingt einhalten), dass der Funktionsname einer Getter-Methode immer mit »get« beginnt, woran der Name des Attributs mit einem großen Anfangsbuchstaben angehängt wird. Der Name des Attributs wiederum ist identisch mit dem Hash-Key des entsprechenden Hash-Elements in $self. Lassen Sie uns doch gleich für beide Attribute unseres User-Objekts die Getter-Methoden implementieren: # Getter für das Attribut "login": sub getLogin { my $self = shift( @_ ); return $self->{ "login" }; } sub getPwd { my $self = shift( @_ ); return $self->{ "pwd" }; }

Dem aufmerksamen Leser wird mit Sicherheit die Zeile my $self = shift( @_ );

ins Auge springen. Was soll diese Zeile? Die Methode soll doch nur den Wert des Attributs zurückliefern und erwartet somit gar keinen Übergabeparameter. Das ist eine der seltsamen Eigenarten, denen man manchmal bei Perl begegnet. Weiter oben hatten wir bereits einen ähnlichen Fall, als wir den Konstruktor einer Klasse kennen gelernt haben. Beim Aufruf einer Instanzmethode (diese müssen zwingend immer über die ObjektReferenzvariable aufgerufen werden) fügt der Interpreter »klammheimlich einen zusätzlichen Parameter am Anfang der Argumentliste ein. Wir können uns denken,

292

5

Objektorientierte Programmierung

was da auf mystische Art und Weise hinzugekommen ist, wenn wir den Variablennamen lesen. Es ist die Objektreferenz, die ja, wie wir nun wissen, gleichzeitig auch als Hash-Referenz herhalten muss. Andere Programmiersprachen wie zum Beispiel Java bieten in ihrem Sprachwortschatz dasselbe unter einem anderen Namen an. Dort heißt die Objektreferenz für das aktuelle Objekt this und ist in allen Instanzmethoden automatisch definiert. Da aber in Perl $self eine ganz normale Variable ist, die in der Konstruktorfunktion new() mit dem Scope my definiert wird, wäre sie normalerweise in allen anderen Funk-

tionen nicht gültig, auch dann nicht, wenn diese Funktionen in derselben Datei definiert werden wie der Konstruktor. Also macht Perl hier einen Salto und fügt die dringend benötigte Variable eben als unsichtbaren Parameter in die Argumentliste der Instanzmethoden ein. Somit ist es möglich, dass wir in der Methode getLogin() auf die Hash-Referenz zugreifen können, die eigentlich nur in der Konstruktorfunktion new() definiert ist. Ebenso möglich ist übrigens der Aufruf einer weiteren Instanzmethode über die Objektreferenz, da $self aufgrund ihres Zwitterdaseins gleichzeitig auch eine Objektreferenz ist.

Setter Methoden Diese Art von Methoden dient dem schreibenden Zugriff auf Instanzattribute. Der Begriff »Setter« kommt vom englischen Verb »to set«, was im Deutschen »setzen« oder auch »mit einem Wert belegen« bedeutet. Auch hier gilt als Konvention für die Namen der Methoden: Er beginnt mit set gefolgt vom Namen des Attributs, das man ändern möchte (wobei dessen erster Buchstabe großgeschrieben wird). Dieser ist identisch mit dem Hash-Key des entsprechenden Elements von $self. Nun wieder an die Arbeit: Wir implementieren die Setter-Methoden für unsere beiden Attribute: sub setLogin { my $self = shift( @_ ); my ( $arg ) = @_; unless ( $arg ) { return undef; } $self->{ "login" } = $arg; return 1; }

Klassen

293

sub setPwd { my $self = shift( @_ ); my ( $arg ) = @_; unless ( $arg ) { return undef; } $self->{ "pwd" } = $arg; return 1; }

Die Zeile my $self = shift( @_ );

sollte von den Getter-Methoden her noch bekannt sein. Beide Methoden erwarten genau ein Argument, nämlich den Wert für das Attribut, dessen Wert geändert werden soll. Wie wir sehen, prüfen die Methoden, ob der Wert leer ist. In diesem Fall geben sie den Status undef an den Aufrufer zurück, um ihm mitzuteilen, dass dies ein Fehler ist. Ansonsten wird der Value des Hash-Elements in $self neu gesetzt. Die Prüfung von Argumenten in Setter-Methoden ist kein Muss, es hängt von der Programmlogik ab, ob ein leerer oder gar ein undef-Wert erlaubt ist oder nicht. Grundsätzlich aber sollten Setter-Methoden immer einen definierten Status an den Aufrufer zurückliefern. Wie bei allen Funktionen gilt: Ein TRUE-Status bedeutet OK, der FALSEStatus (meist) einen Fehler, der undef-Status immer einen Fehler.

Getter- und Setter Methoden für boolesche Attribute Werden Getter-Methoden für Attribute implementiert, die einen booleschen Wert darstellen, dann ist die Namenskonvention für die Funktionsnamen etwas anders: Getter-Methoden für boolesche Attribute beginnen entweder mit is oder mit has, gefolgt vom Namen der logischen Aktion, die durch das Attribut beeinflusst wird. Der Name der Aktion beginnt auch hier mit einem Großbuchstaben. Die Namenskonvention bei Setter-Methoden hat dieselben Regeln, allerdings werden häufg gar keine Setter-Methoden für boolesche Attribute verwendet. Damit Sie sich darunter auch etwas vorstellen können, hier ein Beispiel: Angenommen, die Klasse User hätte ein boolesches Attribut namens enabled. Wie der Name schon vermuten lässt, handelt es sich dabei um ein Flag, das angibt, ob der User freigeschaltet (enabled=TRUE) oder gesperrt (enabled=FALSE) ist.

294

5

Objektorientierte Programmierung

Jetzt kann man natürlich wie üblich eine Getter- und eine Setter-Methode implementieren. Deren Namen wären getEnabled() und setEnabled(). In aller Regel macht man hier aber eine Ausnahme und nennt die Getter-Methode isEnabled(). Die Methode zum Freischalten eines Users ist keine richtige Setter-Methode mehr, sondern eine normale Aktionsmethode (die aber nur das Attribut enabled ändert) und heißt enable(). Dementsprechend gibt es meist auch Methoden für die umgekehrte Aktion: isDisabled() gibt TRUE zurück, wenn der User gesperrt ist, und disable() sperrt den User. Der Vollständigkeit halber noch eine Erklärung für den Begriff »Aktionsmethode«: Während Getter- und Setter-Methoden meist nur den Wert des Attributs im Hauptspeicher ändern und eine eigene spezielle Namenskonvention haben, führen Aktionsmethoden in der Regel eine »echte« Verarbeitung der Daten z.B. in der Datenbank durch. Der Name von Aktionsmethoden beginnt auch nicht mit get oder set. Meist verwendet man für den Status von Objekten jedoch kein boolesches Attribut, sondern nimmt dafür eine Zahl oder einen String. Wir werden das im folgenden Beispiel kurz demonstrieren. Hier ein Beispiel für boolesche Attribute: ... # Konstruktor sub new { ... my $self = { ... "status" => "e", ... }; ... } ... # Getter-Methode für den Status eines Users # Sie liefert TRUE, wenn der User freigeschaltet ist sub isEnabled { my $self = shift( @_ ); return ( $self->{ "status" } eq "e" ) ? 1 : 0; } # Dasselbe in entgegengesetzter Richtung: # Sie liefert TRUE, wenn der User gesperrt ist sub isDisabled { my $self = shift( @_ ); return ! $self->isEnabled(); }

Klassen

295

Wie wir im Beispielcode sehen, heißt das Attribut (und damit der Hash-Key) für den Status des Users nicht enabled, sondern status. Dies hat den Vorteil, dass man nun mehr als nur zwei Zustände darin abspeichern kann. Ich habe für den Zustand »freigeschaltet« den Wert e gewählt. Dementsprechend wäre ein d gleichbedeutend mit »gesperrt«, was im Englischen disabled heißt. Die Methode isEnabled() gibt nur dann TRUE zurück, wenn das Attribut status den Wert e hat, ansonsten liefert sie FALSE zurück. Eine Besonderheit findet sich in der umgekehrten Methode isDisabled(). Normalerweise würde man auch dort direkt den Wert des Hash-Elements abfragen. Dies hätte aber den Nachteil, dass man bei einer Änderung des Designs den Programmcode an zwei verschiedenen Stellen umschreiben müsste. Wesentlich eleganter ist die hier vorgestellte Variante. Die Methode isDisabled() ruft ihrerseits wieder die Methode isEnabled() auf und invertiert mit dem Operator »!« deren Rückgabewert. So viel zu Setter- und Getter-Methoden. Alle weiteren Instanzmethoden sind normale Aktionsmethoden, die mit den Objektdaten der Instanz irgendetwas tun, zum Beispiel den Datensatz aus einer Datenbank oder einer Datei lesen bzw. dort neu anlegen oder ändern. Es gibt noch eine weitere Art von Methoden, die sowohl statisch der Klasse (die Methoden greifen nur auf Klassenvariablen zu) als auch einer Instanz (die Methdoen greifen sowohl auf Klassenvariablen als auch auf Instanzattribute zu) zugeordnet sein können. Dies sind die privaten Methoden (obwohl diese Methoden natürlich auch wieder in Getter-, Setter- und Aktionsmethoden aufgeteilt werden können). Private Methoden zeichnen sich dadurch aus, dass sie nur von Funktionen desselben Moduls aufgerufen werden können, nicht aber von anderem Programmcode, der das Modul lädt. Wie wir sehen werden, ist das in Perl gar nicht so einfach, denn grundsätzlich sind Funktionen nach außen bekannt und können durch die Angabe des absoluten Namespaces aufgerufen werden. Eine Methode der »Privatisierung« von Methoden haben wir bei der _init()-Methode gesehen, die ich im Abschnitt »Konstruktor« beschrieben habe. Dort wird vor den eigentlichen Funktionsnamen ein Unterstrich gestellt, um sie als private Methode zu deklarieren. Aber trotzdem könnte man die Funktion von außen aufrufen, denn der Unterstrich ist nur ein Hilfsmittel für die Kennzeichnung, verhindert aber nichts. Mit Hilfe von Referenzvariablen, die auf einen anonymen Codeblock zeigen, lässt sich aber gänzlich verhindern, dass eine Funktion von außen sichtbar und damit aufrufbar ist. Greifen wir noch einmal unser Beispiel auf und schreiben die Methode _init() so um, dass sie wirklich privat und damit unsichtbar für andere Module wird: # Beispiel für eine wirklich private Klassenfunktion my $_init = sub { my ( $login, $pwd ) = @_;

296

5

Objektorientierte Programmierung

unless ( $login and $pwd and ( length( $login ) getLogin() ) ? $self->getLogin() : "undef" ) . "'\n\tpwd = '" . ( defined( $self->getPwd() ) ? $self->getPwd() : "undef" ) . "'\n"; return $res; }

Unsere toString()-Methode gibt einen String zurück, der alle Instanzattribute mit ihrem Namen und ihrem Wert enthält. Die Attribute werden hier im Beispiel durch ein Zeilenende-Zeichen voneinander getrennt und mit einem TAB-Zeichen eingerückt. Der aktuelle Wert eines Attributs wird jeweils auf undef überprüft. In diesem Fall wird statt des Werts der String undef eingesetzt. Das ist notwendig, weil der Interpreter andernfalls eine Fehlermeldung liefern würde, wenn man versucht, den String auszugeben. Bei komplexeren Objekten, wo Attribute wiederum Objekte oder Hashes oder Arrays sein können, muss die toString()-Methode natürlich entsprechend angepasst werden. Nehmen wir wieder unser bisheriges Beispiel der Klasse User und sehen uns an, was die Methode toString() zurückliefert: # Hauptprogramm #!D:/Perl/bin/perl.exe -w use strict; use User; my $user = new User( "Egon", "Wahr" ); unless ( $user ) { print( STDERR "Fehler beim Anlegen des Objekts\n" ); exit( 1 ); } print( $user->toString(), "\n" ); exit( 0 );

Wenn wir das Skript ausführen, erhalten wir folgende Ausgabe: login = 'Egon' pwd = 'Wahr'

298

5

Objektorientierte Programmierung

5.1.4 Fehlermeldungen von Klassen Bisher haben unsere Klassenmethoden einfach nur den Pseudowert undef zurückgeliefert, wenn Fehler aufgetreten sind. Das ist speziell in der Entwicklungsphase von Modulen nicht besonders hilfreich, da man ja auch wissen möchte, was passiert ist. Grundsätzlich verbietet sich die direkte Ausgabe von Fehlermeldungen nach STDOUT oder STDERR in Modulen, da man nicht wissen kann, in welcher Umgebung das Modul benutzt wird. Speziell im CGI-Umfeld können Fehlermeldungen nicht einfach ausgegeben werden, weil der Anwender im Browser dann meist einen »Server Error 500« erhalten würde (der mit Sicherheit unbeliebteste Fehler bei Programmierern). Dasselbe Problem hat man bei Debug-Informationen, die man im Modul zusätzlich zu den Fehlermeldungen zur Verfügung stellen möchte. Irgendwie muss diese Information zum Programmcode gelangen, der das Package benutzt. Die einfachste Lösung ist eine statische Klassenvariable, die die Fehlermeldungen aufnimmt. Dazu implementiert man noch eine ebenfalls statische Methode, mit deren Hilfe man die Fehlermeldungen von außen lesen kann. Wird eine Klasse in Multi-Threading-Umgebung benutzt, heißt es aufpassen, da hier eine Klasse normalerweise nur ein einziges Mal geladen wird. Dies ist z.B. in Java die Regel. Bis dato wird Perl noch nicht »multi-threaded« benutzt, deswegen ergeben sich hier meist noch keine Probleme bezüglich der Synchronisation von Daten im Hauptspeicher. In einer Multi-Threading-Umgebung hat man grundsätzlich das Problem, dass die gemeinsamen Daten einer Klasse synchronisiert werden müssen, d.h., es ist darauf zu achten, dass die unterschiedlichen Threads die Klassendaten nicht gegenseitig überschreiben. Ein Ausweg wäre, die Fehlerbehandlung nicht statisch in der Klasse zu verarbeiten, sondern separat für jede Objektinstanz. Auf Deutsch heißt das nichts anderes, als dass man für die Verarbeitung von Fehlern Instanzattribute und Instanzmethoden benutzt. Das geht aber nur, wenn vom Konstruktor eine gültige Objektreferenz zurückgeliefert wird. Sehen wir uns eine Beispiel-Implementierung für Fehlermeldungen an: package User; use strict; # Alle Fehlermeldungen der Klasse landen in einer # Array-Variable, auf die man von außen nicht direkt, # sondern nur über eine Getter-Methode Zugriff hat. my @errors = ();

Klassen

299

# Getter-Methode zum Auslesen der Fehlermeldungen # Sie gibt alle Fehlermeldungen als Elemente einer # Liste zurück. sub getErrors { return @errors; } # Methode zum Löschen der Fehlermeldungen sub clearErrors { @errors = (); } # Abfragemethode, mit der man von außen feststellen # kann, ob ein Fehler aufgetreten ist # Sie liefert dann TRUE zurück sub hasErrors { return @errors ? 1 : 0; }

Natürlich benötigen wir nun auch noch eine private Klassenmethode, mit deren Hilfe andere Funktionen unserer Klasse Fehlermeldungen erzeugen können: sub _err { my $str = ""; foreach my $arg ( @_ ) { $str .= defined( $arg ) ? $arg : "undef"; } push( @errors, $str ); }

Die Methode ist so geschrieben, dass mehrere Argumente im Funktionsaufruf angegeben werden können. Um Fehlermeldungen vom Interpreter vorzubeugen, falls eines der Argumente undef ist, werden in einer Schleife alle Parameter auf diesen Wert hin überprüft. Es wird dann stattdessen der String undef angehängt. Nun wollen wir uns ansehen, wie die anderen Funktionen des Moduls geändert werden müssen, um Fehlermeldungen zu erzeugen. Als Beispiel nehmen wir einmal die Funktion _init(): sub _init { my ( $login, $pwd ) = @_; # Präfix, das vor die eigentliche Fehlermeldung # gesetzt wird, damit man weiß, welche Funktion # die Meldung abgesetzt hatte. my $prefix = "_init():";

300

5

Objektorientierte Programmierung

# Fehlermeldung, falls das Argument für den # Benutzernamen undef ist. unless ( defined( $login ) ) { _err( "$prefix undefined login" ); return undef; } # Fehlermeldung, falls das Argument für den # Benutzernamen FALSE ist. unless ( $login ) { _err( "$prefix empty login" ); return undef; } # Fehlermeldung, falls das Argument für das # Kennwort undef ist. unless ( defined( $pwd ) ) { _err( "$prefix undefined pwd" ); return undef; } # Fehlermeldung, falls das Argument für das # Kennwort FALSE ist. unless ( $pwd ) { _err( "$prefix empty pwd" ); return undef; } return 1; }

Wie wir sehen, wird der Programmcode um so länger, je detaillierter die Meldungen sind. Aber das ist leider ein Naturgesetz, dem man machtlos gegenübersteht, also Kopf hoch und durch! Übrigens: Wenn eine Funktion des Moduls wiederum eine andere Funktion aufruft usw., dann erhalten wir im Fehlerfall eine geschachtelte Fehlermeldungsliste, die man landläufig auch als »Error Stack« bezeichnet. Nehmen wir als Beispiel den Konstruktor, der seinerseits die Funktion _init() aufruft. Wenn dort ein Fehler auftritt, dann erzeugt sie die erste Fehlermeldung. Der Konstruktor prüft den Rückgabewert von _init() und schreibt im Fehlerfall ebenfalls eine Meldung in das Array @errors. Das Ganze ergibt einen so genannten »Stack Trace«. Man kann also genau verfolgen, wo welcher Fehler aufgetreten ist, und wer wen wann aufgerufen hat.

Vererbung

301

Jetzt fehlt noch der entsprechende Code im Skript, der die Fehlermeldung entgegennimmt: ... my $user = new User( "Willy", "secret" ); if ( ( ! $user ) or User::hasErrors() ) { # es ist ein Fehler aufgetreten, es werden alle # Fehlermeldungen nach STDOUT ausgegeben my @errors = User::getErrors(); print( join( "\n ", @errors ), "\n" ); exit( 1 ); }

Nun wollen wir uns einem Thema widmen, das wohl eines der wichtigsten bei der objektorientierten Programmierung darstellt: der Vererbung.

5.2 Vererbung Die objektorientierte Programmierung stellt mit der Möglichkeit, Attribute und Methoden von Objekten weiter zu vererben, eines der leistungsfähigsten Programmiermittel zur Verfügung. Im Englischen wird dafür der Begriff »Inheritance« verwendet. Das Vererbungsprinzip ist das Folgende: Zunächst entwickelt man eine möglichst allgemeine Klasse mit wenigen Attributen und Methoden. Dann implementiert man weitere Klassen, welche diese allgemeine Klasse erweitern, indem sie neue Attribute und Methoden hinzufügen. Alle bereits von der allgemeinen Klasse entwickelten Attribute und Methoden werden den »Kind«-Klassen, die aus der allgemeinen Klasse hervorgehen, weitervererbt, d.h., sie können diese benutzen, als seien sie neu implementiert worden. Man muss also den bereits geschriebenen Programmcode nicht noch einmal schreiben. Der Begriff »Kind« heißt im Englischen übrigens »child«, die Mehrzahl ist »children«. Dieses Spiel kann man nun beliebig weiterspielen, mit jeder weiteren »Ableitung«, sprich Vererbung, entsteht eine Klasse, die spezifischer ist als die »Eltern«-Klasse, aus welcher sie hervorgeht. Kurz zum Begriff »Eltern«: Dazu sagt man im Englischen »parent«. Ein Beispiel für eine sehr allgemeine Klasse haben wir bereits in Form unseres Moduls User.pm kennen gelernt. Ausgehend von dieser Basisklasse wollen wir nun die Funktionalität erweitern, indem wir eine davon abgeleitete Klasse implementieren, die zusätzlich zu den bereits bekannten Attributen login und pwd noch das Attribut email anbietet. Ich möchte die Klasse AnonymousUser nennen, weil sich hinter dem Benutzer

302

5

Objektorientierte Programmierung

keine Person verbirgt, sondern irgendjemand, der nur durch seine E-Mail-Adresse bekannt und damit sozusagen »anonym« ist. Den Programmcode speichern wir in der Datei AnonymousUser.pm ab. Die erste Frage, die der aufmerksame Leser auf der Zunge haben wird, ist: »Wie leitet man eine Klasse ab?« Die Antwort auf diese Frage ist gar nicht so schwer, wie man meinen könnte. Das einzige, was man tun muss, ist die Definition einer neuen Variable und eine geringfügige Änderung im Programmcode des Konstruktors.

5.2.1 Die Variable @ISA Damit eine Methode aus der übergeordneten Klasse (zu deutsch »Eltern-Klasse«, englisch »parent class« oder auch »super class«) benutzt werden kann, muss im Modul der daraus abgeleiteten Klasse (zu deutsch »Kind-Klasse«, englisch »child class« oder auch »derived class«) die Variable @ISA definiert und darin alle Packagenamen der übergeordneten Klassen als Elemente des Arrays angegeben sein. Damit Sie ob der neuen Begriffe nicht völlig im Regen stehen, wollen wir uns das Ganze in einem Beispiel ansehen. Den gezeigten Programmcode speichern wir, wie gesagt, in der Datei mit dem Namen AnonymousUser.pm ab, das Package heißt also AnonymousUser. # Modul AnonymousUser.pm package AnonymousUser; use strict; # Laden des Moduls für die Eltern-Klasse use User; # Übernehmen der Methoden aus der Eltern-Klasse in die # Kind-Klasse mit Hilfe der Variable @ISA our @ISA = qw( User ); ...

Was bewirkt die Variable »@ISA«? Die Variable @ISA veranlasst den Interpreter, Funktionen nicht nur in dem Package zu suchen, wo der Aufruf über die Objektreferenz steht, sondern auch in den Packages, die als Liste in dem Array @ISA stehen. Erst mit dieser Variable wird Vererbung überhaupt möglich. Lassen Sie mich die Zusammenhänge der Vererbung mit @ISA anhand eines Schaubildes erläutern:

Vererbung

303



  

   

 



 ! $##%## $

 



 !  "##" #  !  

   

 



&%'    

  

 &%'

  



Abbildung 5.3: Vererbung mit @ISA

Im Hauptprogramm (Package main) wird eine Instanz der Klasse AnonymousUser erzeugt. Das erfolgt mit der Anweisung: my $u = new AnonymousUser();

Kleiner Hinweis: Der Einfachheit halber habe ich die Parameter weggelassen. Wir werden weiter unten noch sehen, was im Konstruktor von AnonymousUser zu tun ist. Mit dem nächsten Statement $u->setEmail( '[email protected]' );

setzt man das Attribut email. Dieses sowie die notwendige Instanzmethode setEmail() werden im Package AnonymousUser definiert. Nun wird im Hauptprogramm mit $u->setLogin( "sepp" );

das Attribut login gesetzt. Schauen Sie noch einmal genau auf den Anfang der Zeile: Es wird die Funktion setLogin() über die Objektreferenz $u aufgerufen. $u ist aber eine Referenz auf ein Objekt der Klasse AnonymousUser. In dieser Klasse aber gibt es gar keine Funktion setLogin(), vielmehr ist die Methode in der Klasse User implementiert. Genau das versteht man unter Vererbung: Dank der Variable @ISA scheint es so, als seien alle Attribute und Methoden von User auch in AnonymousUser vorhanden, obwohl wir in diesem Modul keine einzige Codezeile dafür schreiben müssen. Aus der Sicht des Hauptprogramms existiert gar keine Klasse User. Dort laden wir nur die Klasse AnonymousUser: use AnonymousUser;

Hinweis: @ISA sollte grundsätzlich nur den Namen der Eltern-Klasse enthalten, auch wenn man grundsätzlich beliebig viele Klassen in das Array packen kann. Der Interpreter geht bei der Suche nach Funktionen die Liste von links nach rechts vor. Dieser

304

5

Objektorientierte Programmierung

Mechanismus wird auch »mehrfache Vererbung« genannt und bereitet seit seiner Einführung in C++ allen Programmierern heftige Kopfschmerzen. Also: Lassen Sie die Finger von der »mehrfachen Vererbung«! Jetzt zum zweiten Teil der Antwort auf die Frage »Wie leitet man eine Klasse ab?«: Man schreibt in den Konstruktor der Kind-Klasse einen Aufruf des Konstruktors der Eltern-Klasse und erweitert anschließend das Hash, in dem die Attribute gespeichert sind. In unserem Beispiel ist AnonymousUser die Kind-Klasse und User die Eltern-Klasse. Sehen wir uns nun den Konstruktor der Kind-Klasse an: package AnonymousUser; use strict; # Mit der folgenden Direktive wird die Eltern-Klasse # geladen, von der wir die Attribute und Methoden # vererbt bekommen. use User; # Vererbung heißt @ISA our @ISA = qw( User ); # Konstruktor sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; # Nun müssen wir als Erstes den Konstruktor # unserer Eltern-Klasse aufrufen, dem wir # unsere eigenen Aufrufparameter mitgeben. # $self wird hier nicht als Hash-Referenz # definiert, sondern erhält den Rückgabewert # des Konstruktors der übergeordneten Klasse. my $self = new User( @_ ); # Wir müssen noch überprüfen, ob etwas # schief gegangen ist (der Einfachheit halber # ist hier die Behandlung von Fehlermeldungen # nicht implementiert; wir liefern einfach nur # "undef" zurück, falls ein Fehler aufgetreten ist). unless ( $self ) { return undef; } # $self ist momentan noch eine Objektreferenz # auf eine Instanz der Eltern-Klasse. # Mit der folgenden Anweisung ändern wir die # Referenz, so dass $self zu einer Referenz # unserer eigenen Instanz wird. bless( $self, $class );

Vererbung

305

# Jetzt fügen wir das neue Attribut "email" # hinzu. Den Wert dieses Attributs erhalten wir # aus dem dritten Parameter von @_ # Die ersten beiden sind "login" und "pwd". # Auch hier habe ich der Einfachheit halber # die Fehlerbehandlung weggelassen. $self->setEmail( $_[ 2 ] ); # Wir sind fertig, also geben wir die # Objektreferenz zurück. return $self; }

Aus dem Hauptprogramm heraus wird jetzt nur der Konstruktor der abgeleiteten Klasse aufgerufen: # Hauptprogramm ... use AnonymousUser; my $user = new AnonymousUser( "sepp", "secret", '[email protected]' );

Wie wir sehen, taucht das Modul User.pm im Hauptprogramm überhaupt nicht auf. Vielmehr wird es vom Package AnonymousUser geladen. Da in der E-Mail-Adresse das Zeichen »@« vorkommt, muss man den String entweder in einfache Quotes setzen oder die Sonderbedeutung von »@« durch einen vorangestellten Backslash entwerten. Ich habe mich für die erste Variante entschieden. Mit der Instanz, die uns der Konstruktor von AnonymousUser zurückliefert, können wir nun sowohl auf Methoden von AnonymousUser als auch von User zugreifen: # Wir rufen die Methode "getEmail()" auf, die in # "AnonymousUser" implementiert ist. my $email = $user->getEmail(); # Jetzt rufen wir "getLogin()" auf. Diese Methode # ist in "User" implementiert. # Dem Hauptprogramm kann es aber egal sein, # wo welche Methode definiert wurde, alle # Funktionsaufrufe erfolgen über unsere # Objektreferenz "$user". my $login = $user->getLogin();

306

5

Objektorientierte Programmierung

Der Vollständigkeit halber implementieren wir nun noch die Getter- und Setter-Methoden für das neue Attribut email im Modul AnonymousUser.pm: # Getter sub getEmail { my $self = shift( @_ ); return $self->{ "email" }; } # Setter sub setEmail { my $self = shift( @_ ); my $arg = shift( @_ ); # Hier könnte noch eine Überprüfung der email # stehen $self->{ "email" } = $arg; return 1; }

Damit haben wir die Klasse User abgeleitet und die Kind-Klasse AnonymousUser implementiert, die alle Attribute und Methoden von User erbt. Ein wichtiger Punkt ist aber noch offen: Die Methode toString(). Bisher ist die Funktion nur in »User« implementiert, nicht aber in AnonymousUser. Für den Aufruf der Methode vom Hauptprogramm aus macht das nichts aus, allerdings ist das neu hinzugekommene Attribut email im Package User unbekannt und wird deshalb nicht berücksichtigt. Die Folge davon ist, dass der Programmcode ... my $user = new AnonymousUser( "Sepp", "secret", '[email protected]' ); print( $user->toString() );

nicht alle Attribute ausgibt: Attribute von User: login = 'Sepp' pwd = 'secret'

Es wird ja die Methode toString() von User aufgerufen, und dort ist das Attribut email unbekannt.

Vererbung

307

Als Lösung für dieses Problem kommt ein wichtiger Aspekt der objektorientierten Programmierung ins Spiel, den man »Overloading« nennt, was im Deutschen so viel wie »Überladen« bedeutet.

5.2.2 Overloading Unter dem Begriff »Overloading« versteht man das Aufrufen einer namensgleichen Methode der übergeordneten Eltern-Klasse aus der Kind-Klasse heraus. Um Ihnen gleich ein Anschauungsbeispiel zu liefern: Im letzten Abschnitt hatten wir das Problem, dass für die toString()-Methode von User das Attribut email unbekannt ist, denn es wurde ja erst von der abgeleiteten KindKlasse AnonymousUser hinzugefügt. Um jetzt wirklich alle Attribute in einen String umzuwandeln, muss also auch in der Kind-Klasse eine Methode toString() implementiert werden, die alle Attribute der Klasse AnonymousUser umwandelt. Zusätzlich aber muss diese Funktion die toString()-Methode der Eltern-Klasse aufrufen, damit auch deren Attribute erfasst werden. Die Frage ist nur, wie? Perl bietet hierfür eine Reihe von Möglichkeiten an, von denen ich aber nur die sauberste präsentieren möchte. Das Schlüsselwort heißt SUPER. Mit diesem Schlüsselwort, hinter dem sich eine Pseudoklasse verbirgt, wird der Interpreter dazu veranlasst, in denjenigen Klassen nach der Funktion zu suchen, die in der Liste von @ISA stehen. Zur Demonstration möchte ich die Methode toString() der Kind-Klasse AnonymousUser gleich implementieren: package AnonymousUser; ... sub toString { my $self = shift( @_ ); my $res = $self->SUPER::toString(); $res .= "\nAttribute von AnonymousUser:" . "\n\temail = '" . ( defined( $self->getEmail() ) ? $self->getEmail() : "undef" ) . "'\n"; return $res; }

Die wichtige Zeile ist my $res = $self->SUPER::toString();

denn hier wird die Methode der Eltern-Klasse aufgerufen.

308

5

Objektorientierte Programmierung

Man hätte natürlich auch den Klassennamen statt SUPER hinschreiben können, aber in diesem Fall wäre der Aufruf im Programmcode»hart verdrahtet«. SUPER ist die elegantere Alternative, weil der Code bei einer Änderung von Klassennamen gleich bleiben kann. Das Statement $self->SUPER::toString();

besagt einfach: Lieber Interpreter, rufe bitte die Methode toString() von meiner ElternKlasse auf, egal, wie diese heißt. Dieser Mechanismus funktioniert bei mehr als nur einer abgeleiteten Kind-Klasse. Wenn wir zum Beispiel eine neue Klasse PersUser implementieren, die wiederum von AnonymousUser abgeleitet ist, und der neuen Klasse keine toString()-Methode spendieren, dann ruft der Interpreter die Funktion aus der Klasse AnonymousUser auf. Bevor wir zum nächsten neuen Begriff kommen, möchte ich Sie noch auf eine weitere Eigenart von Perl aufmerksam machen. Weiter oben, als wir die Vererbung besprochen hatten, erzählte ich Ihnen, dass die Eltern-Klasse keine Attribute der Kind-Klasse kennt. Das ist auch richtig, zumindest, was den Namen des Attributs angeht. Jedoch werden alle Attribute, auch die der abgeleiteten Klassen, in einem gemeinsamen Hash gespeichert, und dieses kennt die Eltern-Klasse sehr wohl. Am besten, ich zeige Ihnen die Folgen anhand eines Beispiels. Lassen Sie uns die toString()-Methode der Klasse User ein wenig verändern: package User; # Neue Implementierung der toString()-Methode von "User" sub toString { my $self = shift( @_ ); my $res = "Attribute von User:\n"; foreach my $key ( sort( keys( %{ $self } ) ) ) { my $val = defined( $self->{ $key } ) ? $self->{ $key } : "undef"; $res .= "\t$key = '$val'\n"; } return $res; }

Die neue Variante von toString() greift jetzt nicht mehr über die Namen der Attribute auf die Hash-Elemente zu, sondern anonym, indem sie sich eine Liste der Hash-Keys holt. Die Funktion gibt jetzt also einfach alle im Hash gespeicherten Elemente aus.

Vererbung

309

Wenn wir jetzt nach dieser Änderung die toString()-Methode von AnonymousUser aufrufen: # Hauptprogramm use AnonymousUser; my $user = new AnonymousUser( "Sepp", "secret", '[email protected]' ); ... print( $user->toString() );

dann erhalten wir folgende Ausgabe: Attribute von email login pwd =

User: = '[email protected]' = 'Sepp' 'secret'

Attribute von AnonymousUser: email = '[email protected]'

Ich habe nur die Funktion der Klasse User geändert, nicht die aus der Klasse AnonymousUser. Wie wir deutlich sehen, sind alle Attribute, sowohl die der Eltern- als auch die der Kind-Klasse, in einem einzigen Hash abgelegt. Das bedeutet im Klartext, dass Sie in keinem Fall neue Attribute in Kind-Klassen hinzufügen dürfen, die denselben Namen haben wie bereits in Eltern-Klassen existierende Attribute. Nachdem wir den Begriff »Overloading« alle perfekt buchstabieren können, will ich Ihnen gleich noch einen weiteren vorsetzen: »Overriding«.

5.2.3 Overriding Nachdem Sie wissen, was »Overloading« bedeutet, ist der neue Begriff »Overriding« ein Kinderspiel. Er bedeutet nämlich im Prinzip dasselbe, nur fehlt der Aufruf der gleichnamigen Funktion aus der übergeordneten Eltern-Klasse: package AnonymousUser; ... sub toString { my $self = shift( @_ );

310

5

Objektorientierte Programmierung

my $res = "Attribute von AnonymousUser:" . "\n\temail = '" . ( defined( $self->getEmail() ) ? $self->getEmail() : "undef" ) . "'\n"; return $res; }

Der Unterschied zwischen dieser Variante von toString() und der vorher gezeigten besteht wirklich nur darin, dass jetzt der Funktionsaufruf der Eltern Klasse fehlt. Bevor wir »Overriding« in Aktion sehen, möchte ich Ihnen ein paar Aktionsmethoden zeigen, mit denen wir die Datensätze der Benutzer aus einer Datei lesen oder in eine Datei schreiben können, denn ohne solche Aktionen wären unsere Klassen ein wenig langweilig. Im Zuge der Neuimplementierungen werden wir den bisher präsentierten Demonstrationscode so ändern, dass er für die Praxis tauglich wird. Beginnen wir bei der Klasse User. Bisher haben wir alle benötigten Attribute als Parameterliste im Konstruktor angegeben: my $user = new User( "Sepp", "secret" );

Dieses Programmdesign ist in der Praxis meist untauglich, weil damit die Positionen der Argumente festgelegt sind und später nicht geändert werden können, ohne dass sich damit auch das API des Moduls ändert. Jeder, der Ihr Modul benutzt, muss seinen Programmcode dann also ebenfalls umschreiben. Es gibt zwei Alternativen, die flexibler sind: Man erlaubt einen Konstruktor ohne Argumente und setzt die Attribute über Setter-Methoden (ein leerer Konstruktor wird auch »Default-Konstruktor« bzw. »Standard-Konstruktor« genannt): my $user = new User(); $user->setLogin( "Sepp" ); $user->setPwd( "secret" );

Der Konstruktor wird dann sehr kurz: sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { "login" "pwd" };

=> undef, => undef,

Vererbung

311

# Umwandeln der Hash-Referenzvariable in eine # Objektreferenz bless( $self, $class ); return $self; }

Natürlich muss man in diesem Fall in den Setter-Methoden die Parameter überprüfen, damit von außen kein Müll in das Objekt fließen kann. Der große Vorteil dieser Variante ist, dass man keine Abhängigkeiten in Parametern hat, aus dem einfachen Grund, weil es keine Parameter im Konstruktor gibt. Der leere Konstruktor hat den weiteren Vorteil, dass er in jedem Fall eine gültige Objektreferenz zurückliefert. Damit kann man zum Beispiel Fehlermeldungen in der Instanz selbst statt in einer statischen Klassenvariablen speichern (dies kann speziell bei Multi-Threading-Umgebungen wichtig werden). Allerdings hat diese Implementierung auch einen Nachteil, wenn man zum Beispiel Pflichtattribute in der Klasse vorsieht, die in jedem Fall mit einem gültigen Wert vorhanden sein müssen, bevor eine bestimmte Aktion ausgeführt werden darf. So ergibt es keinen Sinn, nach einem Benutzer zu suchen, wenn dessen Login undef ist. Das ist aber durch den Default Konstruktor (auch »Standard-Konstruktor« genannt) ohne Parameter möglich. Also muss die Methode, mit der nach Benutzern gesucht werden soll, prüfen, ob alle Pflichtattribute gültig sind. Die zweite Alternative bietet sich in Form eines Hashs an, das dem Konstruktor als Referenz übergeben wird: my %args = ( "login" => "Sepp", "pwd" => "secret", ); my $user = new User( \%args );

Damit hat man eine große Freizügigkeit, wenn im Laufe der Weiterentwicklung des Moduls neue Attribute hinzukommen, denn die Positionen der Elemente sind bei einem Hash ja irrelevant. Mit dieser Variante sind Pflichtattribute kein Problem. Allerdings müssen die Namen (Keys) der Hash-Elemente im API des Moduls dokumentiert werden. So muss in unserem Fall der Benutzername durch den Hash-Key login angegeben sein. Wir wollen hier für unsere Beispiele die Variante eines Konstruktors ohne Argumente verwenden.

312

5

Objektorientierte Programmierung

Implementieren wir zunächst die Methode write(), mit der wir den Datensatz der Instanz in eine Datei schreiben. Bevor mit dem Kodieren begonnen werden kann, müssen wir uns die Struktur der Datei für die Datensätze überlegen. Wie wäre es mit folgendem Design? Jeder Datensatz steht in einer einzelnen Zeile der Datei, die Attribute eines Datensatzes werden durch ein TAB-Zeichen getrennt. Die Reihenfolge der Attribute lautet: login, pwd, status.

Ja, Sie sehen richtig, in unserem Code wird auch das Attribut status unterstützt, über das wir weiter oben bereits gesprochen hatten. Die Datei könnte also z.B. so aussehen: Sepp\tsecret\te egon\tmypwd\td

Die Datei hat zwei Datensätze, der erste Benutzer ist freigeschaltet (status=e), der zweite gesperrt (status=d) Nun können wir die Methode write() kodieren: 01 sub write { 02 my $self = shift( @_ ); 03 04 my $login = $self->getLogin(); 05 my $pwd = $self->getPwd(); 06 my $st = $self->getStatus(); 07 08 unless ( $login and $pwd ) { 09 return undef; 10 } 11 12 use FileHandle; 13 14 my $fh = new FileHandle( $dataPath, "r+" ); 15 unless ( $fh ) { 16 return undef; 17 } 18 19 my @users = (); 20 my $written = undef; 21 22 while ( defined( my $line = ) ) { 23 chomp( $line ); 24 if ( $line =~ /^([^\t]+)\t[^\t]+\t.$/ ) { 25 if ( $1 eq $login ) { 26 push( @users, "$login\t$pwd\t$st" ); 27 $written = 1; 28 }

Vererbung 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 }

313 else { push( @users, $line ); } } } unless ( $written ) { push( @users, "$login\t$pwd\t$st" ); } unless ( seek( $fh, 0, 0 ) ) { $fh->close(); return undef; } unless ( truncate( $fh, 0 ) ) { $fh->close(); return undef; } foreach my $line ( @users ) { print( $fh "$line\n" ); } $fh->close(); return 1;

Nachdem Sie sich den Code eine Weile angesehen haben, könnte es durchaus sein, dass Sie ein paar Fragen haben. Bis zur Zeile 08 sollte alles klar sein. Wir lesen die Attribute, die wir in die Datei schreiben wollen, mit Getter-Methoden aus dem Objekt. Aber schon in Zeile 08 08

unless ( $login and $pwd ) {

könnten Sie sich vielleicht wundern, warum ich zwar die Attribute login und pwd überprüfe, nicht aber den Status, der im Attribut status gespeichert wird. Die Werte für den Benutzernamen und das Kennwort kommen normalerweise von außen, d.h. von Perl-Skripts oder anderen Modulen, die wir nicht kennen. Da wir weiter oben gesagt haben, wir erlauben im Konstruktor eine leere Parameterliste, könnte jemand diesen Konstruktor verwenden und sofort danach die write()-Methode aufrufen, ohne dass er vorher die Setter-Methoden für die Attribute benutzt. Die Folge wären undef-Werte und vermutlich eine Fehlermeldung des Interpreters. Das können wir natürlich nicht zulassen, deshalb die Überprüfung.

314

5

Objektorientierte Programmierung

Den Status jedoch müssen wir nicht prüfen, denn dieser wird ausschließlich durch unseren eigenen Programmcode in User.pm versorgt. Wir müssen einfach nur sicherstellen, dass er niemals undef sein kann. Das geschieht bereits im Konstruktor: ... my $self = { "login" "pwd" "status" }; ...

=> undef, => undef, => "e",

Wie Sie sehen, wird der Status im Konstruktor immer mit einem Defaultwert belegt und kann somit gar nicht undef sein. Von außen kann er anschließend mit den Methoden disable() und enable() nur noch auf d oder e geändert, niemals aber undef gemacht werden, also erübrigt sich eine Prüfung. Alle Objektattribute, die von »außen« gesetzt werden können, müssen genau daraufhin geprüft werden, ob sie gültig sind. Werden aber Attribute durch Methoden gesetzt, die im Klassenmodul definiert sind, kann diese Prüfung entfallen (wenn man sauber programmiert). Das führt häufig zu Methoden, die zwar dasselbe tun, aber unterschiedliche Prüfungen durchführen. Ein Beispiel dafür ist die Methode zum Lesen eines Datensatzes aus einer Datei oder einer Datenbank. Hier muss kein einziges Attribut überprüft werden, da diese ja bereits vorher beim Anlegen des Datensatzes einem Check unterlagen. Wir werden also häufig Setter-Methoden in zweifacher Ausführung sehen: eine öffentliche Methode (im Englischen auch »public method« genannt), bei der eine Prüfung der Argumente stattfindet, und eine private Methode, die keine Prüfung durchführt. Die Überlegung, die ich Ihnen gerade erläutert habe, führt an sehr vielen Stellen im Programmcode dazu, dass man unnötigen Code weglassen kann, wenn man vorher genaue Überlegungen anstellt. Das ist gerade bei umfangreicheren Programmen wichtig, wenn es auf Hochgeschwindigkeit ankommt. Überlassen Sie bitte nicht alles dem Compiler. Gerade die jüngeren Entwickler bekommen in Schulen und Universitäten oft den falschen Eindruck, dass heutige Compiler Alleskönner sind und einem den Verstand abnehmen. Die nächste Frage dürften Sie vielleicht in Zeile 12 haben: 12

use FileHandle;

Nein, ich habe keinen Fehler gemacht. Das Laden von Modulen kann an beliebiger Stelle im Programmcode erfolgen, auch in Funktionen. Der Vorteil davon ist derselbe, den man bei der Definition und Benutzung von Variablen hat. Deklarieren Sie erst dann, wenn es unbedingt sein muss, nicht alles am Beginn des Programms.

Vererbung

315

Hier der Vorteil: Sollten Sie sich nach einiger Zeit entschließen, den Code auf Datenbankzugriff umzustellen, dann ändern Sie sicherlich die Methode write() (und read(), aber dazu später). Hätten Sie aber die use-Direktive an den Anfang des Moduls gestellt, dann wäre die Gefahr hoch, dass sie nicht gelöscht wird und weiterhin als »Leiche« im Programmcode steht. Auch Zeile 14 ist erwähnenswert: 14

my $fh = new FileHandle( $dataPath, "r+" );

Wo kommt die Variable $dataPath her? Die Frage ist berechtigt. Um die Wahrheit zu sagen: Ich habe $dataPath eben erst als globale Packagevariable erfunden. Das bedeutet natürlich, dass wir in unser Modul User.pm noch die Variablendefinition mit aufnehmen müssen: our $dataPath = "C:/temp/users.data";

Der tatsächliche Pfad für die Datei ist zweitrangig und hängt auch vom verwendeten Betriebssystem ab. Wir werden weiter unten noch sehen, wie man mit Hilfe von Propertiesdateien feste Pfadnamen in Programmen vermeidet, für unser Beispiel jedoch soll dies genügen. Interessant ist auch der Dateimodus. Die Datei wird sowohl zum Lesen als auch zum Schreiben geöffnet. Vorsicht ist allerdings geboten, denn die Datei muss bereits existieren, sonst straft uns der Interpreter mit einer Fehlermeldung (bzw. wir selbst, da wir den Fehler ja in den folgenden Zeilen abfangen). Es reicht übrigens aus, wenn die Datei leer vorhanden ist. Jedoch kann man den Code verbessern, so dass er auch dann funktioniert, wenn die Datei noch nicht existiert: my $fh = undef; unless ( -f $dataPath ) { $fh = new FileHandle( $dataPath, O_RDWR | O_CREAT ); } else { $fh = new FileHandle( $dataPath, "r+" ); }

Natürlich kann man gleich die erste Variante für das Öffnen der Datei verwenden, bei welcher die Datei angelegt wird, falls sie vorher noch nicht existiert: $fh = new FileHandle ($dataPath, O_RDWR | O_CREAT );

316

5

Objektorientierte Programmierung

Ich wollte Ihnen aber diesen kleinen Unterschied im Konstruktor von »FileHandle« nicht verschweigen. Der nächste Programmteil, zu dem ein paar Worte angebracht sind, steht in den Zeilen 19 bis 37: 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

my @users = (); my $written = undef; while ( defined( my $line = ) ) { chomp( $line ); if ( $line =~ /^([^\t]+)\t[^\t]+\t.$/ ) { if ( $1 eq $login ) { push( @users, "$login\t$pwd\t$st" ); $written = 1; } else { push( @users, $line ); } } } unless ( $written ) { push( @users, "$login\t$pwd\t$st" ); }

Es ist schon seltsam, dass man die Datei lesen muss, wenn man doch einen Datensatz hineinschreiben möchte. Einfacher wäre es natürlich, wenn man die Daten ans Ende der Datei anhängen würde. Das verbietet sich aber deshalb, weil die Methode write() nicht nur neue Datensätze in die Datei schreiben, sondern auch bestehende Daten ändern soll. Hinge man also die Zeile mit den Daten ans Ende der Datei, dann würde in diesem Fall der Benutzer zweimal vorkommen. Nun zu der Frage: »Was macht der Code? « Wir lesen den gesamten Datei-Inhalt Zeile für Zeile und stellen die Zeilen in ein Array. Bei jedem Datensatz überprüfen wir, ob der Benutzername mit dem aktuellen Namen unserer Instanz identisch ist. Falls ja, dann kommt nicht der Datensatz aus der Datei ins Array, sondern unser aktueller Daten-Satz der Instanz. Wir benötigen ein Flag, das wir in diesem Fall auf TRUE setzen, denn es kann ja sein, dass es den Benutzer unserer aktuellen Instanz noch gar nicht gibt. In diesem Fall muss der Datensatz am Ende hinzugefügt werden (Zeilen 35 bis 37). Mit dem Pattern Matching 24

if ( $line =~ /^([^\t]+)\t[^\t]+\t.$/ ) {

Vererbung

317

werden nur solche Zeilen der Datei berücksichtigt, die unserem vorgegebenen Format entsprechen. Falls Sie Probleme beim Verständnis des regulären Ausdrucks haben, empfehle ich Ihnen das Kapitel »Pattern Matching«. Der nächste Programmteil, der Schwierigkeiten bereiten könnte, ist: 39 40 41 42 43 44 45 46 47

unless ( seek( $fh, 0, 0 ) ) { $fh->close(); return undef; } unless ( truncate( $fh, 0 ) ) { $fh->close(); return undef; }

Nachdem wir den Datei-Inhalt gelesen und gegebenenfalls einen Datensatz verändert haben, müssen wir den Inhalt der Datei neu schreiben. Dazu muss der Dateizeiger zuerst (mit seek()) auf den Beginn der Datei gesetzt und anschließend die Datei geleert werden (mit truncate()). Der alleinige Aufruf von truncate() reicht nicht aus, da er sich ab der aktuellen Position des Dateizeigers auswirkt, die nach dem Lesen ja am Ende der Datei ist. Um es deutlich zu sagen: In der Praxis wäre diese Implementierung nicht ausreichend. Man müsste eigentlich eine Dateisperre mit Hilfe der Perl-Funktion flock() verwenden. Aber ich will das Kapitel OOP nicht zu sehr strapazieren. Im Anhang finden Sie bei der Beschreibung der Perl-Funktion flock() auch ein Beispiel. Weiter geht es mit einer read()-Methode, denn wir wollen ja schließlich nicht nur neue Benutzer anlegen oder bestehende Datensätze ändern, sondern auch Benutzerdaten einlesen. Bevor ein Benutzer eingelesen werden kann, muss das Attribut login mit dem Benutzernamen gesetzt werden, dieses Attribut ist also ein Pflichtfeld. Sehen wir uns doch gleich den Programmcode an: 01 sub read { 02 my $self = shift( @_ ); 03 04 my $login = $self->getLogin(); 05 unless ( $login ) { 06 return undef; 07 } 08 09 use FileHandle; 10 11 my $fh = new FileHandle( $dataPath, "r" ); 12 unless ( $fh ) {

318 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 }

5

Objektorientierte Programmierung

return undef; } my $found = 0; while ( defined( my $line = $fh->getline() ) ) { chomp( $line ); if ( $line =~ /^([^\t]+)\t([^\t]+)\t(.)$/ ) { if ( $1 eq $login ) { $self->setPwd( $2 ); $self->setStatus( $3 ); last; } } } $fh->close(); return $found;

Bis Zeile 16 sollte alles klar sein. Der Code ist ähnlich wie vorher bei der write()Methode, nur dass wir diesmal die Datei nur zum Lesen öffnen. In Zeile 16 definieren wir ein Flag, das uns auch als Rückgabewert dient und als Merkmal dafür verwendet wird, ob der gesuchte Datensatz gefunden wurde oder nicht. Hier sei angemerkt, dass die Funktion drei verschiedene Rückgabewerte hat: undef bei einem Fehler, FALSE, wenn kein Datensatz gefunden wurde, und TRUE bei einem Treffer. Das Lesen aus der Datei in Zeile 18 ist übrigens mit Absicht etwas unterschiedlich kodiert, damit Sie die Variationen von Perl kennen lernen. Die anschließende Leseschleife ist ähnlich aufgebaut wie bei der write()-Methode. Wenn der Benutzername mit dem Suchbegriff im Attribut login übereinstimmt, werden die restlichen Attribute der Instanz versorgt und die Schleife beendet. Jetzt können wir Datensätze der Klasse User erzeugen, ändern und lesen. Nun wollen wir das Ganze noch für die abgeleitete Klasse AnonymousUser implementieren. Die beiden gerade gezeigten Methoden können wir nicht verwenden, weil sie das in der Klasse AnonymousUser hinzugekommene Attribut email nicht kennen. Also müssen wir in dieser Klasse zwei neue Methoden write() und read() entwickeln. Die gleichnamigen Methoden der abgeleiteten Klasse überschreiben sozusagen die der Eltern-Klasse (Overriding). Wie wir sehen werden, ist der Unterschied nicht so groß.

Vererbung

319

Zunächst überlegen wir uns wieder ein Format für die Datensätze in der Datei. Damit wir abwärtskompatibel sind, übernehmen wir das bisherige Format und hängen das neue Attribut am Ende an: login\tpwd\tstatus\email

Fangen wir mit der Methode write() an: 01 sub write { 02 my $self = shift( @_ ); 03 04 my $login = $self->getLogin(); 05 my $pwd = $self->getPwd(); 06 my $st = $self->getStatus(); 07 my $em = $self->getEmail(); 08 09 unless ( $login and $pwd and $em ) { 10 return undef; 11 } 12 13 use FileHandle; 14 15 my $fh = new FileHandle( $dataPath, "r+" ); 16 unless ( $fh ) { 17 return undef; 18 } 19 20 my @users = (); 21 my $written = undef; 22 23 while ( defined( my $line = $fh->getline() ) ) { 24 chomp( $line ); 25 my $pattern = '^([^\t]+)' . '\t[^\t]+' x 2 . 26 '\t.$'; 27 if ( $line =~ /$pattern/ ) { 28 if ( $1 eq $login ) { 29 push( 30 @users, 31 "$login\t$pwd\t$st\tem" 32 ); 33 34 $written = 1; 35 } 36 else { 37 push( @users, $line ); 38 } 39 } 40 } 41 42 unless ( $written ) {

320 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 }

5

Objektorientierte Programmierung

push( @users, "$login\t$pwd\t$st\t$em" ); } unless ( seek( $fh, 0, 0 ) ) { $fh->close(); return undef; } unless ( truncate( $fh, 0 ) ) { $fh->close(); return undef; } foreach my $line ( @users ) { print( $fh "$line\n" ); } $fh->close(); return 1;

Der gezeigte Code ist bis auf wenige Ausnahmen identisch mit dem in der Klasse User gezeigten. Eine Ausnahme ist natürlich die zusätzliche Unterstützung des neuen Attributs. Aber dennoch verdienen die Zeilen 25 bis 27 besondere Aufmerksamkeit: 25 26 27

my $pattern = '^([^\t]+)' . '\t[^\t]+' x 2 . '\t.$'; if ( $line =~ /$pattern/ ) {

Hier sind zwei Besonderheiten festzustellen: Wir verwenden eine Variable als Searchpattern. Damit nicht genug, außerdem habe ich den Vervielfältigungsoperator für das Pattern benutzt. Der Ausdruck '^([^\t]+)' . '\t[^\t]+' x 2 . '\t.$'

ist derselbe wie '^([^\t]+)\t[^\t]+\t[^\t]+\t.$'

Weitere Beispiele hierzu gibt es bei der Beschreibung des Vervielfältigungsoperators. Ich glaube, nun haben wir das Wichtigste von OOP besprochen. Echte Praxisbeispiele finden Sie in den weiteren Kapiteln. Wem das nicht genügt, der kann in der PerlOnline-Dokumentation unter den Themen »perlboot«, »perlobj«, »perltoot« und »perltootc« schmökern. Einen wichtigen Aspekt von OOP haben wir bis jetzt noch nicht besprochen: Factories.

Factories

321

5.3 Factories Bisher haben wir nur Klassen aus der Sicht eines Objekts betrachtet (Instanzierung eines Objekts, Attribute eines Objekts, Schreiben und Lesen eines Objekts). Nun aber wollen wir unseren Horizont erweitern. Häufig benötigt man nicht eine einzelne Instanz einer Klasse, sondern eine Liste von mehreren Objekten. Dies ist zum Beispiel in administrativen Programmen der Fall, wenn ein Verwalter Daten von Benutzern ändern, Benutzer sperren oder löschen, neue Benutzer hinzufügen muss etc. Solche übergeordneten Funktionalitäten packt man heutzutage gerne in so genannte »Factories«, zu Deutsch »Fabriken«. Hinter diesem Ausdruck verbirgt sich eigentlich nichts anderes als ein ganz normales Package wie User oder AnonymousUser auch, es ist also nichts Mystisches daran. Das wichtigste Unterscheidungsmerkmal zu normalen Klassenmodulen ist, dass in Factories alle Funktionen definiert sind, die über einer oder mehreren Klassen angesiedelt sind. Nehmen wir doch gleich ein typisches Beispiel für eine Factory: Ein Administrator möchte den Datensatz eines Benutzers ändern. Dafür benötigt er eine Liste von Benutzern, aus der er einen für die Änderung auswählt. Diese Liste wird üblicherweise in einer Factory-Funktion erzeugt. Bei der Erzeugung der Liste von Datensätzen kommt in manchen Fällen die Geschwindigkeit vor der Forderung der sauberen Programmierung, die besagt: »Jede Operation für die Klasse User muss auch in dieser Klasse implementiert werden.« Nehmen wir unsere Klasse AnonymousUser als Beispiel: In einem System mit 100.000 Benutzern kann es ziemlich lange dauern, bis man eine Liste von instanzierten Objekten mit allen Attributen zusammengestellt hat. Doch was braucht man in der Liste wirklich? Meist doch nur den Benutzernamen, der anschließend ausgewählt wird. Es bedeutet also, mit Kanonen auf Spatzen zu schießen, wenn wir 100.000 Instanzen mit allen Attributen im Hauptspeicher erzeugen, obwohl das Speichern des Benutzernamens völlig ausreichend ist. Wenn wir zu den Themen »CGI« und »DBI« kommen, werden Sie sehen, dass man nicht einmal einen Namen, sondern nur die Datenbank-ID in der Liste braucht. Vor allem Hochschulabgänger der heutigen Generation unterliegen immer wieder dem Trugschluss, dass Interpreter bzw. Compiler einem die ganze Arbeit abnehmen und man so tun könnte, als hätte man einen unendlich großen Hauptspeicher und eine unendlich schnelle CPU. Mit ein wenig Hirnzellengymnastik aber kann man Applikationen erstellen, die zum einen so wenig Hauptspeicher wie möglich verwenden, zum anderen so schnell wie möglich ablaufen. Glauben Sie mir, ich weiß es aus Erfahrung: 1 GB Hauptspeicher ist schnell gefüllt, wenn man seinen Verstand nicht richtig einsetzt.

322

5

Objektorientierte Programmierung

Ich habe schon Entwickler gesehen, die drei Wochen lang Fehlersuche in ihrem Programm betrieben haben, das eine unbegrenzte Anzahl von Threads gestartet hat. Der Grund, warum das Programm auf Rechner A lief, auf Rechner B aber nicht, lag einfach daran, dass auf Rechner B die Anzahl der geöffneten FileHandles begrenzt war. Ich will Sie damit nicht auf eine Zeitreise ins Jahr 1985 schicken, als der Hauptspeicher noch in KBytes und die Festplattenkapazität in MBytes angegeben wurde. Aber gesunder Menschenverstand ist nie fehl am Platz! Nun zurück zu unserem eigentlichen Thema, den Factories. Lassen Sie uns eine (statische) Funktion implementieren, mit der man eine Liste aller Benutzer erhält. Wir wollen sie readUserLogins() nennen und im Modul UserFactory.pm abspeichern: sub readUserLogins { my @logins = (); use FileHandle; my $fh = new FileHandle( $dataPath, "r" ); unless ( $fh ) { return( undef, @logins ); } while ( defined( my $line = $fh->getline() ) ) { if ( $line =~ /^([^\t]+)/ ) { push( @logins, $1 ); } } $fh->close(); return ( 1, @logins ); }

Die Funktion benutzt, wie die vorherigen auch, die globale Variable $dataPath, in welcher der Pfad der Datendatei gespeichert ist. Von jedem Datensatz wird nur der Benutzername gelesen, die anderen Attribute sind uninteressant. Es soll ja nur eine Liste aller Benutzer zurückgegeben werden. Die Sache wäre etwas anders, wenn man Filter einsetzt, wie zum Beispiel: »Gib mir eine Liste aller Benutzer, die gesperrt sind.« In diesem Fall müsste man zusätzlich das Attribut status lesen.

Factories

323

Die zurückgegebene Liste der Benutzer enthält also nur die Werte des Attributs login. Das sieht sehr effizient aus. Ist es auch, muss ich hinzufügen. Kurz und prägnant. Man muss sich immer vor Augen halten, was mit dieser Liste passiert. In den meisten Fällen wird vom Anwender der Applikation ein einziges Element der Liste ausgewählt, und erst dann ist es notwendig, alle Attribute des selektierten Benutzers zu lesen. Für die Ausgabe der Liste reicht der Benutzername. Ich möchte Ihnen als Kontrast noch die (etwas langsamere) Lang-Version der Funktion zeigen, bei der jeder einzelne Datensatz als komplette Klasseninstanz angelegt wird (und dementsprechend viel CPU-Zeit und Hauptspeicher in Anspruch nimmt): sub readUsers { my @users = (); use FileHandle; my $fh = new FileHandle( $dataPath, "r" ); unless ( $fh ) { return( undef ); } while ( defined( my $line = $fh->getline() ) ) { if ( $line =~ /^([^\t]+)\t([^\t]+)(.)$/ ) { my $user = new User(); $user->setLogin( $1 ); $user->setPwd( $2 ); $user->setStatus( $3 ); push( @logins, $user ); } } $fh->close(); return ( 1, @users ); }

Übrigens, hier sehen wir ein Beispiel, wie man in einer Funktion mehrere Rückgabewerte an den Aufrufer zurückliefert. Der erste Wert ist der Rückgabestatus, der zweite beinhaltet die Liste der Benutzer. Im Falle eines Fehlers ist der Rückgabestatus undef, und es ist in diesem Fall auch das einzige Argument, das zurückgegeben wird. Denken Sie bitte daran, dass die Reihenfolge der Rückgabewerte nicht vertauschbar ist. Wenn wir nämlich mit return ( @users, 1 );

324

5

Objektorientierte Programmierung

zuerst das Array und am Schluss den Status zurückgeben, dann haben wir ein Problem, weil für den Aufrufer der Funktion der Status als letztes Element der Liste aufgenommen wird und kein eigenständiger Wert mehr ist. Der Aufruf my ( @users, $status ) = UserFactory::readUsers();

würde die Variable $status nicht versorgen, sie wäre undef. Es soll übrigens Entwickler geben, die in der Schleife Folgendes programmieren: while ( defined( my $line = $fh->getline() ) ) { if ( $line =~ /^([^\t]+)\t([^\t]+)(.)$/ ) { my $user = new User(); $user->setLogin( $1 ); $user->read(); push( @logins, $user ); } }

Der Code wäre tödlich, weil beim Aufruf $user->read();

die Datei noch einmal geöffnet und gelesen wird, ohne dass man es direkt sieht.

6 Die File-Module In diesem Kapitel möchte ich Ihnen einige hilfreiche Module für Zugriffe im Dateisystem vorstellen. Wenn Sie in Ihrem Skript Verzeichnisse anlegen, Dateien kopieren oder ganze Baumstrukturen im Filesystem durchsuchen müssen, dann führt an den FileModulen kein Weg vorbei. Ein Anwendungsbeispiel für Webadministratoren wird Ihnen den praktischen Nutzen der Module vor Augen führen.

6.1 File::Path Das Modul File::Path stellt zwei Funktionen zur Verfügung, mkpath() zum Anlegen von Verzeichnissen sowie rmtree() zum rekursiven Löschen von Verzeichnissen samt Unterverzeichnissen.

6.1.1 File::Path::mkpath() Syntax (Angaben in eckigen Klammern sind optional): use File::Path[ ()]; File::Path::mkpath( path[, flag[, perms ]] ) File::Path::mkpath( listRef [, flag[, perms ]] )

path ist der absolute oder relative Pfadname des anzulegenden Verzeichnisses. flag ist ein boolescher Wert (Default: FALSE). Ist für flag der TRUE-Wert angegeben, dann gibt die Funktion alle angelegten Verzeichnisse auf STDOUT aus. perms ist eine Bitmaske für die Zugriffsrechte und muss oktal angegeben sein (Default: 0777). Siehe hierzu die Funktionen chmod() sowie umask(). Die Funktion mkpath() gibt im skalaren Kontext die Anzahl der angelegten Verzeichnisse, im List-Kontext die Pfadnamen aller angelegten Verzeichnisse als Liste zurück. Existiert das anzulegende Verzeichnis bereits, liefert die Funktion im skalaren Kontext 0 zurück, im List-Kontext eine leere Liste. Es werden alle benötigten übergeordneten Verzeichnisse angelegt (und sind auch Bestandteil des Rückgabewerts), falls sie noch nicht existieren.

326

6

Die File-Module

Kann ein Verzeichnis nicht angelegt werden (z.B. weil der Prozess dafür nicht die notwendigen Rechte besitzt), dann beendet die Funktion das Hauptprogramm mit die(). Es empfiehlt sich also, den Aufruf von mkpath() in einen eval-Block zu stellen. Anstelle eines einzelnen Pfadnamens für das anzulegende Verzeichnis kann auch eine Referenz auf eine Liste von Pfadnamen angegeben werden. Beispiele für die Benutzung von mkpath(): use File::Path (); my $ret = File::Path::mkpath( "/a/b/c" ); # $ret enthält 3, falls weder /a noch /a/b noch /a/b/c # existiert # $ret enthält 2, falls /a existiert, aber /a/b und # /a/b/c nicht # $ret enthält 1, falls sowohl /a als auch /a/b # existiert, nicht aber /a/b/c # $ret enthält 0, falls sowohl /a, /a/b als auch /a/b/c # existieren mkpath() beendet das Hauptprogramm mit die(), falls ein Fehler aufgetreten ist. Deshalb wird im folgenden Beispielcode die Fehlermeldung des Skripts nicht mehr ausgegeben, wenn das DOS-Laufwerk X nicht existiert: use File::Path; my $dir = "X:/users/temp"; unless ( mkpath( $dir ) ) { print( STDERR "Fehler beim Anlegen von $dir\n" ); exit( 1 ); } print( "Verzeichnis $dir angelegt\n" );

Es erscheint nicht etwa die Fehlermeldung Fehler beim Anlegen von X:/users/temp

sondern mkdir X:/: No such file or directory at - line 4

File::Path

327

Das Programm wurde also in Zeile 4 abgebrochen, die nächste Zeile wird damit gar nicht mehr ausgeführt. Mit eval kann man den Abbruch verhindern: use File::Path; my $dir = "X:/users/temp"; eval { mkpath( $dir ); }; if ( $@ ) { print( STDERR "Fehler beim Anlegen von $dir\n" ); exit( 1 ); } print( "Verzeichnis $dir angelegt\n" );

Mit dieser Änderung wird das Programm nicht mehr abgebrochen, und unsere eigene Fehlermeldung kann ausgegeben werden: Fehler beim Anlegen von X:/users/temp

Die Funktionsweise von eval ist ausführlich in Anhang C beschrieben. $@ ist eine von Perl vordefinierte Variable, die nur dann gesetzt ist, wenn der davor stehende evalAufruf einen Fehler produziert hat. Diese Variable ist in Anhang B näher beschrieben. Hinter der schließenden geschweiften Klammer von eval muss ein Strichpunkt stehen. Ein weiteres Beispiel für mkpath() mit List-Kontext: my @ret = File::Path::mkpath( "/a/b/c/d", 0, 0755 ); # @ret enthält ( "/a", "/a/b", "/a/b/c", "a/b/c/d" ), # falls keines der Verzeichnisse existiert. # @ret ist leer, falls alle Verzeichnisse bereits # existieren.

Hinweis für UNIX: Alle angelegten Verzeichnisse erhalten die Zugriffsrechte, die sich ergeben, wenn 0755 mit dem negierten Wert von umask() bitweise UND-verknüpft wird. Detailinformationen über umask() finden Sie in Anhang C. Beispiel mit Angabe einer Liste von Verzeichnissen: my $ret = File::Path::mkpath( [ "/a", "/b", ] ); # Vermeidung des Programmabbruchs bei Fehlern mit # eval: my $ret = 1;

328

6

Die File-Module

eval { $ret = File::Path::mkpath( "/a/b" ); }; if ( $@ ) { print( "Laufzeitfehler '$@'\n" ); ... }

6.1.2 File::Path::rmtree() Syntax (Angaben in eckigen Klammern sind optional): use File::Path[ ()]; File::Path::rmtree( path ) File::Path::rmtree( listRef )

Die Funktion rmtree() arbeitet umgekehrt wie mkpath(). Sie löscht das angegebene Verzeichnis mit allen Unterverzeichnissen. Anstelle der skalaren Variable path kann auch eine Referenz auf eine Liste von Verzeichnispfaden (listRef) angegeben sein. rmtree() gibt die Anzahl erfolgreich gelöschter Objekte zurück.

Im Gegensatz zu mkpath() beendet rmtree() im Fehlerfall nicht das Hauptprogramm, es wird also kein eval-Block benötigt. Beispiele für rmtree(): use File::Path; my $ret = File::Path::rmtree( "/a/b" ); # $ret enthält entweder die Anzahl gelöschter Elemente, # wobei alle Dateien und Unterverzeichnisse von /a/b # sowie /a/b selbst gelöscht werden, # oder 0, falls kein Objekt gelöscht werden konnte. # Löschen mehrerer Verzeichnisse my $ret = File::Path::rmtree( [ "/a/b", "/a/c", ] );

6.2 File::Find Das Perl-Modul File::Find stellt die Funktionen find() sowie finddepth() zur Verfügung, mit deren Hilfe rekursive Operationen im Filesystem möglich sind. Die Funktionen arbeiten ähnlich wie das gleichnamige UNIX-Kommando, das rekursiv beginnend bei einem Startverzeichnis alle Unterverzeichnisse und Dateien ausgibt.

File::Find

329

6.2.1 File::Find::find() Syntax: use File::Find[ ()]; File::Find::find( \&wanted, dirList ) File::Find::finddepth( \&wanted, dirList ) # Ab Perl Version 5.6 File::Find::find( hashRef, dirList ) ... sub wanted { ... }

dirList ist eine Liste von Verzeichnissen, ab denen die Funktion find() rekursiv alle Unterverzeichnisse und Dateien findet und für jedes Verzeichnis und jede Datei die angegebene Funktion wanted aufruft. Die Funktion wanted, die von find() für jeden besuchten Pfad aufgerufen wird, erhält per Default den Pfadnamen des aktuell besuchten Pfades als Argument. Standardmäßig wird von find() vor dem Aufruf der Funktion in das Verzeichnis gewechselt (mit chdir()), zu dem der besuchte Pfad gehört, und die Variable $_ enthält nur den letzten Pfadanteil, nicht den kompletten Pfadnamen des besuchten Pfades. $File::Find::dir enthält den absoluten oder relativen Pfadnamen des aktuellen Ver-

zeichnisses, je nachdem, ob das Startverzeichnis in dirList absolut oder relativ angegeben ist. $File::Find::name enthält den absoluten oder relativen Pfadnamen des aktuellen

besuchten Pfades, je nachdem, ob das Startverzeichnis in dirList absolut oder relativ angegeben ist. Die letzte Variante von find(), die man erst ab Perl Version 5.6 benutzen kann, ist weiter unten erläutert. Die Funktion finddepth(), die ebenfalls im Modul File::Find enthalten ist, besucht zuerst alle Verzeichniseinträge, bevor für das Verzeichnis selbst die Funktion wanted aufgerufen wird. Ansonsten arbeitet sie genauso wie find(). Ich glaube, jetzt ist es an der Zeit, Licht ins Dunkel zu bringen und Ihnen die Arbeitsweise der Funktionen anhand von Beispielen zu zeigen.

330

6

Die File-Module

Beispiel für File::Find::find(): #!D:/Perl/bin/perl.exe -w use strict; use File::Find (); File::Find::find( \&process, @ARGV ); File::Find::finddepth( \&process, @ARGV ); exit( 0 ); sub process { # In "$_" steht nur der letzte Pfadanteil der # gerade besuchten Datei bzw. des Verzeichnisses. my $name = $_; # Da der Schalter -w angegeben ist, müssen die # Warnungen kurzzeitig ausgeschaltet werden, # weil der Perl-Interpreter sich sonst darüber # beschweren würde, dass die Variablen # $File::Find::dir und $File::Find::name jeweils nur # ein einziges Mal benutzt werden. no warnings; my $dir = $File::Find::dir; my $path = $File::Find::name; print( "dir = $dir, name = $name, path = $path\n" ); # -w Schalter von Perl wieder aktivieren use warnings; }

Angenommen, das Skript wurde mit dem Argument /temp aufgerufen und die Verzeichnisstruktur von /temp ist: /temp/ a.txt d1/ b.txt d2/

Dann wird Folgendes ausgegeben: # Ausgabe von find() dir = /temp, name = ., path = /temp dir = /temp, name = a.txt, path = /temp/a.txt dir = /temp, name = d2, path = /temp/d2 dir = /temp, name = d1, path = /temp/d1 dir = /temp/d1, name = b.txt, path = /temp/d1/b.txt

File::Find

331

# Ausgabe von finddepth() dir = /temp, name = a.txt, path = /temp/a.txt dir = /temp, name = d2, path = /temp/d2 dir = /temp/d1, name = b.txt, path = /temp/d1/b.txt dir = /temp, name = d1, path = /temp/d1 dir = /temp, name = ., path = /temp

Wie wir in der Ausgabe sehen, bearbeitet finddepth() die Einträge in einem Verzeichnis zuerst, dieses wird erst nach dem letzten Eintrag abgearbeitet. Nun kommen wir zur letzten Variante von find(), die es seit der Perl-Version 5.6 gibt. Ab dieser Version kann man bestimmte Einstellungen in einer Hash-Referenz verändern, die Funktion ist also flexibler geworden. Bedeutung der Keys in hashRef: hashRef ist eine Referenz auf ein Hash, in dem folgende Attribute angegeben werden können: 왘 wanted 왘 bydepth 왘 preprocess 왘 postprocess 왘 follow 왘 follow_fast 왘 follow_skip 왘 no_chdir Das wichtigste Attribut ist wanted, das als Value eine Referenz auf eine Funktion enthält, genauso wie bei der ersten Variante (\&wanted). Dieses Attribut muss angegeben sein, während alle weiteren optional sind. bydepth bewirkt, dass ein Verzeichnis nach allen darin enthaltenen Dateien und Unterverzeichnissen gefunden wird und hat dieselbe Auswirkung, als wäre die Funktion finddepth() anstelle von find() aufgerufen worden. preprocess kann für Filterzwecke verwendet werden und muss eine Codereferenz ent-

halten (zum Beispiel eine Referenz auf eine Funktion). Sie wird von find() aufgerufen, nachdem ein Verzeichnis gelesen worden ist, aber bevor die Schleife beginnt, in der die im Attribut wanted angegebene Funktion aufgerufen wird. Als Argument erhält sie von find() eine Liste von Dateien und Unterverzeichnissen und muss auch eine Liste (gegebenenfalls sortiert oder gefiltert) zurückliefern.

332

6

Die File-Module

postprocess kann für Übersichtszwecke verwendet werden und muss ebenfalls eine

Codereferenz enthalten. Der Code wird ohne Argumente aufgerufen, nachdem alle Einträge eines Verzeichnisses abgearbeitet wurden, und kann zum Beispiel den benutzten Plattenspeicher für dieses Verzeichnis ausgeben. Der Pfadname des aktuellen Verzeichnisses ist in $File::Find::dir gespeichert. Wenn man diese Variable nur einmal im Programmcode benutzt und das Modul mit der require-Direktive geladen hat, dann liefert Perl bei aktivem »-w«-Schalter eine Warnung, die man mit der Direktive no warnings; unterdrücken kann. Anschließend sollte man den -w-Schalter aber unbedingt wieder mit der Direktive use warnings; aktivieren. Hat man das File::Find-Modul mit der use-Direktive geladen, dann wird keine Fehlermeldung ausgegeben, man muss den -w-Schalter also nicht deaktivieren. follow wird verwendet, wenn find() symbolische Links verfolgen soll, und ist nur wirksam, wenn das verwendete Betriebssystem symbolische Links unterstützt. Das DefaultVerhalten ist, dass symbolische Links nicht verfolgt werden. Diese Option kann sehr viel Zeit und Systemressourcen in Anspruch nehmen, da für jeden gefundenen Pfad ein Hash-Element aufgebaut werden muss. Wenn das Attribut follow den Wert TRUE enthält, wird die Variable $File::Find::fullname gesetzt, die den aufgelösten absoluten Pfadnamen enthält, auf den der symbolische Link zeigt. follow_fast arbeitet wie follow, ist jedoch schneller, da nur für symbolische Links Hash-

Elemente aufgebaut werden. Allerdings kann es hier vorkommen, dass ein Pfad mehrfach besucht wird. follow_skip wird in Verbindung mit follow_fast benutzt und kann 3 Werte haben:

왘 0 Wird irgendein Pfad ein zweites Mal besucht, dann beendet sich find(). 왘 1 (Default) Werden Pfade besucht, die weder Verzeichnisse noch symbolische Links sind und die bereits einmal besucht worden sind, werden diese bei einem zweiten Besuch ignoriert. Werden Verzeichnisse oder symbolische Links ein zweites Mal besucht, dann beendet sich find(). 왘 2 Wird irgendein Pfad ein zweites Mal besucht, dann ignoriert find() diesen Pfad. no_chdir mit einem TRUE-Wert bedeutet, dass das aktuelle Verzeichnis unverändert bleibt.

In diesem Fall enthält die Variable $_ in der Funktion, die durch das Attribut wanted angegeben ist, denselben Pfadnamen wie $File::Find::name. Das Default-Verhalten ist, dass in

File::Find

333

das aktuell besuchte Verzeichnis gewechselt wird, bevor die Funktion aufgerufen wird, die durch das Attribut wanted gekennzeichnet ist. Beispiel für File::Find::find() ohne Verzeichniswechsel: #!D:/Perl/bin/perl.exe -w use strict; use File::Find; my %args = ( "wanted" => \&process, "no_chdir" => 1, ); File::Find::find( \%args, @ARGV ); exit( 0 ); sub process { # Durch "no_chdir" => 1 # enthält jetzt "$_" dasselbe wie # $File::Find::name my $name = $_; # Da der Schalter -w angegeben ist, müssen die # Warnungen kurzzeitig ausgeschaltet werden, # weil der Perl-Interpreter sich sonst darüber # beschweren würde, dass die Variablen # $File::Find::dir und $File::Find::name jeweils nur #ein einziges Mal benutzt werden no warnings; my $dir = $File::Find::dir; my $path = $File::Find::name; print( "dir = $dir, name = $name, path = $path\n" ); # Schalter -w von Perl wieder aktivieren use warnings; }

Angenommen, das Skript wurde mit dem Argument /temp aufgerufen, und die Verzeichnisstruktur von /temp ist: /temp/ a.txt d1/ b.txt d2/

334

6

Die File-Module

Dann wird Folgendes ausgegeben: dir dir dir dir dir

= = = = =

/temp, name = /temp, path = /temp /temp, name = /temp/a.txt, path = /temp/a.txt /temp, name = /temp/d2, path = /temp/d2 /temp, name = /temp/d1, path = /temp/d1 /temp/d1, name = /temp/d1/b.txt, path = /temp/d1/b.txt

Anwendungsbeispiel für File::Find Als Administrator von Webseiten steht man häufig vor dem Problem, dass alle Dokumente einer Website angepasst werden müssen, weil sich zum Beispiel die Struktur der Website oder der Servername geändert hat. Diese Aufgabe lässt sich sehr gut mit dem File::Find-Modul in Verbindung mit Pattern Matching erledigen. Nehmen wir den Fall an, dass in vielen Seiten der Website ein Link auf den Server www1.mydomain.com enthalten ist. Der Name des Servers hat sich vor kurzem geändert, er heißt nun www.otherdomain.com. Wir implementieren nun ein Perl-Skript namens chLinks.pl, das in allen HTML- und JSP-Dokumenten die entsprechenden Änderungen vornimmt. Für unser Beispiel soll das Rootverzeichnis der Dokumente /usr/httpd/htdocs sein. Dem Skript soll das Rootverzeichnis, in dem die Dokumente stehen, als Kommandozeilen Argument übergeben werden. Hier unser Skript: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21

#!/usr/bin/perl -w use strict; use use use use

Fcntl qw( :seek ); IO::Handle; FileHandle; File::Find;

my $root = shift( @ARGV ); unless ( $root and ( -d $root ) ) { usage(); exit( 1 ); } my %args = ( "wanted" => \&process, "no_chdir" => 1, ); File::Find::find( \%args, $root );

File::Find 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

335

exit( 0 ); sub process { my $path = $_; if ( $path !~ /html?$|jsp$/i ) { return; } my $fh = new FileHandle( $path, "r+" ); unless ( $fh ) { return; } my $data = join( "", $fh->getlines() ); my $spat = 'www1\.mydomain\.com'; my $rpat = 'www.otherdomain.com'; unless ( $data =~ s~$spat~$rpat~g ) { $fh->close(); return; } seek( $fh, 0, SEEK_SET ); truncate( $fh, 0 ); $fh->print( $data ); $fh->close(); } sub usage { STDERR->print( "usage: $0 \n" ); }

Erläuterungen zum Skript: In Zeile 05 sehen wir eine von verschiedenen Methoden, die symbolischen Konstanten SEEK_SET, SEEK_CUR und SEEK_END als Barewords zu verwenden. Diese sind im Modul Fcntl.pm als Exportgruppe :seek definiert und werden mit der use-Direktive in den Namespace unseres Hauptprogrammes aufgenommen. Die Direktive use IO::Handle in Zeile 06 sollte uns aus den Grundlagen bekannt sein. Hier noch einmal kurz, wofür sie gut ist: Mit der Direktive kann man statt print( STDERR "usage: $0 \n" );

den schöneren Code STDERR->print( "usage: $0 \n" );

verwenden.

336

6

Die File-Module

Die Zeilen 10 my $root = shift( @ARGV ); 11 unless ( $root and ( -d $root ) ) { 12 usage(); 13 exit( 1 ); 14 }

übernehmen das Argument aus der Kommandozeile, mit dem das Rootverzeichnis angegeben wird, ab dem die Suche beginnen soll. Das übergebene Argument wird daraufhin überprüft, ob es leer ist oder kein Verzeichnis enthält. In der Zeile 26

my $path = $_;

wird der absolute Pfadname der aktuell besuchten Datei oder des Verzeichnisses übergeben, weil das Hash-Element no_chdir einen TRUE-Wert hat. Die Zeile 28

if ( $path !~ /html?$|jsp$/i ) { return; }

stellt einen Filter dar. Alle Pfadnamen, die nicht mit HTM, HTML oder JSP (caseinsensitive durch die Matching-Option i) enden, werden vom Skript nicht bearbeitet. Normalerweise müsste man noch den Filter unless ( -f $path ) { return; }

einbauen, um auch Verzeichnisse und Spezialdateien zu filtern. Wenn man aber davon ausgeht, dass alle zu bearbeitenden Dateien grundsätzlich die Dateiendungen »HTM«, »HTML« oder »JSP« besitzen, kann man sich diese Zeile sparen. Die Filter kann man auch in einer eigenen Funktion implementieren und das Attribut preprocess von find() verwenden. Schneller arbeitet jedoch die hier gezeigte Variante. In den folgenden Zeilen wird die Datei geöffnet und der Inhalt in die Variable $data eingelesen. Der Programmcode 35 36 37 38 39 40 41

my $spat = 'www1\.mydomain\.com'; my $rpat = 'www.otherdomain.com'; unless ( $data =~ s~$spat~$rpat~g ) { $fh->close(); return; }

File::Copy

337

definiert zunächst das Suchpattern und den Ersetzungsstring. Beachten Sie bitte, dass das Suchpattern in einfache Quotes gestellt werden muss, damit der Backslash nicht als Sonderzeichen bewertet wird. Hätten wir doppelte Quotes verwendet, dann würde der Interpreter den Punkt ohne vorangestellten Backslash im Pattern Matching verwenden. Dort hätte dieser die Sonderbedeutung »beliebiges Zeichen«. Anschließend wird versucht, alle Vorkommnisse des Suchpatterns zu ersetzen. Kommt das Suchpattern nicht vor, wird das FileHandle geschlossen, und die Funktion beendet sich, weil keine weiteren Aktionen in der Datei notwendig sind. Das Schließen des FileHandles ist in jedem Fall notwendig. Wenn Sie das vergessen, bekommen Sie sehr schnell Probleme. Stellen Sie sich eine normale Website mit 10.000 Dateien vor. In UNIX darf ein Prozess normalerweise höchstens 61 gleichzeitig geöffnete FileHandles haben (eigentlich sind es 64, aber vom System werden ja bereits 3 für STDIN, STDOUT und STDERR belegt). Man kann das Limit zwar mit dem Kommando ulimit erhöhen, aber das ist nicht im Sinne des Erfinders. Merken Sie sich: Nicht mehr benötigte FileHandles nach Gebrauch schließen! Die Zeilen 43 44 45 46 47 }

seek( $fh, 0, SEEK_SET ); truncate( $fh, 0 ); $fh->print( $data ); $fh->close();

schreiben schließlich den geänderten Datei-Inhalt zurück und schließen das FileHandle. Vergessen Sie bitte nicht das Statement zum Schließen, weil damit die Systemressource für das FileHandle wieder frei wird. Nun können wir unser Skript mit folgendem Kommando ausprobieren: chLinks.pl /usr/httpd/htdocs

6.3 File::Copy Das Perl-Modul File::Copy bietet die beiden Funktionen copy() und move() an, mit denen Dateien kopiert oder umbenannt werden können. Mit move() können auch rekursive Strukturen verschoben bzw. umbenannt werden. Es ist also möglich, ein Verzeichnis, das wiederum Unterverzeichnisse und Dateien enthält, umzubenennen. copy() kann jedoch nur normale Dateien kopieren.

338

6

Die File-Module

Syntax (alle Angaben in eckigen Klammern sind optional): use File::Copy[ ()]; File::Copy::copy( sourcePath, destinationPath ) File::Copy::move( oldPath, newPath ) copy() kopiert die durch sourcePath angegebene Datei an die durch destinationPath

angegebene Stelle. move() kopiert zunächst wie copy(), löscht anschließend jedoch die durch oldPath ange-

gebene Datei. Beide Funktionen liefern bei Erfolg einen TRUE-Wert, bei einem Fehler einen FALSE-Wert zurück. Wenn das zweite Argument beider Funktionen ein bereits existierendes Verzeichnis und das erste Argument eine normale Datei ist, dann wird die Datei in dieses Verzeichnis kopiert (copy()) bzw. verschoben (move()). Beispiele: use File::Copy (); # Kopieren der Datei "/temp/a.txt" in die Datei # "/temp/a1.txt" unless ( File::Copy::copy( "/temp/a.txt", "temp/a1.txt" ) ) { # Fehler aufgetreten } # Umbenennen der Datei "/temp/a.txt" in # "/temp/a1.txt" unless ( File::Copy::move( "/temp/a.txt", "temp/a1.txt" ) ) { # Fehler aufgetreten }

Weitere Informationen zu den File-Modulen finden Sie in der Online-Dokumentation unter den Themen »File::Path«, »File::Find« und »File::Copy«.

7 Anwendungsbeispiele In den bisherigen Kapiteln habe ich Sie mit Grundlagen von Perl, Pattern Matching, objektorientierter Programmierung und Ein-/Ausgabe gefüttert. Das Augenmerk dieses Kapitel liegt nicht so sehr auf reiner Wissensvermittlung eines bestimmten Themas. Vielmehr möchte ich das bisher beschriebene Wissen in praktischen Beispielskripts vertiefen, die Ihnen im alltäglichen Umgang mit Daten und Dateien das Leben erleichtern.

7.1 dos2Unix.pl Mit diesem Anwendungsbeispiel möchte ich Ihnen helfen, alltägliche Probleme im Umgang mit Textdateien zu lösen, wenn man heterogene Netzwerke im Einsatz hat (manchmal reicht es auch, wenn man von jemandem eine Floppy mit Dateien bekommt; diese Vernetzung wird landläufig »Footnet« oder »Floppynet« genannt). Die Probleme kommen dadurch zustande, dass in UNIX das Zeilenende-Zeichen »\n« verwendet wird, während in Windows die zwei Steuerzeichen »\r\n« für denselben Zweck benötigt werden. Die Auswirkung unter UNIX ist: Wenn man in einem herkömmlichen Editor eine Windows-Textdatei editiert, hat man am Zeilenende immer ein seltsames Zeichen (beim »vi«, dem besten aller Editioren überhaupt, oder, sagen wir lieber, dem bekanntesten Editor unter UNIX, ist es das Zeichen »^M«). Alle Texteditoren, unter anderem auch der »vi« unter Linux (den man auch unter dem Namen »vim« kennt), sind mittlerweile so intelligent geworden, dass sie den Dateityp automatisch erkennen und Sonderzeichen gar nicht am Bildschirm ausgeben, wenn man z.B. in UNIX eine DOS-Datei editiert. Der Anwender merkt überhaupt nicht, dass die Textdatei eigentlich gar nicht für das verwendete Betriebssystem gedacht ist. Manche Editoren geben allerdings den Dateityp »DOS« oder »UNIX« in einer Statuszeile aus. In jedem Fall heißt es hier aufpassen!

340

7

Anwendungsbeispiele

Mit dem Skript, das wir im Folgenden implementieren, tritt dieses Problem nicht mehr auf, weil alle Windows-Zeilenende-Zeichen in die UNIX-Variante umgewandelt werden (es werden also alle »\r« vor den »\n« entfernt). Das Skript benötigt als Kommandozeilen-Argument den Pfadnamen der umzuwandelnden Datei. An den Pfad wird die Endung .unix angehängt und in diese Datei der umgewandelte Inhalt geschrieben. Es werden mehrere Möglichkeiten implementiert. Beispiel 1 (Umwandlung Zeile für Zeile): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

#!D:/Perl/bin/perl.exe -w use strict; use IO::Handle; use FileHandle; # Überprüfung der Argumente unless ( @ARGV ) { err( "keine Datei angegeben" ); exit( 1 ); } unless ( dos2Unix( $ARGV[ 0 ] ) ) { exit( 1 ); } exit( 0 ); sub dos2Unix { my ( $srcPath ) = @_; my $prefix = "dos2Unix(): "; unless ( $srcPath ) { err( $prefix, "ungültiges Argument" ); return undef; } unless ( -f $srcPath ) { err( $prefix, "Der Pfad '$srcPath' ist ungültig" ); return undef; } my $dstPath = "$srcPath.unix";

dos2Unix.pl 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 } 82 83 sub 84 85 86 87 88 89 90 91 }

341 if ( -f $dstPath ) { err( $prefix, "Die Datei '$dstPath' existiert bereits" ); return undef; } my $srcFh = new FileHandle( $srcPath, "r" ); unless ( $srcFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $srcPath ); return undef; } my $dstFh = new FileHandle( $dstPath, "w" ); unless ( $dstFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $dstPath ); $srcFh->close(); return undef; } binmode( $dstFh ); while ( defined( my $l = $srcFh->getline() ) ) { if ( $l =~ s/\s+$// ) ) { $dstFh->print( "$l\n" ); } else { $dstFh->print( "$l" ); } } $srcFh->close(); $dstFh->close(); return 1;

err { foreach my $arg ( @_ ) { STDERR->print( defined( $arg ) ? $arg : "undef" ); } STDERR->print( "\n" );

342

7

Anwendungsbeispiele

Erläuterungen: Das Statement 05 use IO::Handle;

benötigen wir, damit die Ausgabe nach STDERR mit der print()-Funktion von IO::Handle verwendet werden kann, sonst müssten wir uns mit der Eigenart der Perl-Funktion print() herumschlagen, dass nach STDERR kein Komma stehen darf. Die eigentliche Arbeit erledigt die Funktion dos2Unix(). Im Hauptprogramm prüfen wir nur, ob ein Argument angegeben wurde, und rufen dann die Funktion mit dem ersten Kommandozeilen-Argument als Parameter auf. Bis zur Zeile 66 sollten keine größeren Probleme auftauchen: Es werden FileHandles geöffnet. Das Statement 66

binmode( $dstFh );

in Verbindung mit 68 69 70 71 72 73 74 75

while ( defined( my $l = $srcFh->getline() ) ) { if ( $l =~ s/\s+$// ) ) { $dstFh->print( "$l\n" ); } else { $dstFh->print( "$l" ); } }

jedoch ist schon ein paar Worte der Erklärung wert. Die Funktion binmode() selbst habe ich bereits früher beschrieben, auch in Anhang C finden sich ein paar Zeilen darüber. Bei der Ausgabe mit print() muss das FileHandle auf Binärmodus geschaltet sein, damit wirklich nur das Zeichen »\n« geschrieben wird, unabhängig davon, auf welchem Betriebssystem das Skript läuft. Während der Binärmodus für die Ausgabe unbedingt nötig ist, kann (und muss) man die Eingabedatei im Textmodus belassen: binmode( $dstFh ); while ( defined( my $l = $srcFh->getline() ) ) { if ( chomp( $l ) ) {

Wie wir sehen, wird nur das FileHandle für die Ausgabe auf Binärmodus geschaltet, die Eingabedatei bleibt im Textmodus. Außerdem habe ich für die Entfernung der Zeilenende-Zeichen kein Pattern Matching, sondern die Funktion chomp() benutzt.

dos2Unix.pl

343

Wenn ich auch die Eingabedatei auf Binärmodus geschaltet hätte, würde unser Programm keine Umwandlung mehr durchführen, weil die Funktion chomp() nur für Textdateien sinnvoll ist, nicht aber für Binärdateien. Mehr zu dieser Problematik finden Sie im Kapitel »Ein-/Ausgabe«. Also: chomp() nur bei Dateien im Textmodus benutzen! Nun wollen wir uns eine andere Implementierung von dos2Unix.pl ansehen. Beispiel 2 (Umwandlung in einem Stück): # Es muss nur die Funktion "dos2Unix()" geändert werden sub dos2Unix { my ( $srcPath ) = @_; my $prefix = "dos2Unix(): "; unless ( $srcPath ) { err( $prefix, "ungültiges Argument" ); return undef; } unless ( -f $srcPath ) { err( $prefix, "Der Pfad '$srcPath' ist ungültig" ); return undef; } my $dstPath = "$srcPath.unix"; if ( -f $dstPath ) { err( $prefix, "Die Datei '$dstPath' existiert bereits" ); return undef; } my $srcFh = new FileHandle( $srcPath, "r" ); unless ( $srcFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $srcPath ); return undef; } my $dstFh = new FileHandle( $dstPath, "w" ); unless ( $dstFh ) {

344

7

Anwendungsbeispiele

err( $prefix, "Fehler beim Öffnen von Datei '", $dstPath ); $srcFh->close(); return undef; } binmode( $dstFh ); my $data = join( "", $srcFh->getlines() ); $data =~ s/\r\n/\n/g; $dstFh->print( $data ); $srcFh->close(); $dstFh->close(); return 1; }

Erläuterungen: Ich habe die Eingabeschleife ersetzt durch: my $data = join( "", $srcFh->getlines() ); $data =~ s/\r\n/\n/g; $dstFh->print( $data );

Die Funktion $srcFh->getlines()

gibt ein Array mit allen Zeilen der Datei als Elemente zurück. Dieses Array wird durch die join()-Funktion in einen String umgewandelt und landet in der skalaren Variable $data. In der darauf folgenden Programmzeile werden alle DOS-Zeilenende-Zeichen mit Pattern Matching durch das UNIX-Zeichen »\n« ersetzt, und der geänderte DateiInhalt wird in die Ausgabedatei geschrieben. Man hätte auch folgenden Code schreiben können: my @lines = $srcFh->getlines(); chomp( @lines ); $dstFh->print( join( "\n", @lines ) );

Aber Vorsicht: Auch in diesem Fall muss die Eingabedatei im Textmodus sein, weil wir chomp() verwenden!

unix2Dos.pl

345

Wenn wir uns die verschiedenen Varianten der Implementierung ansehen, ist der Unterschied oberflächlich gesehen nicht besonders groß. Ich möchte jedoch anmerken, dass speziell bei umfangreichen Dateien die erste Variante in jedem Fall vorzuziehen ist, weil hier immer nur eine einzelne Zeile eingelesen und bearbeitet wird. Der Speicherbedarf der anderen Varianten ist wesentlich größer, weil der gesamte Inhalt der Eingabedatei in den Hauptspeicher eingelesen wird. Nun wollen wir uns noch die umgekehrte Variante ansehen:

7.2 unix2Dos.pl Jetzt implementieren wir genau die entgegengesetzte Funktion, nämlich das Umwandeln einer UNIX-Textdatei in das DOS-Format, d.h., alle Zeilenende-Zeichen »\n« müssen durch die DOS Variante »\r\n« ersetzt werden. Hier die entsprechende Funktion: 01 sub unix2Dos { 02 my ( $srcPath ) = @_; 03 04 my $prefix = "unix2Dos(): "; 05 06 unless ( $srcPath ) { 07 err( $prefix, 08 "ungültiges Argument" 09 ); 10 return undef; 11 } 12 13 unless ( -f $srcPath ) { 14 err( $prefix, 15 "Der Pfad '$srcPath' ist ungültig" 16 ); 17 return undef; 18 } 19 20 my $dstPath = "$srcPath.dos"; 21 if ( -f $dstPath ) { 22 err( $prefix, 23 "Die Datei '$dstPath' existiert bereits" 24 ); 25 return undef; 26 } 27 28 my $srcFh = new FileHandle( $srcPath, "r" ); 29 unless ( $srcFh ) { 30 err( $prefix,

346 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 }

7

Anwendungsbeispiele

"Fehler beim Öffnen von Datei '", $srcPath ); return undef; } my $dstFh = new FileHandle( $dstPath, "w" ); unless ( $dstFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $dstPath ); $srcFh->close(); return undef; } binmode( $dstFh ); while ( defined( my $l = $srcFh->getline() ) ) { if ( $l =~ s/\s+$// ) { $dstFh->print( "$l\r\n" ); } else { $dstFh->print( "$l" ); } } $srcFh->close(); $dstFh->close(); return 1;

Wie wir sehen, ist diese Funktion der vorherigen sehr ähnlich, lediglich die Endung des Dateinamens der Ausgabedatei (Zeile 20) und der Pattern Matching-Ausdruck (Zeilen 50 und 51 ) sind unterschiedlich. Eigentlich müssten wir den Code in der Datei unix2Dos.pl abspeichern. Ich möchte Ihnen aber gerne ein Feature zeigen, das man »Wrapper« nennt. Wir erstellen nur eine einzige Sourcedatei, die beide Funktionen enthält. Dann kopieren wir diese Datei, so dass wir zwei gleiche Dateien dos2Unix.pl und unix2Dos.pl haben (in UNIX geht das natürlich mit einem symbolischen Link eleganter). Im Hauptprogramm überprüfen wir nun den Dateinamen unseres eigenen Skripts, so wie es aufgerufen wurde. Hierfür stellt Perl die vordefinierte Variable $0 zur Verfügung. Sie enthält den Pfadnamen des aufgerufenen Skripts, so wie er in der Kommandozeile eingegeben wurde.

unix2Dos.pl

347

Beispiel für »$0«: ./dos2Unix.pl /tmp/test.txt

Im Skript dos2Unix.pl enthält »$0« den Wert ./dos2Unix.pl. Sehen wir uns nun den Programmcode im Hauptprogramm an: if ( $0 =~ /dos2Unix\.pl$/ ) { unless ( dos2Unix( $ARGV[ 0 ] ) ) { exit( 1 ); } } else { unless ( unix2Dos( $ARGV[ 0 ] ) ) { exit( 1 ); } }

Wird das Skript also unter dem Namen dos2Unix.pl aufgerufen, dann verwendet es die Funktion dos2Unix(), andernfalls wird unix2Dos() benutzt. Der Vorteil ist, dass man den Umwandlungscode in einer einzelnen Datei verwalten kann. Und hier der gesamte Sourcecode für dos2Unix.pl bzw. unix2Dos.pl: #!D:/Perl/bin/perl.exe -w use strict; use IO::Handle; use FileHandle; # Überprüfung der Argumente unless ( @ARGV ) { err( "keine Datei angegeben" ); exit( 1 ); } if ( $0 =~ /dos2Unix\.pl$/ ) { unless ( dos2Unix( $ARGV[ 0 ] ) ) { exit( 1 ); } } else { unless ( unix2Dos( $ARGV[ 0 ] ) ) { exit( 1 ); } } exit( 0 );

348

7

sub dos2Unix { my ( $srcPath ) = @_; my $prefix = "dos2Unix(): "; unless ( $srcPath ) { err( $prefix, "ungültiges Argument" ); return undef; } unless ( -f $srcPath ) { err( $prefix, "Der Pfad '$srcPath' ist ungültig" ); return undef; } my $dstPath = "$srcPath.unix"; if ( -f $dstPath ) { err( $prefix, "Die Datei '$dstPath' existiert bereits" ); return undef; } my $srcFh = new FileHandle( $srcPath, "r" ); unless ( $srcFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $srcPath ); return undef; } my $dstFh = new FileHandle( $dstPath, "w" ); unless ( $dstFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $dstPath ); $srcFh->close(); return undef; } binmode( $dstFh ); while ( defined( my $l = $srcFh->getline() ) ) { if ( $l =~ s/\s+$// ) { $dstFh->print( "$l\n" );

Anwendungsbeispiele

unix2Dos.pl

349 } else { $dstFh->print( "$l" ); }

} $srcFh->close(); $dstFh->close(); return 1; } sub err { foreach my $arg ( @_ ) { STDERR->print( defined( $arg ) ? $arg : "undef" ); } STDERR->print( "\n" ); } sub unix2Dos { my ( $srcPath ) = @_; my $prefix = "unix2Dos(): "; unless ( $srcPath ) { err( $prefix, "ungültiges Argument" ); return undef; } unless ( -f $srcPath ) { err( $prefix, "Der Pfad '$srcPath' ist ungültig" ); return undef; } my $dstPath = "$srcPath.dos"; if ( -f $dstPath ) { err( $prefix, "Die Datei '$dstPath' existiert bereits" ); return undef; } my $srcFh = new FileHandle( $srcPath, "r" );

350

7

Anwendungsbeispiele

unless ( $srcFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $srcPath ); return undef; } my $dstFh = new FileHandle( $dstPath, "w" ); unless ( $dstFh ) { err( $prefix, "Fehler beim Öffnen von Datei '", $dstPath ); $srcFh->close(); return undef; } binmode( $dstFh ); while ( defined( my $l = $srcFh->getline() ) ) { if ( $l =~ s/\s+$// ) { $dstFh->print( "$l\r\n" ); } else { $dstFh->print( "$l" ); } } $srcFh->close(); $dstFh->close(); return 1; }

7.3 Hexdump von Dateien Häufig steht man vor dem Problem, den Inhalt einer Binärdatei überprüfen zu müssen. Es gibt zwar einige leistungsstarke Editoren, die auch Binärdaten im Hex-Format bearbeiten können, aber nicht jeder hat so etwas zur Hand, wenn er es braucht. Als Ersatz wollen wir ein Perl-Skript implementieren, das den Inhalt einer beliebigen Datei (sowohl Text- als auch Binärdatei) in Blöcken von jeweils 8 Bytes ausgibt. Die Ausgabe besteht aus drei Teilen, die jeweils durch einen Bar »|« getrennt sind. Über dem ersten Block wird ein Header ausgegeben, der die einzelnen Ausgabeteile beschreibt.

Hexdump von Dateien

351

Die drei auszugebenden Teile sind: 왘 Offset in der Datei (Position des Dateizeigers) Der Offset des Dateizeigers gibt die Position des ersten Bytes des gelesenen Blocks an. Die Feldbreite für den Datei-Offset beträgt 11 Zeichen (10 Stellen für den Offset, ein Leerzeichen). Alle Offsets sind hexadezimal. 왘 Binäre Darstellung des Inhaltes (Zeichensatz ISO-LATIN-1) Jedes Byte eines Dateiblocks (in unserem Beispiel werden pro Block jeweils 8 Bytes gelesen) wird in der Binärdarstellung 2-stellig mit seinem hexadezimalen Zeichencode angezeigt. Zwischen den Zeichencodes sowie vor dem ersten und nach dem letzten Zeichencode steht als Trenner ein Leerzeichen. Somit ergibt sich eine gesamte Feldbreite von 8*2+7*1+2 = 25 Zeichen. 왘 Direkte Darstellung der Zeichen (nicht darstellbare Zeichen werden als Punkt angezeigt) Die Feldbreite für die Textdarstellung eines gelesenen Blocks ist 8 Zeichen. Insgesamt beträgt die Breite der Ausgabe eines Datenblocks 47 Zeichen. Genug der Vorgaben und Definitionen. Jetzt wollen wir endlich etwas sehen! Beispiel (Datei test.data): Hallo Das ist eine Datei mit Steuerzeichen (hier ein Tab)

Ausgabe des Scripts: Offset | Hex-Darstellung |Text | ----------------------------------------------0 | 48 61 6c 6c 6f 0a 44 61 |Hallo.Da| 8 | 73 20 69 73 74 20 65 69 |s ist ei| 10 | 6e 65 20 44 61 74 65 69 |ne Datei| 18 | 0a 09 6d 69 74 20 53 74 |..mit St| 20 | 65 75 65 72 7a 65 69 63 |euerzeic| 28 | 68 65 6e 20 28 68 69 65 |hen (hie| 30 | 72 20 65 69 6e 20 54 61 |r ein Ta| 38 | 62 29 0a |b). |

Zunächst der Programmcode, mit dem der Header, der aus zwei Zeilen besteht, ausgegeben wird: # Ausgabe der ersten Zeile: printf( "%-10s | %-23s |%-8s|\n", "Offset", "Hex-Darstellung", "Text "

352

7

Anwendungsbeispiele

); # Ausgabe der zweiten Zeile: print( "-" x 47, "\n" );

Eine Beschreibung der Perl-Funktion printf() finden Sie im Anhang C. Sie funktioniert genauso wie sprintf(), jedoch gibt sie nach STDOUT, oder, falls ein FileHandle angegeben ist, in eine Datei aus, während sprintf() benutzt wird, um den formatierten String in einer Variable zu speichern. Die zweite Zeile gibt man am einfachsten mit dem Operator »x« (Vervielfältigungsoperator) aus. Im Beispiel wird also der String »-« 47-mal vervielfacht. Nun zum Einlesen der Datei. Für das Lesen von Binärdateien verwendet man nicht den Operator , sondern die Funktion read(), da der Inhalt der Datei nicht zeilenweise, sondern blockweise gelesen wird (in unserem Beispiel in 8-Byte-Blöcken). Außerdem sollte das FileHandle in jedem Fall in den Binärmodus umgestellt sein. Hier der Programmcode für das Einlesen der Daten: ... binmode( $fh ); while ( 1 ) { my $nbytes = read( $fh, $buf, $buflen ); ... } ...

Auch die read()-Funktion ist in Anhang C beschrieben. Die 10-stellige Darstellung des aktuellen Offsets für den Dateizeiger mit führenden Nullen kann man mit der Funktion printf() erreichen: printf( "%010d", $offset );

Die hexadezimale Darstellung der Zeichencodes erfolgt ebenfalls mit der printf()Funktion sowie der ord()-Funktion: printf( "%02d ", ord( $c ) );

Die Funktion ord() ist in Anhang C beschrieben. Hier nur so viel: Sie liefert den dezimalen Zeichencode des als Argument angegebenen Zeichens zurück. Bei der Ausgabe der letzten Datenblockzeile muss man beachten, dass der tatsächlich gelesene Block weniger Zeichen enthalten kann (im Minimalfall wird nur 1 Zeichen, im Maximalfall werden 8 Zeichen gelesen). Um eine konstante Feldbreite zu gewährleisten, müssen Leerzeichen als Füller ausgegeben werden. Dabei gelten folgende Formeln:

Hexdump von Dateien

353

Berechnung der Anzahl von Füllzeichen für den Binärteil: Pro eingelesenem Zeichen müssen insgesamt 3 Zeichen ausgegeben werden (2 Zeichen für den Code und ein Leerzeichen). Darin ist bereits das letzte Leerzeichen vor dem folgenden Trenner enthalten. Dazu muss ein Zeichen addiert werden (erstes Leerzeichen nach dem führenden Trenner). Daraus ergibt sich die maximale Breite, wenn 8 Bytes gelesen werden: maxLen = ( 3 * 8 ) + 1

Die (konstante) maximale Feldbreite ist also 25 Zeichen. Nun berechnen wir die tatsächliche Breite in Abhängigkeit von der Anzahl gelesener Zeichen (die am Dateiende ja geringer sein kann als 8 Byte): actualLen = ( 3 * nbytes ) + 1 # nbytes enthält die Anzahl tatsächlich gelesener Bytes

Die Differenz aus maxLen und actualLen ergibt die Anzahl der benötigten Füllzeichen: fillerCount = maxLen - actualLen;

Beispiel für nbytes = 1: actualLen = 4, fillerCount = 21 Beispiel für nbytes = 8: actualLen = 25, fillerCount = 0

Berechnung der Anzahl von Füllzeichen für den Textteil: Pro eingelesenem Zeichen wird genau 1 Zeichen ausgegeben. Da weder vor noch nach dem Textteil ein Leerzeichen folgt, ist die Berechnung relativ einfach: Berechnung der (konstanten) maximalen Breite: maxLen = 8

Berechnung der tatsächlichen Breite in Abhängigkeit von der Anzahl gelesener Zeichen: actualLen = nbytes;

Die Differenz aus maxLen und actualLen ergibt auch hier wieder die Anzahl der benötigten Füllzeichen: fillerCount = maxLen - actualLen;

354

7

Anwendungsbeispiele

Beispiel für nbytes = 1: actualLen = 1, fillerCount = 7 Beispiel für nbytes = 8: actualLen = 8, fillerCount = 0 Nachdem wir nun alle Grundlagen erörtert haben, folgt jetzt der vollständige Programmcode, den wir z.B. in der Datei hd.pl abspeichern: #!D:/Perl/bin/perl.exe -w use strict; use IO::Handle; use FileHandle; # Argument prüfen # Wir erwarten den Pfad einer Datei # als Argument unless ( @ARGV and ( -f $ARGV[ 0 ] ) ) { usage(); exit( 1 ); } my $path = shift( @ARGV ); my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { err( "cannot open file '", $path, "'" ); exit( 1 ); } # Nicht vergessen: Wir bearbeiten # Binärdateien! binmode( $fh ); # Offset des Dateizeigers initialisieren my $off = 0; # Blockgröße für die Leseoperationen my $bsize = 8; # Variable, in der die gelesenen Bytes # gespeichert werden my $buf = undef; # Maximale Länge des Binärteils der Ausgabe my $hexFieldLength = 25; # Maximale Länge des Textteils der Ausgabe my $textFieldLength = 8; # Header ausgeben

Hexdump von Dateien printf( "%-10s | %-23s |%-8s|\n", "Offset", "Hex-Darstellung", "Text " ); print( "-" x 49, "\n" ); # Eingabeschleife while ( 1 ) { # einen Block einlesen my $nbytes = read( $fh, $buf, $bsize ); unless ( defined( $nbytes ) ) { # es ist ein Fehler passiert err( "read error in file '", $path, "'" ); last; } # Wir sind am Ende der # Datei angelangt unless ( $nbytes ) { last; } # Aktuellen Offset ausgeben printf( "%10d | ", $off ); $off += $nbytes; # Zusammenbauen des Binärteils my $text = ""; # Schleife über alle eingelesenen # Bytes foreach my $c ( split( "", $buf ) ) { # Numerischen Zeichencode besorgen my $code = ord( $c ); # Alle nicht druckbaren Zeichen # werden als "." ausgegeben if ( ( $code < 0x20 ) or ( $code > 0x7f ) ) { $text .= "."; } else { $text .= $c; } # Hexcode des Zeichens ausgeben printf( "%02x ", $code ); } # Füllzeichen für Binärteil my $hexFiller = " " x ( $hexFieldLength -

355

356

7

Anwendungsbeispiele

( ( 3 * $nbytes ) + 1 ) ); # Füllzeichen für Textteil my $textFiller = " " x ( $textFieldLength - $nbytes ); print( $hexFiller ); $text .= $textFiller; print( "|$text|\n" ); # Wenn weniger als 8 Bytes gelesen # wurden, sind wir am Ende der # Datei angelangt if ( $nbytes < 8 ) { last; } } $fh->close(); exit( 0 ); # Funktion für Fehlerausgaben sub err { foreach my $arg ( @_ ) { STDERR->print( defined( $arg ) ? $arg : "undef" ); } STDERR->print( "\n" ); } # Funktion für Ausgabe, wie das Skript # aufgerufen werden muss sub usage { STDERR->print( "usage: $0 \n" ); } 1;

In unserem Skript habe ich die Perl-Funktion read() verwendet, um die Daten von Binärdateien einzulesen. Diese Funktion benutzt intern die Bibliotheksfunktion fread() der C-Library. Es gibt noch eine weitere Funktion, die denselben Zweck erfüllt, sie heißt sysread() und benutzt intern die Funktion read() der C-Library. Während read() den Betriebssystempuffer benutzt, ist dies bei sysread() nicht der Fall.

Lesen von Properties-Dateien

357

Wer von der Programmiersprache »C« kommt, kennt die Probleme mit der C-LibraryFunktion read() in Bezug auf Performance (bitte nicht verwechseln mit der Perl-Funktion read()). Der Programmierer muss sich selbst um die Pufferung der Daten kümmern. Wenn man nämlich eine große Datei in 8-Byte-Blöcken ungepuffert einliest, wird man ziemlich überrascht sein, wie lange es dauern kann, bis die Datei gelesen ist.

7.4 Lesen von Properties-Dateien Im letzten Anwendungsbeispiel haben wir einige Konstanten »hart verdrahtet« in unser Skript geschrieben. Ein eleganter und weitsichtiger Programmierer tut so etwas aber nicht, er versucht vielmehr, den Programmcode so zu schreiben, dass man das Verhalten eines Programms von außen steuern kann. Bisher haben wir dafür nur Kommandozeilen-Argumente benutzt, d.h., beim Aufruf eines Skripts wurde über Argumente in der Kommandozeile das Verhalten des Programms verändert. Speziell dann, wenn ein Programm nicht direkt von der Kommandozeile einer Shell aufgerufen wird, sondern zum Beispiel über CGI vom Webserver (das Thema werde ich weiter unten ziemlich ausführlich behandeln), kommen direkte Argumente nicht in Frage. Deshalb werden anstelle von Kommandozeilen-Argumenten oder hart codierten Konstanten im Skript häufig so genannte Properties-Dateien verwendet, mit denen das Verhalten von Skripts eingestellt werden kann, ohne den Programmcode ändern zu müssen. Properties-Dateien (ins Deutsche übersetzt: »Eigenschaftendateien«) sind den iniDateien von Windows ähnlich, nur besitzen sie keine Sektionen, die in eckige Klammern gesetzt sind. Eine Properties-Datei besteht grundsätzlich aus Key/Value-Paaren, zum Beispiel: rootDir = D:/Perl/scripts locale = de

In der oben aufgeführten Properties-Datei werden zwei variable Elemente definiert. Dies ist zum einen das Wurzelverzeichnis für das Skript, zum anderen die zu verwendende Sprachvariante (Locale). Das Skript muss die Einstellungen aus der Properties-Datei lesen, um dynamisch die eingestellten Parameter zu verwenden. Zu diesem Zweck implementieren wir nun eine Funktion, die den Pfadnamen der Properties-Datei sowie eine Hash-Referenz als Aufrufparameter erhält und das Hash mit den Einstellungen in der Datei füllt. Ich möchte Ihnen hier zwei verschiedene Vorgehensweisen präsentieren, einmal die althergebrachte prozedurale Implementierung, zusätzlich auch die objektorientierte Variante.

358

7

Anwendungsbeispiele

7.4.1 Prozedurale Implementierung Der folgende Beispielcode für das Lesen von Properties-Dateien ist prozedural implementiert (nicht objektorientiert): # Datei Properties.pm package Properties; use FileHandle; sub readProps { my ( $path, $href ) = @_; unless ( $path and ( -f $path ) ) { return undef; } unless ( $href and ref( $href ) and ( ref( $href ) =~ m/hash/i ) ) { return undef; } my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { return undef; } while ( defined( my $line = ) ) { chomp( $line ); my $pattern = '^\s*([^\s=]+)' . '\s*=\s*(.+)'; if ( $line =~ /$pattern/ ) { my ( $key, $val ) = ( $1, $2 ); $href->{ $key } = $val; } } $fh->close(); return 1; }

Verwendet werden kann die Funktion readProps() aus dem Hauptprogramm folgendermaßen: #!D:/Perl/bin/perl.exe -w use strict; use IO::Handle;

Lesen von Properties-Dateien use Properties (); my $path = "D:/properties/myScript.properties"; my %props = (); unless ( Properties::readProps( $path, \%props ) ) { STDERR->print( "Fehler \n" ); exit( 1 ); } my $rootDir = $properties{ "rootDir" }; my $locale = $properties{ "locale" }; ...

7.4.2 Objektorientierte Implementierung Dasselbe Beispiel (Properties) objektorientiert: # Datei Properties.pm package Properties; use FileHandle; sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { "path" => undef, "props" => {}, }; bless( $self, $class ); unless ( $self->setPath( @_ ) ) { return undef; } unless ( $self->read() ) { return undef; } return $self; } sub getPath { my $self = shift( @_ ); return $self->{ "path" }; }

359

360

7

sub getProperty { my $self = shift( @_ ); my $name = shift( @_ ); unless ( defined( $name ) ) { return undef; } my $props = $self->{ "props" }; unless ( exists( $props->{ $name } ) ) { return undef; } return $props->{ $name }; } sub setPath { my $self = shift( @_ ); my $arg = shift( @_ ); unless ( $arg and ( -f $arg ) ) { return undef; } $self->{ "path" } = $arg; return 1; } sub setProperty { my $self = shift( @_ ); my ( $name, $val ) = @_; unless ( defined( $name ) ) { return undef; } my $props = $self->{ "props" }; $props->{ $name } = $val; return 1; } sub read { my $self = shift( @_ ); my $path = $self->getPath(); my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { return undef; }

Anwendungsbeispiele

Lesen von Properties-Dateien

361

while ( defined( my $line = ) ) { chomp( $line ); my $pattern = '^([^\s=#]+)' . '\s*=\s*(.+)'; if ( $line =~ /$pattern/ ) { my ( $key, $val ) = ( $1, $2 ); $self->setProperty( $key, $val ); } } $fh->close(); return 1; } 1;

Ein kleiner Hinweis ist angebracht, glaube ich. Diese Implementierung für Properties ist relativ einfach und unterstützt z.B. keine Property-Werte, die sich über mehrere Zeilen erstrecken. Solche »Multi-Line«-Properties sehen in der Regel so aus: # Auszug aus einer Propertiesdatei mit # Multi-Line-Values header = Hallo und \ guten Tag \ zusammen! footer = Bis zum nächsten \ Mal

Values, die sich über mehrere Zeilen erstrecken, werden in der Regel durch einen Backslash am Ende der Zeile gekennzeichnet. Der Parser, der die Properties-Datei ausliest, muss solche Fortsetzungszeilen dann wieder zusammenbauen. Das wäre doch eine richtig schöne Hausaufgabe für Sie. Benutzt werden kann das Package-Properties wie folgt: #!D:/Perl/bin/perl.exe -w use strict; use IO::Handle; use Properties; my $props = new Properties( "D:/properties/myScript.properties" ); unless ( $props ) {

362

7

Anwendungsbeispiele

STDERR->print( "Fehler\n" ); exit( 1 ); } my $rootDir = $props->getProperty( "rootDir" ); my $locale = $props->getProperty( "locale" ); ...

Einer der Vorteile von OOP ist, dass man von Hash-Keys etc unabhängig wird. Ich glaube, wenn Sie ein paar Module objektorientiert implementiert (und vor allem später erweitert) haben, werden Sie meiner Meinung sein.

7.5 Ausgabe aller Hypertext-Links Als Webadministrator steht man häufig vor dem Problem, dass nach einiger Zeit haufenweise neue Webseiten vorhanden sind und der Überblick verloren geht. Dies trifft vor allem auf das Thema »Verlinkung« der Seiten zu. Nahezu in jeder Website gibt es tote Links, die ins Leere führen, weil das Zieldokument entweder gelöscht oder umbenannt worden ist. In diesem Anwendungsbeispiel wollen wir eine Funktion implementieren, die alle verwendeten Hypertext-Links extrahiert und in einem Hash speichert. Der Einfachheit halber sollen nur Hypertext-Links berücksichtigt werden, die durch das HTMLTag gekennzeichnet sind. Mit der implementierten Funktion kann man zum Beispiel eine Anwendung schreiben, die alle Hypertext-Links der Dokumente einer Website überprüft. Die Funktion hat zwei Aufrufparameter, den Pfad der zu untersuchenden HTMLDatei sowie eine Referenz auf ein Hash, in dem das Ergebnis gespeichert wird: # Funktionsdeklaration sub extractLinks { my ( $path, $href ) = @_; ... } # Aufruf der Funktion ... my $path = "..."; my %links = (); my $status = extractLinks( $path, \%links ); ...

Zum Auffinden der Hypertext-Links wird Pattern Matching mit regulären Ausdrücken verwendet. Dabei muss beachtet werden, dass ein Link nicht immer in der Form

Ausgabe aller Hypertext-Links

363

vorliegt, sondern zum Beispiel folgende Form haben kann:

Man kann also nicht davon ausgehen, dass das Attribut href direkt auf den Namen des Tags folgt. Außerdem müssen Hypertext-Links herausgefiltert werden, die JavaScript verwenden, zum Beispiel:

Des Weiteren können folgende Formen von Links vorkommen:

Die Quotes des URI (Wert des Attributs href) können weggelassen sein, oder der URI kann in einfachen Quotes statt in doppelten Quotes stehen. Wie man sieht, muss man schon ein bisschen Überlegung aufbringen, um den richtigen regulären Ausdruck für die Suche nach Hypertext-Links zu finden. Grundsätzlich findet man HTML-Hypertext-Links mit folgendem Suchpattern: /()/is # oder auch /(]+)>)/i

Jeder HTML-Hypertext-Link beginnt mit der Zeichenfolge »]/i

Im Suchpattern ist berücksichtigt, dass sowohl einfache als auch doppelte Quotes für den Attributwert verwendet werden, auch ein blanker Wert ohne Quotes wird gefunden. Leerzeichen zwischen Attributname und Attributwert können vorhanden sein oder nicht. Mit der Option i wird nicht zwischen Groß- und Kleinbuchstaben unterschieden. Beispiele möglicher Treffer der Suche: href="hallo.html"> href='hallo.html'> href=hallo.html> href = hallo.html> hallo.html> href="hallo.html" class="c1">

und viele weitere Varianten. Nun können wir die Funktion für das Extrahieren der Hypertext-Links implementieren: sub extractLinks { my ( $path, $href ) = @_; my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { return undef; } my $data = join( "", ); $fh->close(); my $pat1 = '()'; while ( $data =~ /$pat1/gis ) { my $all = $1; my $content = $2; my $pat2 = 'href\s*=\s*["\']?' . '(.+?)[\'" >]';

Ausgabe aller Hypertext-Links

365

my ( $hr ) = $content =~ /$pat2/is; next unless ( $hr ); $hr =~ s/#.+//s; $href->{ $hr } = $all; } return 1; }

Bei Links, die einen Anchornamen (gekennzeichnet durch das Zeichen »#« im Link) enthalten, wird dieser entfernt. Erläuterungen zur Funktion extractLinks(): In der Variable $pat1 ist das Suchpattern abgelegt, mit dem alle Hypertext-Links gefunden werden. Wichtig ist das Fragezeichen im Ausdruck (.+?). Mit dem Fragezeichen wird »Minimal-Matching« verwendet (siehe hierzu auch das Kapitel »Pattern Matching«). Bei einem Treffer wird das gesamte Tag in der Variable $1 abgelegt, in $2 stehen alle Attribute des Tags, die in einer zweiten Pattern Matching-Operation weiterverarbeitet werden. Aus allen möglichen Attributen muss der Wert des Attributs href extrahiert werden. Anschließend wird im Hash, das als Referenz übergeben wurde, ein Element erzeugt, das als Key den Attributwert von href enthält. Im Value wird der Inhalt des gesamten Tags gespeichert. Zur Demonstration wollen wir nun das Skript extractLinks.pl implementieren, das die Funktion benutzt und anschließend alle Hypertext-Links ausgibt: #!D:/Perl/bin/perl.exe -w use strict; use FileHandle; my %links = (); my $path = "tst.html"; extractLinks( $path, \%links ); foreach my $hr ( sort( keys( %links ) ) ) { print( "$hr = '$links{ $hr }'\n" ); } exit( 0 ); sub extractLinks { my ( $path, $href ) = @_;

366

7

Anwendungsbeispiele

my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { return undef; } my $data = join( "", ); $fh->close(); my $pat1 = '()'; while ( $data =~ /$pat1/gis ) { my $all = $1; my $content = $2; my $pat2 = 'href\s*=\s*["\']?' . '(.+?)[\'" >]'; my ( $hr ) = $content =~ /$pat2/is; next unless ( $hr ); $hr =~ s/#.+//s; $href->{ $hr } = $all; } return 1; }

7.6 dirname.pl Wer Umgang mit UNIX hat, kennt vielleicht das Programm dirname. Es wird häufig benutzt, um aus einem Dateipfad den Anteil aller Verzeichnisse zu extrahieren. Sehen wir uns dazu ein Beispiel an: # Gesamter Dateipfad /usr/local/bin/perl # Extrakt aller Verzeichnisse /usr/local/bin

Wenn man also das Programm dirname aufruft, erwartet es als Argument einen Dateipfad, von dem es den letzten Pfadanteil wegwirft und den Rest ausgibt. Genau das wollen wir jetzt in unserem sehr einfachen Skript auch tun: #!D:/Perl/bin/perl.exe -w use strict; unless ( @ARGV ) { exit( 1 ); } my $path = shift( @ARGV );

basename.pl

367

$path =~ s~\\~/~g; my ( $dir ) = $path =~ m~^(.+)/[^/]+$~; $dir = "" unless $dir; print( "$dir\n" ); exit( 0 );

Das Skript kann auch Pfadnamen in DOS-Notation mit dem Backslash als Trennzeichen für Verzeichnisse verarbeiten, indem es alle Backslashes durch Slashes ersetzt.

7.7 basename.pl Das Gegenstück zu dem vorher gezeigten UNIX-Kommando dirname heißt basename. Es extrahiert nicht den Verzeichnisanteil eines Dateipfads, sondern den letzten Anteil, der meist der Dateiname selbst ist (oder der Name des untersten Verzeichnisses). Hierzu ein Beispiel: # Gesamter Dateipfad /usr/local/bin/perl # Extrakt des letzten Pfadanteils perl

Diese Funktionalität kann u.a. in Skripts benutzt werden, die meist eine Funktion usage() haben. Wir selbst benutzten diese bereits mehrfach, um dem Anwender des Skripts mitzuteilen, dass er das Skript mit falschen oder fehlenden Argumenten aufgerufen hat. In der Ausgabe von usage() ist immer der Name des Skripts enthalten, der in der vordefinierten Variable $0 gespeichert ist. Allerdings steht in $0 der gesamte Dateipfad des Skripts, so wie es aufgerufen wurde, und nicht nur der Dateiname allein. Wenn nun der Dateipfad des Skripts sehr lang ist, sieht die Ausgabe von usage() nicht besonders schön aus, weil die Ausgabezeile umbrochen wird, zum Beispiel: usage: /export/home/home1/users/department1/group1/tools/bin/basename.pl

Schneidet man den vorderen Teil des Pfades ab, so dass nur noch der Dateiname übrig bleibt, dann wird die Ausgabe übersichtlicher: usage: basename.pl

Das Skript basename.pl ist schnell erstellt: #!D:/Perl/bin/perl.exe -w use strict;

368

7

Anwendungsbeispiele

unless ( @ARGV ) { exit( 1 ); } my $path = shift( @ARGV ); $path =~ s~\\~/~g; my ( $file ) = $path =~ m~^.+/([^/])+$~; $file = $path unless ( $file ); print( "$file\n" ); exit( 0 );

Das Skript berücksichtigt den Fall, dass der Pfad bereits so angegeben wurde, dass er nur den Dateinamen enthält. Wenn Sie in der Funktion usage() Ihres Skripts den Dateinamen aus $0 extrahieren wollen, müssen Sie nur die beiden Zeilen: my ( $file ) = $0 =~ m~^.+/([^/])+$~; $file = $0 unless ( $file );

verwenden.

7.8 Pfadnamen mit Sonderzeichen finden Web-Administratoren stehen leider allzu oft vor dem Problem, dass die einzelnen Autoren von Web-Dokumenten an Windows-PCs sitzen, während der Webserver auf einem UNIX-Rechner läuft. Das alleine wäre noch kein Beinbruch. Doch leider gibt es einen gravierenden Unterschied bei Pfadnamen zwischen Windows und UNIX. Nein, wenn Sie jetzt an die Backslashes denken, liegen Sie falsch. Ich meine vielmehr den Umstand, dass Pfadnamen in Windows Leerzeichen (Blanks) enthalten dürfen, während man diese in UNIX tunlichst nicht verwenden sollte, es sei denn, man möchte sich (und andere) ärgern. Das folgende Skript sucht ab dem angegebenen Verzeichnis (es dürfen auch mehrere angegeben sein) nach Dateien bzw. Verzeichnissen, die Leerzeichen enthalten, und gibt die Übeltäter aus: #!/usr/bin/perl -w use strict; use File::Find (); File::Find::find( { wanted => \&process, no_chdir => 1, }, @ARGV );

Pfadnamen mit Sonderzeichen finden

369

exit( 0 ); sub process { my $path = $File::Find::name; if ( $path =~ /[\s]/ ) { print( "$path\n" ); } }

Eine Beschreibung der Funktion File::Find::find() finden Sie im vorangegangenen Kapitel »Die File-Module«. Hier eine kurze Beschreibung, was alles passiert: Durch den Aufruf der Funktion find() des Packages File::Find werden alle Verzeichnisse rekursiv durchsucht, die man dem Skript beim Aufruf als KommandozeilenArgumente übergibt. Bei jedem Unterverzeichnis und jeder Datei unterhalb der angegebenen Verzeichnisse wird die Funktion process() aufgerufen. In der Variable $File::Find::name wird ihr der aktuelle Pfadname übergeben (dies kann ein Verzeichnis- oder ein Dateipfad sein). Die Funktion prüft nun, ob im Pfadnamen Leerzeichen vorkommen und gibt den Pfad aus, falls Leerzeichen vorhanden sind. Will man nicht nur Leerzeichen, sondern auch andere Sonderzeichen im Pfadnamen finden, dann sollte man die Abfrage ändern: if ( $path =~ m~^[\w\./-]$~ ) { return; } print( "$path\n" );

Jetzt beendet sich die Funktion bei allen Pfadnamen, die nur alphanumerische Zeichen, Punkte, Slashes oder Bindestriche enthalten, ohne Ausgabe, während bei der vorherigen Version eine Ausgabe erfolgt ist, wenn ein unerlaubtes Zeichen gefunden wurde. Der große Unterschied zum vorherigen Beispiel ist, dass wir nun nach erlaubten Zeichen suchen, während wir vorher unerlaubte Zeichen als Suchkriterium im Pattern Matching hatten. Welche Wahl wir für das Pattern treffen, hängt immer davon ab, was mehr Schreibaufwand bedeutet. Je mehr unerlaubte Zeichen vorhanden sind, desto eher sollte man die letzte Methode anwenden, bei der wir nach erlaubten Zeichen suchen.

370

7

Anwendungsbeispiele

7.9 Automatische Dateien erzeugen Im Weiteren werden wir ein Perl-Skript implementieren, das per Zufallszahlengenerator Dateibäume erzeugt. Die Dateien sind HTML-Dateien und untereinander verlinkt. Sowohl Dateinamen als auch die Texte der erzeugten Dateien werden automatisch per Zufallsgenerator erzeugt. Über Variablen kann man einstellen, wie tief die Baumstruktur sein soll, wie viele Dateien und Unterverzeichnisse in einem Verzeichnis angelegt werden und wie viele Wörter pro Datei maximal verwendet werden sollen. Mancher wird jetzt einwerfen: »Wofür soll so ein Skript gut sein?« Nun, es ist ganz einfach ein Tool zum Erzeugen von Testdaten, mit denen man die Performance von Webservern und Suchmaschinen prüfen kann. Ganz nebenbei kann man damit natürlich auch den Hauptspeicher und die Festplatte bis zum letzten freien Byte füllen. Zudem ist es ein Beispiel für rekursive Programmierung. Am besten, wir sehen uns die Arbeitsweise in einem Beispiel an: C:\temp\files>..\createFiles.pl 6 Verzeichnisse und 35 Dateien angelegt C:\temp\files>dir Datenträger in Laufwerk C: hat keine Bezeichnung. Datenträgernummer: D0BD-39DF Verzeichnis von C:\temp\files 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002

15:40

. 15:40

.. 18:57 461 index.html 18:57 2.693 napkoxoajg.html 18:57 3.828 leijjioliunwld.html 18:57 1.971 egvnuiymfsoujoi.html 18:57 4.199 sariewoeyg.html 18:57

js 18:57

obwoxxkyebsmm 5 Datei(en) 13.152 Bytes 4 Verzeichnis(se), 1.154.564.096 Bytes frei

C:\temp\files>cd js C:\temp\files\js>dir Datenträger in Laufwerk C: hat keine Bezeichnung. Datenträgernummer: D0BD-39DF Verzeichnis von C:\temp\files\js 29.06.2002 29.06.2002 29.06.2002 29.06.2002

18:57 18:57 18:57 18:57



. .. 518 index.html 3.061 zhzijaensoeknue.html

Automatische Dateien erzeugen 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002

371

18:57 4.340 mpmehrlyimvuvia.html 18:57 1.017 gbaru.html 18:57 2.522 bvgltihkxeef.html 18:57

fpjjgtdfeuaihdm 18:57

uwuanuo 5 Datei(en) 11.458 Bytes 4 Verzeichnis(se), 1.154.564.096 Bytes frei

C:\temp\files\js>cd uwuanuo C:\temp\files\js\uwuanuo>dir Datenträger in Laufwerk C: hat keine Bezeichnung. Datenträgernummer: D0BD-39DF Verzeichnis von C:\temp\files\js\uwuanuo 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002 29.06.2002

18:57

. 18:57

.. 18:57 429 index.html 18:57 792 fiwolkytbl.html 18:57 3.855 eoupezsiuo.html 18:57 4.451 miaupreaac.html 18:57 2.001 oosfdigb.html 5 Datei(en) 11.528 Bytes 2 Verzeichnis(se), 1.154.457.600 Bytes frei

C:\temp\files\js\uwuanuo>

Wie wir an den Ausgaben sehen, legt das Skript im aktuellen Verzeichnis 2 Unterverzeichnisse und 5 Dateien an. Bis auf die Datei index.html sind alle Datei- und Verzeichnisnamen automatisch generiert. Derselbe Vorgang findet in den Unterverzeichnissen js und obwoxxkyebsmm statt, ebenso wie in der nächsten Ebene des Dateibaums. Hier der Inhalt der Datei index.html in der obersten Ebene: C:\temp\files>type index.html

Index

egvnuiymfsoujoi.html
leijjioliunwld.html
napkoxoajg.html
sariewoeyg.html
js
obwoxxkyebsmm


C:\temp\files>

372

7

Anwendungsbeispiele

Diese Datei ist sozusagen die »Einstiegsseite«, bei der man mit der Navigation durch den erzeugten Baum beginnt. Der Vollständigkeit halber noch der Inhalt einer HTMLDatei: C:\temp\files>type egvnuiymfsoujoi.html

Bilde setStatus allow zusammenbauen sogar

Festkommazahl Kurz them HTTP_ACCEPT_ENCODING Einsen geglie Kodieren beschrieben literal DataKonsult hierf³r comment zustõndi trast er³brigt Telnet Allgemeinen Kourne Bibliotheken inklusive ³b triebssysteme Anspruch lower Konstruktor Option nacktes Draft vorb N UNIX doppelt Hõufigkeit geliefert assoziatives einzeln umlenken ebener dargestellte nette Eingabefeld Schlie¯en Wir Neuimplementie characters berechtigt verdrahteten inside insofern removed Dimensi rce lesbarer ge ballaballa W³rde entschlie¯en Abrauchen skalaren V rationszeile includePrefix Wollmilchsau denn Dateimanager Satzteil rent qualifizierten reqUri Pflichtargument zwischengespeichert Uni tDir p_label_fr h÷chsten doris Steuerzahler zuordnet ³bersichtlich AR austreten angeforderte hen hallo Abspeichern video sonst dbDriv ktive Gr³nde Pflichtargument tatsõchliche Programmdatei LONGBLOB Z ts durcheinanderbringen Uns ausgef³hrten Templateteil jemandem abg Einzig sqrt gehabt Leiche ReadWrite getCookieHeader bewertet Para inzip Ende Betrieb R³ckgabedaten betrachten Standardmodul Ersetzun able seconds LATIN NICHT Beispiele handhaben never unwahr document efgehen Instanzierung quid _setExpires weitergereicht asin softwa rn M³ller Gl³cksfall Dienstag gewichen

C:\temp\files>

Da die Zeilen der Ausgabe nicht auf die Seite eines Buches passt, sind sie rechts abgeschnitten. Auch sind Umlaute nicht richtig dargestellt. Das liegt wie immer am Zeichensatz von DOS. Wem die angezeigten Wörter bekannt vorkommen: Ich habe den ASCII-Text des Buches verwendet, um eine Wortliste zu erstellen. Aus dieser Wortliste werden per Zufallsgenerator die Inhalte der Dateien generiert. Nun haben wir die Arbeitsweise des Dateigenerators gesehen. Jetzt wollen wir in den Programmcode einsteigen. Fangen wir mit dem Hauptprogramm an: 01 #!D:/Perl/bin/perl -w 02 03 use strict; 04

Automatische Dateien erzeugen 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

use FileHandle; use IO::Handle; use Cwd (); # Ausgabepuffer ausschalten STDOUT->autoflush( 1 ); # Variablen für die minimale und maximale Anzahl von # Wörtern pro Datei. Die tatsächliche Anzahl von # Wörtern, die in eine Datei geschrieben werden, # ermittelt der Zufallsgenerator. Jede Datei # hat also eine andere Größe. our $maxWordCount = 500; our $minWordCount = 50; # Variable für die Tiefe des erzeugten Dateibaums. # Sie ist hier auf 1 gesetzt, d.h., es werden keine # Unterverzeichnisse angelegt, sondern nur Dateien # in der obersten Ebene des Baums. our $maxLevel = 1; # Variable für die Anzahl von Dateien, die pro # Ebene des Dateibaums angelegt werden. our $fileCount = 5; # Variable für die Anzahl von Verzeichnissen, die # in einer Ebene des Dateibaums angelegt werden. our $dirCount = 2; # Endung für Dateinamen our $ext = "html"; # Name der Index-Datei, die für die Navigation # durch den Dateibaum benötigt wird. our $indexName = "index.$ext"; # Variablen, in denen die Gesamtzahl der erzeugten # Dateien und Verzeichnisse gespeichert werden. our $nfiles = 0; our $ndirs = 0; # Referenzvariable auf ein anonymes Hash, das die # Baumstruktur enthält my $tree = {}; # Aktuelles Verzeichnis auf der Festplatte ermitteln # (entspricht dem Betriebssystem Kommando "pwd") my $curDir = Cwd::cwd(); # Variable, die man für Erweiterungszwecke verwenden # kann. Sie enthält das Verzeichnis, ab dem der

373

374 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

7

Anwendungsbeispiele

# Dateibaum angelegt wird. our $rootDir = $curDir; # Array für die Wörter, die per Zufallsgenerator in die # angelegten HTML-Dateien geschrieben werden. our @words = (); # Jetzt lesen wir eine Datei, in der alle Wörter enthalten # sind, die wir für die Inhalte der erzeugten Dateien # verwenden. Jedes Wort steht dabei in einer Zeile. readWords( "../wordlist.txt" ); # Mit "createInRam()" legen wir die gesamte Baumstruktur # im Hauptspeicher an. Erst später werden auch die Verzeichnisse # und Dateien auf der Festplatte erzeugt. # Das ist deshalb notwendig, weil die einzelnen Dokumente # über Hypertextlinks miteinander verbunden werden müssen. # In der Datei "index.html" einer Ebene sind also alle # Dateien der Ebene sowie die Indexdatei der nächsten Ebene # als Link enthalten. Die Namen der nächsten Ebene sind aber # bei der Erzeugung der darüber liegenden Ebene noch nicht # bekannt. Deshalb wird der gesamte Baum erst im Hauptspeicher, # dann im Filesystem angelegt. createInRam(); # Zurücksetzen der Zähler für die angelegten Dateien # und Verzeichnisse $nfiles = 0; $ndirs = 0; # "createOnDisk()" legt die Dateien und Verzeichnisse # des Hauptspeichers auf der Festplatte an. createOnDisk(); # Ausgabe, wie viele Dateien und Verzeichnisse insgesamt # angelegt wurden. print( "$ndirs Verzeichnisse und $nfiles Dateien angelegt\n" ); exit( 0 );

In Zeile 07 verwende ich ein bisher unbekanntes Modul Cwd. Daraus verwende ich die Funktion cwd(). Wenn man aber weiß, dass sich hinter dem Kürzel cwd der Begriff »current working directory« versteckt, wird deutlich, was damit gemeint ist. In Zeile 52 lese ich das aktuelle Verzeichnis der Shell. In diesem werden die Dateien und Verzeichnisse vom Skript angelegt. Das Modul Cwd ist übrigens Bestandteil der Standard-Distribution von Perl. Mit der Anweisung readWords( "../wordlist.txt" ); in Zeile 56 werden alle Wörter eingelesen, die in der Datei wordlist.txt stehen (je Zeile ein Wort). Diese Datei ist im übergeordneten Verzeichnis abgelegt. Die Funktion readWords() geht davon aus, dass

Automatische Dateien erzeugen

375

pro Zeile ein Wort steht und legt die Wörter im Array @words ab. Sie können also durch Editieren der Datei für unterschiedliche Inhalte der erzeugten Dateien sorgen (ich habe darin alle Wörter dieses Buches abgelegt). Interessant ist der Aufruf der Funktion createInRam() (Zeile 79). Diese Funktion ist nämlich rekursiv, d.h., sie ruft sich selbst auf. Doch dazu mehr weiter unten. An dieser Stelle sei nur erwähnt, dass sie die komplette Struktur des Dateibaums zunächst im Hauptspeicher ablegt. Verwendet wird hierzu die Variable $tree, in der alle Einträge (sowohl Dateien als auch Unterverzeichnisse) als Subhashes gespeichert sind. Erst in Zeile 88 wird durch den Aufruf der Funktion createOnDisk() der Dateibaum auch auf der Festplatte erzeugt (wiederum durch Rekursion). Den Zwischenschritt im Hauptspeicher benötigen wir, weil die Dateien untereinander verlinkt werden. Dafür brauchen wir die Dateinamen der Ziele eines Hypertextlinks. Diese werden aber später erzeugt als der Link auf die Dateien, deshalb müssen wir alle Dateien erst virtuell im Hauptspeicher anlegen, damit deren Dateiname bekannt ist. Nun wollen wir uns den Unterfunktionen in der Reihenfolge ihrer Komplexität nähern. Beginnen wir mit den einfachen Funktionen: Einlesen der Wortliste: sub readWords { my ( $path ) = @_; my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { print( STDERR "Fehler beim Lesen der Datei $path\n" ); return undef; } # Alle Wörter (je Wort eine Zeile) einlesen @words = ; # Alle Zeilenende-Zeichen mit einem einzigen # Statement entfernen chomp( @words ); return 1; }

Funktion für die zufällige Auswahl eines Wortes aus der Liste: sub getWord { # @words enthält eine Liste aller Wörter. # Es wird per Zufallsgenerator ein Index # aus dem Array ausgewählt und das entsprechende # Element zurückgegeben. return $words[ int( rand( scalar( @words ) ) ) ]; }

376

7

Anwendungsbeispiele

Erzeugen eines Datei- oder Verzeichnisnamens per Zufallsgenerator: Die Funktion wird beim Anlegen des Dateibaums im Hauptspeicher verwendet. Dort sind alle Verzeichnisse und Dateien als Keys von Subhashes vertreten: sub createName { my ( $href, $ext ) = @_; # Die erste Datei eines Verzeichnisses ist immer die # Datei "index.html" (der Dateiname ist in der globalen # Variable "$indexName" abgelegt). unless ( exists ( $href->{ $indexName } ) ) { return $indexName; } # Der Datei- oder Verzeichnisname ist maximal # 16 Zeichen lang my $maxLength = 16; # Array der Zeichen, aus denen der Datei- oder # Verzeichnisname zusammengebaut wird. Die Vokale # tauchen öfter auf, da sie in deutschen Worten auch # öfter vorkommen als Konsonanten. my @chars = ( "a" .. "z", "a", "e", "i", "o", "u", "e", "e", "o", "u", "a", "i" ); # Ermittlung der tatsächlichen Länge des Datei- oder # Verzeichnisnamens my $len = int( rand( $maxLength ) ) + 1; my $name = ""; # Schleife zum Aufbauen des Namens while ( 1 ) { # Erst den kompletten Namen erzeugen for ( my $i = 1; $i { $name } ) ) { last; } else { $name = ""; } } return $name; }

Sehen wir uns als Nächstes die Funktionen an, mit denen Dateien und Verzeichnisse auf der Festplatte angelegt werden. Erzeugen der Indexdatei: sub createIndexFile { my ( $index, $dirs, $files ) = @_; # "$index" ist eine Hash-Referenz auf den Eintrag für die # Indexdatei (im RAM als Subhash abgelegt). # "$dirs" ist eine Array-Referenz, welche die Verzeichnisnamen # der nächsten Ebene enthält. # "$files" ist eine Array-Referenz, welche die Dateinamen # aller im aktuellen Verzeichnis anzulegenden Dateien # enthält (ohne "index.html"; dieser Name wird vorher # gefiltert). # Pfadnamen aus dem Key "path" des Hashs extrahieren my $path = $index->{ path }; # Datei anlegen my $fh = new FileHandle( $path, "w" ); unless ( $fh ) { print( STDERR "Fehler beim Anlegen der Datei $path\n" ); return undef; } # Header in Datei schreiben print( $fh { entries } = {}; # Das aktuelle Verzeichnis für die Ebene wird im # Hash-Element "path" abgelegt. $tree->{ path } = $rootDir; # Hier kommt die Rekursion: # Die Funktion ruft sich selbst auf, diesmal # mit den Argumenten für "$parent", "$href" und # "$level" createInRam( $tree, $tree->{ entries }, 1 ); # Nicht vergessen: Bei rekursiven Funktionen ist # es besonders wichtig, dass wir keine Endlosaufrufe # bauen. return; } # Wenn diese Programmstelle erreicht wird, dann ist # sichergestellt, dass die Funktion mit drei Argumenten # aufgerufen wurde. # Wir erzeugen so viele Dateinamen, wie in der Variable # "$fileCount" vorgegeben ist. for ( my $i = 1; $i { $name } = {}; # Referenz für eine einfachere Schreibweise my $hr = $href->{ $name }; # Attribute des Objekts im Subhash speichern

Anwendungsbeispiele

Automatische Dateien erzeugen $hr->{ parent } = $parent; $hr->{ type } = "f"; $hr->{ path } = $parent->{ path } . "/$name"; # Dateizähler erhöhen $nfiles++; } # Bei einer großen Anzahl von Objekten im Dateibaum # empfiehlt es sich, ab und zu etwas auszugeben, damit # der Anwender des Skripts weiß, was abläuft. # Jeweils nach 100 erzeugten Dateiobjekten machen wir # eine Statusausgabe. unless ( $nfiles % 100 ) { printf( "%10d Dateien im RAM angelegt\n", $nfiles ); } # Mit der folgenden Abfrage stellen wir sicher, dass nur # so viele Ebenen im Dateibaum angelegt werden, wie durch # die Variable "$maxLevel" vorgegeben ist. # Bei einem Wert von "1" werden nur Dateien in der obersten # Ebene angelegt, aber keine Unterverzeichnisse. if ( $level >= $maxLevel ) { return; } # Nun legen wir Unterverzeichnisse in der aktuellen # Verzeichnisebene an. Es werden so viele Unterverzeichnisse # erzeugt wie durch die Variable "$dirCount" vorgegeben. for ( my $i = 1; $i { $name } = {}; # Referenzvariable für eine abkürzende Schreibweise my $hr = $href->{ $name }; # Objektattribute als Hash-Elemente speichern $hr->{ parent } = $parent; $hr->{ type } = "d"; $hr->{ path } = $parent->{ path } . "/$name"; # # # # #

Subhash für die Verzeichniseinträge anlegen. Wie schon weiter oben gesagt, kommen die Einträge nicht direkt als Keys in "$hr", sondern es wird ein Subhash mit dem Key "entries" dafür verwendet, um Namenskollisionen zu vermeiden.

383

384

7

Anwendungsbeispiele

$hr->{ entries } = {}; # Zähler für die erzeugten Verzeichnisse erhöhen $ndirs++; # Statusausgabe nach jeweils 50 erzeugten Verzeichnissen unless ( $ndirs % 50 ) { printf( "%10d Verzeichnisse im RAM angelegt\n", $ndirs ); } # Rekursiver Aufruf für die nächste Ebene. Als Parent # wird die Hash-Referenz für die aktuelle Ebene übergeben, # der Zähler für die Ebene ist um eins erhöht. createInRam( $hr, $hr->{ entries }, $level + 1 ); } }

Und zu guter Letzt createOnDisk(): # Die Funktion "createOnDisk()" legt alle Elemente des Dateibaums, # die vorher mit "createInRam()" erzeugt # worden sind, auf der Festplatte an. sub createOnDisk { my ( $href ) = @_; # "$href" enthält eine Hash-Referenz auf das aktuelle # Verzeichnisobjekt. Wenn es nicht angegeben ist, dann # wird die Hash-Referenz für die erste Ebene verwendet. unless ( $href ) { $href = $tree->{ entries }; } # Arrays für die Datei- und Verzeichnisnamen my @dirs = (); my @files = (); # Schleife über alle Keys des Subhashs. Diese sind identisch # mit den Datei- bzw. Verzeichnisnamen. # Mit der Perl-Funktion "grep()" filtern wir die Indexdatei # heraus, denn diese wird gesondert erzeugt. foreach my $name ( grep( ! /^\Q$indexName\E$/, keys( %{ $href } ) ) ) { # Referenzvariable für abkürzende Schreibweise my $hr = $href->{ $name }; # Je nach Typ des Eintrags den Namen in das entsprechende # Array stellen.

Automatische Dateien erzeugen

385

if ( $hr->{ type } eq "f" ) { push( @files, $name ); } else { push( @dirs, $name ); } } # Indexdatei erstellen my $index = $href->{ $indexName }; createIndexFile( $index, \@dirs, \@files ); # Dateien und Unterverzeichnisse erzeugen createFiles( $href, \@files ); createDirs( $href, \@dirs ); # Per Rekursion die Einträge der Unterverzeichnisse # anlegen foreach my $name ( @dirs ) { # Hash-Referenz für das Objekt des Unterverzeichnis # auslesen my $dir = $href->{ $name }->{ entries }; # Rekursiver Aufruf für das aktuelle Unterverzeichnis createOnDisk( $dir ) if ( $dir ); } return 1; }

Jetzt haben wir alle Einzelteile zusammen. Hier noch einmal der komplette Programmcode des Skripts, das ich unter dem Dateinamen createFiles.pl abgespeichert habe: #!D:/Perl/bin/perl -w use strict; use FileHandle; use IO::Handle; use Cwd (); # Ausgabepuffer ausschalten STDOUT->autoflush( 1 ); # Variablen für die minimale und maximale Anzahl von # Wörtern pro Datei. Die tatsächliche Anzahl von # Wörtern, die in eine Datei geschrieben werden, # ermittelt der Zufallsgenerator. Jede Datei # hat also eine andere Größe. our $maxWordCount = 500; our $minWordCount = 50;

386

7

# Variable für die Tiefe des erzeugten Dateibaums. # Sie ist hier auf 1 gesetzt, d.h., es werden keine # Unterverzeichnisse angelegt, sondern nur Dateien # in der obersten Ebene des Baums. our $maxLevel = 1; # Variable für die Anzahl von Dateien, die pro # Ebene des Dateibaums angelegt werden. our $fileCount = 5; # Variable für die Anzahl von Verzeichnissen, die # in einer Ebene des Dateibaums angelegt werden. our $dirCount = 2; # Endung für Dateinamen our $ext = "html"; # Name der Index-Datei, die für die Navigation # durch den Dateibaum benötigt wird. our $indexName = "index.$ext"; # Variablen, in denen die Gesamtzahl der erzeugten # Dateien und Verzeichnisse gespeichert werden. our $nfiles = 0; our $ndirs = 0; # Referenzvariable auf ein anonymes Hash, das die # Baumstruktur enthält my $tree = {}; # Aktuelles Verzeichnis auf der Festplatte ermitteln # (entspricht dem Betriebssystem Kommando "pwd") my $curDir = Cwd::cwd(); # Variable, die man für Erweiterungszwecke verwenden # kann. Sie enthält das Verzeichnis, ab dem der # Dateibaum angelegt wird. our $rootDir = $curDir; # Array für die Wörter, die per Zufallsgenerator in die # angelegten HTML-Dateien geschrieben werden. our @words = (); # Jetzt lesen wir eine Datei, in der alle Wörter enthalten # sind, die wir für die Inhalte der erzeugten Dateien # verwenden. Jedes Wort steht dabei in einer Zeile. readWords( "../wordlist.txt" ); # Mit "createInRam()" legen wir die gesamte Baumstruktur # im Hauptspeicher an. Erst später werden auch die Verzeichnisse # und Dateien auf der Festplatte erzeugt.

Anwendungsbeispiele

Automatische Dateien erzeugen # Das ist deshalb notwendig, weil die einzelnen Dokumente # über Hypertextlinks miteinander verbunden werden müssen. # In der Datei "index.html" einer Ebene sind also alle # Dateien der Ebene sowie die Indexdatei der nächsten Ebene # als Link enthalten. Die Namen der nächsten Ebene sind aber # bei der Erzeugung der darüber liegenden Ebene noch nicht # bekannt. Deshalb wird der gesamte Baum erst im Hauptspeicher, # dann im Filesystem angelegt. createInRam(); # Zurücksetzen der Zähler für die angelegten Dateien # und Verzeichnisse $nfiles = 0; $ndirs = 0; # "createOnDisk()" legt die Dateien und Verzeichnisse # des Hauptspeichers auf der Festplatte an. createOnDisk(); # Ausgabe, wie viele Dateien und Verzeichnisse insgesamt # angelegt wurden. print( "$ndirs Verzeichnisse und $nfiles Dateien angelegt\n" ); exit( 0 ); # Rekursive Funktion, mit der ein hierarchischer Dateibaum # in Form von Hashes von Hashes (usw.) im Hauptspeicher # erzeugt wird. # Wenn die Funktion ohne Argumente aufgerufen wird, dann # beginnt sie in der obersten Ebene des Dateibaumes mit # der Erzeugung von Datei und Verzeichnisobjekten. sub createInRam { my ( $parent, $href, $level ) = @_; # "$parent" ist eine Hash-Referenz auf die übergeordnete # Ebene des Dateibaums. Wenn "$parent" nicht angegeben ist, # dann beginnt die Generierung des Baums in der obersten # Ebene der Hierarchie. # "$href" ist eine Referenz auf die Ebene, in der # Datei- und Verzeichniseinträge erzeugt werden sollen. # "$level" ist ein Zähler für die Tiefe des Baums. Mit # jeder neu hinzukommenden Ebene wird der Zähler um eins # erhöht. unless ( $parent ) { # Hash für die erste Ebene des Dateibaums als Referenz # anlegen. Alle Verzeichniseinträge werden im Subhash # "entries" abgelegt. Warum ich eigens ein Subhash für # die Einträge verwende, anstatt die Datei- und # Verzeichnisnamen direkt als Keys in "$tree" verwende? # In diesem Fall hätten wir ein Problem, wenn einer # der Einträge zufällig den Namen "path" hätte,

387

388

7 # denn dieser Key wird bereits als Attribut für # den Pfadnamen des aktuellen Verzeichnisses # verwendet. $tree->{ entries } = {}; # Das aktuelle Verzeichnis für die Ebene wird im # Hash Element "path" abgelegt. $tree->{ path } = $rootDir; # Hier kommt die Rekursion: # Die Funktion ruft sich selbst auf, diesmal # mit den Argumenten für "$parent", "$href" und # "$level" createInRam( $tree, $tree->{ entries }, 1 ); # Nicht vergessen: Bei rekursiven Funktionen ist # es besonders wichtig, dass wir keine Endlosaufrufe # bauen. return; } # Wenn diese Programmstelle erreicht wird, dann ist # sichergestellt, dass die Funktion mit drei Argumenten # aufgerufen wurde. # Wir erzeugen so viele Dateinamen, wie in der Variable # "$fileCount" vorgegeben ist. for ( my $i = 1; $i { $name } = {}; # Referenz für eine einfachere Schreibweise my $hr = $href->{ $name }; # Attribute des $hr->{ parent } $hr->{ type } = $hr->{ path } =

Objekts im Subhash speichern = $parent; "f"; $parent->{ path } . "/$name";

# Dateizähler erhöhen $nfiles++; }

Anwendungsbeispiele

Automatische Dateien erzeugen # Bei einer großen Anzahl von Objekten im Dateibaum # empfiehlt es sich, ab und zu etwas auszugeben, damit # der Anwender des Skripts weiß, was abläuft. # Jeweils nach 100 erzeugten Dateiobjekten machen wir # eine Statusausgabe. unless ( $nfiles % 100 ) { printf( "%10d Dateien im RAM angelegt\n", $nfiles ); } # Mit der folgenden Abfrage stellen wir sicher, dass nur # so viele Ebenen im Dateibaum angelegt werden, wie durch # die Variable "$maxLevel" vorgegeben ist. # Bei einem Wert von "1" werden nur Dateien in der obersten # Ebene angelegt, aber keine Unterverzeichnisse. if ( $level >= $maxLevel ) { return; } # Nun legen wir Unterverzeichnisse in der aktuellen # Verzeichnisebene an. Es werden so viele Unterverzeichnisse # erzeugt wie durch die Variable "$dirCount" vorgegeben. for ( my $i = 1; $i { $name } = {}; # Referenzvariable für eine abkürzende Schreibweise my $hr = $href->{ $name }; # Objektattribute als Hash-Elemente speichern $hr->{ parent } = $parent; $hr->{ type } = "d"; $hr->{ path } = $parent->{ path } . "/$name"; # Subhash für die Verzeichniseinträge anlegen. # Wie schon weiter oben gesagt, kommen die Einträge # nicht direkt als Keys in "$hr", sondern es wird # ein Subhash mit dem Key "entries" dafür verwendet, # um Namenskollisionen zu vermeiden. $hr->{ entries } = {}; # Zähler für die erzeugten Verzeichnisse erhöhen $ndirs++; # Statusausgabe nach jeweils 50 erzeugten Verzeichnissen unless ( $ndirs % 50 ) { printf(

389

390

7

Anwendungsbeispiele

"%10d Verzeichnisse im RAM angelegt\n", $ndirs ); } # Rekursiver Aufruf für die nächste Ebene. Als Parent # wird die Hash-Referenz für die aktuelle Ebene übergeben, # der Zähler für die Ebene ist um eins erhöht. createInRam( $hr, $hr->{ entries }, $level + 1 ); } } # Die Funktion "createOnDisk()" legt alle Elemente des Dateibaums, # die vorher mit "createInRam()" erzeugt # worden sind, auf der Festplatte an. sub createOnDisk { my ( $href ) = @_; # "$href" enthält eine Hash-Referenz auf das aktuelle # Verzeichnisobjekt. Wenn es nicht angegeben ist, dann # wird die Hash-Referenz für die erste Ebene verwendet. unless ( $href ) { $href = $tree->{ entries }; } # Arrays für die Datei- und Verzeichnisnamen my @dirs = (); my @files = (); # Schleife über alle Keys des Subhashs. Diese sind identisch # mit den Datei- bzw. Verzeichnisnamen. # Mit der Perl-Funktion "grep()" filtern wir die Indexdatei # heraus, denn diese wird gesondert erzeugt. foreach my $name ( grep( ! /^\Q$indexName\E$/, keys( %{ $href } ) ) ) { # Referenzvariable für abkürzende Schreibweise my $hr = $href->{ $name }; # Je nach Typ des Eintrags den Namen in das entsprechende # Array stellen. if ( $hr->{ type } eq "f" ) { push( @files, $name ); } else { push( @dirs, $name ); } } # Indexdatei erstellen my $index = $href->{ $indexName };

Automatische Dateien erzeugen createIndexFile( $index, \@dirs, \@files ); # Dateien und Unterverzeichnisse erzeugen createFiles( $href, \@files ); createDirs( $href, \@dirs ); # Per Rekursion die Einträge der Unterverzeichnisse # anlegen foreach my $name ( @dirs ) { # Hash-Referenz für das Objekt des Unterverzeichnis # auslesen my $dir = $href->{ $name }->{ entries }; # Rekursiver Aufruf für das aktuelle Unterverzeichnis createOnDisk( $dir ) if ( $dir ); } return 1; } sub createDirs { my ( $href, $dirs ) = @_; # "$href" enthält eine Hash-Referenz auf das aktuelle # Verzeichnisobjekt, "$dirs" ist eine Array-Referenz # auf alle Verzeichnisnamen. # Schleife über alle Verzeichnisnamen foreach my $name ( @{ $dirs } ) { # Hash Objekt für aktuelles Verzeichnis besorgen my $dir = $href->{ $name }; # Verzeichnis anlegen unless ( mkdir( $dir->{ path } ) ) { print( STDERR "Fehler $! beim Anlegen ", "des Verzeichnisses ", $dir->{ path }, "\n" ); return undef; } # Anzahl erzeugter Verzeichnisse erhöhen und # Statusausgabe nach jeweils 50 Verzeichnissen $ndirs++; unless ( $ndirs % 50 ) { printf( "%10d Verzeichnisse auf FP angelegt\n", $ndirs ); } }

391

392

7 return 1;

} sub createFiles { my ( $href, $files ) = @_; # # # # #

"$href" enthält eine Hash-Referenz auf das aktuelle Verzeichnisobjekt, "$files" ist eine Array-Referenz auf alle Dateinamen ausschließlich des Namens für die Indexdatei. Diese wird mit einer eigenen Funktion angelegt.

# Schleife über alle Dateinamen foreach my $name ( @{ $files } ) { # HTML-Titel per Zufallsgenerator erzeugen my $title = ""; # Wir erzeugen insgesamt 5 Wörter für den Titel for ( my $i = 1; $i { path }, "\n" ); if ( $hr->{ type } eq "d" ) { printTree( $hr->{ entries }, $level + 1 ); } } } sub createName { my ( $href, $ext ) = @_; # Die erste Datei eines Verzeichnisses ist immer die # Datei "index.html" (der Dateiname ist in der globalen # Variable "$indexName" abgelegt).

395

396

7 unless ( exists ( $href->{ $indexName } ) ) { return $indexName; } # Der Datei- oder Verzeichnisname ist maximal # 16 Zeichen lang my $maxLength = 16; # Array der Zeichen, aus denen der Datei- oder # Verzeichnisname zusammengebaut wird. Die Vokale # tauchen öfter auf, da sie in deutschen Worten auch # öfter vorkommen als Konsonanten. my @chars = ( "a" .. "z", "a", "e", "i", "o", "u", "e", "e", "o", "u", "a", "i" ); # Ermittlung der tatsächlichen Länge des Datei- oder # Verzeichnisnamens my $len = int( rand( $maxLength ) ) + 1; my $name = ""; # Schleife zum Aufbauen des Namens while ( 1 ) { # Erst den kompletten Namen erzeugen for ( my $i = 1; $i { $name } ) ) { last; } else { $name = ""; } } return $name;

}

Anwendungsbeispiele

Automatische Dateien erzeugen

397

sub readWords { my ( $path ) = @_; my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { print( STDERR "Fehler beim Lesen der Datei $path\n" ); return undef; } # Alle Wörter (je Wort eine Zeile) einlesen @words = ; # Alle Zeilenende-Zeichen mit einem einzigen # Statement entfernen chomp( @words ); return 1; } sub getWord { # @words enthält eine Liste aller Wörter. # Es wird per Zufallsgenerator ein Index # aus dem Array ausgewählt und das entsprechende # Element zurückgegeben. return $words[ int( rand( scalar( @words ) ) ) ]; } sub createWord { my @chars = ( "a", "e", "i", "a" .. "z", "a", "e", "i", "ä", "ö", "ü", "a", "e", "i", );

"o", "u", "o", "u", "ß", "o", "u",

my $maxLen = 12; my $len = int( rand( $maxLen ) ) + 3; my $word = ""; for ( my $i = 1; $i close(); # Schleife, in der wir nach Wörtern suchen. # Wörter bestehen aus Buchstaben und deutschen Umlauten. # Mit dem regulären Ausdruck werden nur Wörter gefunden, # die aus mindestens zwei Zeichen bestehen. while ( $text =~ /([a-zäöüß][a-zäöüß]+)/gi ) { # Abspeichern des Treffers in "$word". Dies ist notwendig, # weil wir anschließend noch einmal Pattern Matching # verwenden; dabei würde "$1" überschrieben. my $word = $1; # Filtern von Wörtern, die nur gleiche Zeichen # enthalten, z.B. "aaaaa" if ( $word =~ /^(.)\1+$/ ) { next; } # Nun filtern wir noch Wörter, die mehr als zwei gleiche # Zeichen hintereinander enthalten, z.B. "annnders". if ( $word =~ /(.)\1{2,}/ ) {

399

400

7

Anwendungsbeispiele

next; } # Wir haben ein gültiges Wort gefunden, das wir als Key # in unserem Hash speichern. Interessant ist nur der Key, # nicht aber der Value des Hash-Elements. $words{ $word } = 1; } # Jetzt haben wir eine Wortliste in unserem Hash. Diese # speichern wir in der Datei "wordlist.txt" ab, pro Wort eine # Zeile. $fh = new FileHandle( $wordlistPath, "w" ); unless ( $fh ) { print( "kann Datei $wordlistPath nicht anlegen\n" ); exit( 1 ); } foreach my $word ( keys( %words ) ) { print( $fh "$word\n" ); } exit( 0 ); END { $fh->close() if ( $fh ); }

7.10 Dateibäume verwalten Im Weiteren möchte ich Ihnen ein Skript zeigen, mit dem sich Änderungen in Dateibäumen verfolgen lassen. Damit kann man feststellen, ob neue Dateien und Verzeichnisse seit einem bestimmten Zeitpunkt hinzugekommen sind, ob sich die Daten auf der Festplatte geändert haben (und vor allem, welche Dateien oder Verzeichnisse davon betroffen sind), oder welche Dateien und Verzeichnisse gelöscht wurden. Ein Einsatzgebiet für das folgende Skript könnte im Backupbereich (Stichwort »Differenzsicherung«) liegen. Ich persönlich habe es für einen speziellen Anwendungsfall geschrieben: Unter UNIX gibt es den Begriff »Package«. Darunter versteht man meist ein Softwarepaket, das als Ganzes auf dem Rechner installiert wird (und auch als Ganzes wieder entfernt werden kann). Vor allem als Administrator mehrerer Rechner, die gleichartig installiert sein sollen, z.B. wenn Loadbalancer eingesetzt werden, muss man immer die gleichen Pakete installieren, und zwar nicht nur ein einziges, sondern meist Dutzende. UNIX stellt mehrere Tools zur Verfügung, mit denen man selbst Softwarepakete schnüren kann, die dann als eine Einheit installiert (und auch wieder entfernt) werden können.

Dateibäume verwalten

401

Um ein solches Paket bestehend aus mehreren Einzelpaketen zusammenzustellen, muss man wissen, welche Dateien letztlich zu installieren sind. Das ist oft gar nicht so einfach, weil bei der Installation der Software die zugehörigen Dateien in unterschiedlichen Verzeichnissen abgelegt werden. Genau hier hilft das Skript. Nachdem alle Einzelpakete installiert sind, bildet man die Differenz aus dem alten Datenbestand der Festplatte vor der Installation des ersten Pakets und nach der Installation des letzten Pakets. Alle Dateien und Verzeichnisse, die sich dazwischen geändert haben oder hinzugekommen sind, müssen in das Gesamtpaket mit aufgenommen werden. Bevor ich Sie nun mit dem Programmcode erschlage, möchte ich kurz die Arbeitsweise des Skripts erläutern: Angenommen, wir lassen das Skript zum ersten Mal laufen. Dann liest es standardmäßig den gesamten Dateibaum der Festplatte. Allerdings kann man in der Kommandozeile beliebig viele Verzeichnisse angeben, die durchsucht werden sollen. Ohne ein Argument wird unter UNIX das Rootverzeichnis »/«, unter Windows das Rootverzeichnis des aktuellen Laufwerks als Startpunkt für die Suche verwendet. Wenn Sie das Skript ohne Argumente starten, wird der gesamte Dateibaum ab dem Rootverzeichnis gelesen. Je nach Datenbestand und Prozessor sowie Hauptspeicherausbau kann dies sehr lange dauern. Auf meinem Windows-PC mit ca. 310.000 Dateien und Verzeichnissen in einem Laufwerk kommen da schon mal 350 MB Hauptspeicherbedarf zusammen, und das Skript benötigt evtl. mehr als 30 Minuten für das Lesen des Datenbestandes. Unter UNIX werden Sie evtl. Fehlermeldungen erhalten, wenn Sie das Skript nicht unter dem Account root starten, da man dann nicht für alle Verzeichnisse oder Dateien die nötigen Leserechte besitzt. Gehen wir einmal davon aus, dass genügend Hauptspeicher und auch die nötigen Rechte vorhanden sind, dann speichert das Skript die gelesenen Daten in einer Datei im aktuellen Verzeichnis ab (standardmäßig ist das die Datei newFiles.txt). Je nach Festplatte und Filesystem kann die Datei 50 oder 100 MB groß werden. Wird das Skript anschließend im gleichen Verzeichnis noch einmal gestartet, dann benennt es die Datei des letzten Programmlaufs um und erstellt eine neue. Anschließend führt es einen Vergleich der Daten beider Dateien durch und schreibt für neu hinzugekommene, für geänderte und für gelöschte Dateien und Verzeichnisse jeweils eine Datei, in der die Pfade der betroffenen Einträge im Dateibaum stehen. Als Dateiname für die Daten des letzten Laufs wird oldFiles.txt verwendet.

402

7

Anwendungsbeispiele

Die Daten für neu hinzugekommene Dateien oder Verzeichnisse ist addedFiles.txt, für gelöschte Objekte wird deletedFiles.txt verwendet, und Änderungen schreibt das Skript in die Datei changedFiles.txt. Man sollte das Skript nicht unterhalb eines der zu durchsuchenden Verzeichnisse starten, da sonst die Datendateien des Skripts ebenfalls in den Vergleich mit aufgenommen werden. Wenn man das Skript ein drittes Mal startet, dann benennt es die Datei des vorletzten Laufs um und macht sozusagen einen Backup der Daten dieses Laufs. Nun wissen Sie, wie das Skript vom Prinzip her arbeitet, und jetzt folgt der Programmcode: #!D:/Perl/bin/perl.exe -w # Skript, mit dem man Veränderungen in Dateibäumen # verfolgen kann use strict; use FileHandle; use File::Find (); # Hash für die aktuell von der Festplatte gelesenen # Dateien und Verzeichnisse our $newFiles = {}; # Pfad für die Datendatei, in welche die gelesenen # Daten geschrieben werden our $newPath = "newFiles.txt"; # Hash für die bereits früher gelesenen Daten. # Es wird verwendet, um einen Abgleich mit den # aktuellen Daten durchzuführen. our $oldFiles = {}; # Pfad für die Datendatei, aus welcher die abgespeicherten # Daten gelesen werden our $oldPath = "oldFiles.txt"; # Zähler für die Anzahl der gelesenen bzw. bearbeiteten # Einträge our $nfiles = 0; # Hash, das diejenigen Dateien und Verzeichnisse aufnimmt, # die seit dem letzten Lauf hinzugekommen sind our $addedFiles = {}; # Pfad für die Datendatei our $addedPath = "addedFiles.txt"; # Hash, das die gelöschten Objekte seit dem letzten

Dateibäume verwalten # Lauf aufnimmt our $deletedFiles = {}; # Pfad für die Datendatei our $deletedPath = "deletedFiles.txt"; # Hash, das die seit dem letzten Lauf geänderten Dateien # aufnimmt our $changedFiles = {}; # Pfad für die Datendatei our $changedPath = "changedFiles.txt"; # Dem Skript können ein oder mehrere Startverzeichnisse # als Argumente in der Kommandozeile übergeben werden, # die durchsucht werden sollen. # Ohne Angabe wird das Rootverzeichnis des aktuellen # Filesystems verwendet. Unter UNIX ist dies das # Rootverzeichnis aller Filesysteme, unter Windows # ist es das oberste Verzeichnis des aktuellen Laufwerks. my @startDirs = (); foreach my $arg ( @ARGV ) { if ( -d $arg ) { push( @startDirs, $arg ); } } unless ( @startDirs ) { @startDirs = ( "/" ); } # Falls schon eine Datei für bereits gelesene Dateien # existiert, wird diese gesichert. An den Dateinamen wird # das aktuelle Datum angehängt. if ( -f $oldPath ) { my ( $secs, $mins, $hours, $mday, $mon, $year ) = localtime(); $year += 1900; $mon++; my $dateString = sprintf( "%d_%02d_%02d_%02d_%02d_%02d", $year, $mon, $mday, $hours, $mins, $secs ); my $backupPath = "$oldPath.$dateString"; unless ( rename( $oldPath, $backupPath ) ) { print( STDERR "kann Datei $backupPath nicht sichern\n" ); exit( 1 ); } } # Wenn das Skript bereits aufgerufen wurde, wird die # Datendatei des letzten Laufs, in der die aktuellen # Daten des Laufs abgelegt wurden, umbenannt.

403

404

7

if ( -f $newPath ) { unless ( rename( $newPath, $oldPath ) ) { print( STDERR "kann Datei $newPath nicht umbenennen\n" ); exit( 1 ); } } # Es werden die aktuellen Daten der Festplatte eingelesen unless ( readNewFiles( @startDirs ) ) { print( STDERR "kann Dateien nicht von FP lesen\n" ); exit( 1 ); } # Die gelesenen Daten werden in der Datei abgespeichert. unless ( writeNewFiles() ) { print( STDERR "kann neue Dateiliste nicht schreiben\n" ); exit( 1 ); } # Falls keine Vergleichsdatei eines früheren Laufs existiert, # sind wir nun mit der Arbeit fertig. unless ( -f $oldPath ) { exit( 0 ); } # Wir lesen die Daten des letzten Laufs unless ( readOldFiles() ) { print( STDERR "kann alte Daten nicht einlesen\n" ); exit( 1 ); } # Nun werden die Daten des letzten und des aktuellen # Datenbestands verglichen process(); # Alle neu hinzugekommenen Dateien und Verzeichnisse # schreiben. unless ( writeFiles( $addedFiles, $addedPath ) ) { print( STDERR "kann neu hinzugekommene Dateien ", "nicht schreiben\n" ); exit( 1 ); } # Alle gelöschten Dateien und Verzeichnisse # schreiben. unless ( writeFiles( $deletedFiles, $deletedPath ) ) { print( STDERR "kann geloeschte Dateien ", "nicht schreiben\n" );

Anwendungsbeispiele

Dateibäume verwalten exit( 1 ); } # Alle geänderten Dateien und Verzeichnisse # schreiben. unless ( writeFiles( $changedFiles, $changedPath ) ) { print( STDERR "kann geaenderte Dateien ", "nicht schreiben\n" ); exit( 1 ); } exit( 0 ); # Funktion, mit welcher die Vergleichsdaten geschrieben # werden sub writeFiles { my ( $href, $path ) = @_; my $fh = new FileHandle( $path, "w" ); unless ( $fh ) { return undef; } foreach my $path ( keys( %{ $href } ) ) { $fh->print( "$path\n" ); } $fh->close(); return 1; } # Die aktuellen Daten der Festplatte lesen und im # Hash ablegen sub readNewFiles { $nfiles = 0; File::Find::find( { wanted => \&addNewFile, no_chdir => 1, }, @_ ); print( "insgesamt $nfiles Dateien von FP gelesen\n" ); } # Die Daten des letzten Laufs aus der Datei lesen und # im Hash abspeichern sub readOldFiles { my $fh = new FileHandle( $oldPath, "r" ); unless ( $fh ) { return undef; } $nfiles = 0;

405

406

7

Anwendungsbeispiele

while ( defined( my $line = $fh->getline() ) ) { chomp( $line ); if ( $line =~ m~^([^\t]+)\t([^\t]+)\t([^\t]+)\t(.+)~ ) { $nfiles++; unless ( $nfiles % 1000 ) { printf( "%10d alte Dateien gelesen\n", $nfiles ); } $oldFiles->{ $1 } = {}; my $hr = $oldFiles->{ $1 }; ( $hr->{ type }, $hr->{ size }, $hr->{ mtime }, ) = ( $2, $3, $4 ); } } $fh->close(); print( "insgesamt $nfiles alte Dateien gelesen\n" ); return 1; } # Vergleichen der Daten aus dem letzten und dem aktuellen # Datenbestand sub process { unless ( %{ $newFiles } and %{ $oldFiles } ) { return undef; } $nfiles = 0; foreach my $path ( keys( %{ $newFiles } ) ) { $nfiles++; unless ( $nfiles % 1000 ) { printf( "%10d Dateien der FP bearbeitet\n", $nfiles ); } unless ( exists( $oldFiles->{ $path } ) ) { $addedFiles->{ $path } = 1; next; } if ( ( $newFiles->{ $path }->{ mtime } != $oldFiles->{ $path }->{ mtime } ) or ( $newFiles->{ $path }->{ size } !=

Dateibäume verwalten

407 $oldFiles->{ $path }->{ size }

) ) { $changedFiles->{ $path } = 1; } } print( "insgesamt $nfiles Dateien der FP bearbeitet\n" ); $nfiles = 0; foreach my $path ( keys( %{ $oldFiles } ) ) { $nfiles++; unless ( $nfiles % 1000 ) { printf( "%10d alte Dateien bearbeitet\n", $nfiles ); } unless ( exists( $newFiles->{ $path } ) ) { $deletedFiles->{ $path } = 1; } } print( "insgesamt $nfiles alte Dateien bearbeitet\n" ); return 1; } # Die aktuellen Daten der Festplatte in der Datei # ablegen sub writeNewFiles { my $fh = new FileHandle( $newPath, "w" ); unless ( $fh ) { return undef; } foreach my $path ( keys( %{ $newFiles } ) ) { my $entry = $newFiles->{ $path }; $fh->print( "$path\t", $entry->{ type }, "\t", $entry->{ size }, "\t", $entry->{ mtime }, "\n" ); } $fh->close(); return 1; }

408

7

# Einen Eintrag aus dem Dateibaum der Festplatte # im Hash ablegen. Jeder Eintrag wird in einem # Subhash gespeichert. Der Pfadname des Eintrags # dient dabei als Key. sub addNewFile { my $path = $File::Find::name; $newFiles->{ $path } = {}; my $hr = $newFiles->{ $path }; # Dateigröße und Datum der letzten Änderung # speichern ( $hr->{ size }, $hr->{ mtime }, ) = ( stat( $path ) )[ 7, 9 ]; # Dateityp ablegen if ( -d $path ) { $hr->{ type } = "d"; } elsif ( -f $path ) { $hr->{ type } = "f"; } elsif ( -b $path ) { $hr->{ type } = "b"; } elsif ( -c $path ) { $hr->{ type } = "c"; } elsif ( -l $path ) { $hr->{ type } = "l"; } elsif ( -p $path ) { $hr->{ type } = "p"; } elsif ( -S $path ) { $hr->{ type } = "S"; } else { $hr->{ type } = ""; } $nfiles++; unless ( $nfiles % 1000 ) { printf( "%10d Dateien von FP gelesen\n", $nfiles ); } } 1;

Anwendungsbeispiele

Dateibäume verwalten

409

Ich hoffe, das Skript so kommentiert zu haben, dass es einigermaßen verständlich ist. Aber noch einmal der Hinweis: Das Skript kann sehr lange Zeit in Anspruch nehmen, wenn es das gesamte Filesystem der Festplatte durchsuchen muss. Deshalb habe ich in den Programmcode auch Ausgaben eingebaut, die jeweils nach 1000 Einträgen aktiviert werden. Damit sehen Sie, dass das System nicht steht, sondern noch etwas Sinnvolles tut.

8 CGI In diesem Kapitel lernen Sie, wie man effektiv Webanwendungen mit Perl erstellt. Ich setze voraus, dass Sie bereits Erfahrungen mit HTML gemacht haben. Zu Beginn des Kapitels werde ich Sie mit den Grundlagen des WWW (World Wide Web) vertraut machen, damit Sie nicht verzweifeln, wenn Ihnen Begriffe wie "Request", "Response", "Cookie", "URI" oder "Querystring" um die Ohren sausen. Auch das http-Protokoll, das die Grundlage der Kommunikation zwischen Webclient und Webserver ist, werden Sie bei der Gelegenheit näher kennen lernen. Am Ende des Kapitels werden Sie in der Lage sein, komplexe CGI-Anwendungen mit Unterstützung von Templates zu erstellen. Hier zunächst ein paar Grundbegriffe:

Webclient Wenn Sie zu Hause an Ihrem PC sitzen und im Internet surfen oder die lästigen Rechnungen per Online-Banking bezahlen, dann benutzen Sie dazu meist einen Browser. Das ist ein ganz normales Programm mit einer eigenen Oberfläche, über die das Internet zu Ihnen ins Haus kommt. Dieser Browser ist der Webclient oder kurz Client, es ist sozusagen der »Kunde«, der von einem Informationsanbieter etwas möchte. Die beiden am häufigsten benutzten Vertreter der Gattung »Browser« sind zurzeit der Navigator von Netscape (bzw. »Mozilla«) und der Microsoft Internet Explorer. Daneben gibt es aber eine Reihe weiterer Hersteller von Browsern.

Webserver Der Webserver oder kurz Server ist ein Programm irgendwo im Internet (oder auch bei Ihnen auf dem PC), das auf Anfragen von Webclients wartet und diese beantwortet. Der am häufigsten eingesetzte Webserver weltweit ist im Sourcecode kostenlos verfügbar und kommt von der so genannten »Apache Group«. Natürlich bieten auch andere Hersteller wie zum Beispiel Netscape, IBM oder Microsoft eigene Produkte für Geld an, doch oft sind diese Produkte nur angepasste Derivate des Apache-Webservers. In allen meinen Beispielen werde ich mich daher auf den Apache-Webserver beziehen.

412

8

CGI

Request Dieser Begriff wird ins Deutsche übersetzt mit »Anforderung«. Wann immer Sie im Browser die WWW-Adresse eines Webservers eintippen oder einen Eintrag aus den Bookmarks (zu Deutsch: »Lesezeichen«) des Browsers benutzen, setzt dieser einen Request an einen Webserver ab, mit dem ein Dokument vom Server angefordert wird. Der Inhalt dieses Dokuments wird dann im Browser angezeigt.

Response Unter diesem Begriff, der mit »Antwort« ins Deutsche übersetzt wird, versteht man das, was ein Webserver an den Client zurückschickt, nachdem er einen Request von diesem erhalten hat.

URI Unter einem URI (Uniform Resource Identifier) versteht man eine beliebige InternetRessource, sozusagen die Adresse eines Internet-Dokuments. Im einfachsten Fall ist ein Internet-Dokument eine einfache HTML-Seite, es kann sich aber auch um eine Audio-, Video- oder Bilddatei handeln. Hinweis: Neben diesem Begriff werden Sie des Öfteren das Wort »URL« (Uniform Resource Locator) sehen. Beide Begriffe sind nahezu identisch, nur ist die Spezifikation eines »URI« später entstanden und erweitert die des »URL«. Ich möchte nicht versuchen, die Abkürzungen ins Deutsche zu übersetzen, ganz einfach deshalb, weil es nicht nötig ist. Sie werden immer nur die Abkürzungen verwendet sehen. Für Ungeduldige: Das, was Sie in die Adresszeile im Browser eintippen, ist ein URI. Deshalb nennt man diese Zeile im Browser auch »URI-Zeile«. Dann sehen wir uns einmal an, was man sich unter einem URI vorzustellen hat. URI-Syntax (Angaben in eckigen Klammern sind optional): protocol://host[:port][path][?querystring]

Sieht doch schon recht verwirrend aus, oder? Aber keine Angst, wir gehen alle Unbekannten der Reihe nach durch: protocol ist der Name des zu verwendenden Internet-Protokolls. Immer dann, wenn sich zwei Programme miteinander unterhalten, in unserem Fall sind dies Webclient und Webserver, müssen die beiden die gleiche Sprache sprechen, sonst klappt es nicht mit der Kommunikation. Für jede Art des Datenaustauschs über das Internet wurde deshalb eine Protokollspezifikation definiert, an die sich sowohl Client als auch Server halten müssen.

413

Zur Zeit werden folgende Internet-Protokolle verwendet: 왘 http (Standard-Protokoll für WWW auf Port 80) 왘 file (lokale Datei im Filesystem des Webclients) 왘 https (HTTP-Protokoll mit Verschlüsselung, Standard-Port 443) 왘 mailto (Protokoll für E-Mail) 왘 ftp (Protokoll für Datei-Transfer, Standard-Port 21) 왘 news (Newsreader-Protokoll NNTP, Standard-Port 119) 왘 telnet (Telnet-Protokoll, Standard-Port 23) 왘 wais (WideAreaInformationServer-Protokoll, Standard-Port 210) 왘 gopher (GOPHER-Protokoll, Standard-Port 70) »://« ist ein Trenner, der immer nach dem Protokollnamen stehen muss. host ist entweder der Name des Webservers (z.B. »www.nowhere.com«) oder dessen IPAdresse (zum Beispiel »162.13.17.6«). port ist optional. Falls angegeben, muss er durch einen Doppelpunkt vom host getrennt sein. »Was ist ein Port?« Diese Frage haben viele Einsteiger. Deshalb möchte ich diesen Begriff genauer erklären. Alle Serverprogramme auf einem Rechner müssen von Clients eindeutig erreichbar sein. Zu diesem Zweck hat man numerische Adressen in Form von Ports erfunden. Wenn zum Beispiel ein Webserver auf einem Rechner gestartet wird, meldet sich das Programm beim Betriebssystem mit der numerischen Adresse 80 an und ist weltweit unter der Angabe des Rechnernamens und dieser Adresse erreichbar. Das bedeutet natürlich, dass auf einem Rechner nicht zwei Programme mit demselben Port gleichzeitig laufen können. Versuchen Sie zum Beispiel, den Webserver ein zweites Mal zu starten, dann erhalten Sie eine Fehlermeldung, weil der Port bereits belegt ist. Ports sind Zahlen zwischen 1 und 65535 und geben den Serverdienst genauer an, bei HTTP ist die Portnummer 80 als Standard-Port für Webserver festgelegt worden, d.h., man muss im URI nur dann einen Port angeben, wenn der Webserver keinen Standard-Port verwendet (z.B. 8080). Das gilt im übrigen für alle Serverdienste, bei denen Standard-Ports definiert wurden (siehe auch die obige Liste der Internet-Protokolle). Noch ein Hinweis für UNIX: In UNIX sind die Ports im Bereich von 1 bis 1023 für weltweit fest definierte Serverdienste reserviert, man nennt diese auch »Privileged Ports«. Programme, die einen Port aus diesem Bereich benutzen, können deshalb nur unter der UNIX-Kennung root gestartet werden.

414

8

CGI

path ist ein virtueller Pfad (der immer mit einem Slash beginnen muss), unter welchem der angegebene Webserver das Dokument (oder Bild oder Programm) in seinem Filesystem sucht. Der Pfad im Filesystem des Webservers kann durchaus völlig verschieden sein vom virtuellen Pfad. Man kann den Pfad auch ganz weglassen, dann verwendet der Webserver einen Default-Pfad, der meist auf die Homepage (Einstiegsseite des Webdienstes) zeigt. Der Pfad kann zusätzlich das Zeichen "#" gefolgt von einem Identifier enthalten. In diesem Fall wird eine bestimmte Textstelle innerhalb des angeforderten Dokuments adressiert. Diese Identifizierung einer bestimmten Textstelle innerhalb eines HTMLDokuments nennt man auch »Anchor«, was zu Deutsch so viel wie »Anker« heißt. Zwei typische Beispiele für den virtuellen Pfad sind: /index.html # Damit wird die Datei "index.html" im virtuellen # Rootverzeichnis des Webservers angefordert. # Wenn das Rootverzeichnis für Dokumente des # Webservers ("DocumentRoot") z.B. das Verzeichnis # "/usr/local/httpd/htdocs" ist, dann würde die # Datei "/usr/local/httpd/htdocs/index.html" # angefordert werden. / # # # # #

Mit diesem virtuellen Pfad wird keine Datei, sondern ein Verzeichnis angesprochen, in unserem Fall das Rootverzeichnis für Webdokumente. Der Webserver hängt in diesem Fall implizit einen Default-Dateinamen an (z.B. "index.html").

querystring enthält Informationen des Clients an den Server. Durch Anhängen eines Fragezeichens am Ende des virtuellen Pfads kann man dem Webserver Daten in Form von Key/Value-Paaren senden, die in einem Querystring verpackt werden müssen. Dabei gilt folgende Regel: Die Key/Value-Paare müssen durch ein Kaufmännisches Und (&) getrennt werden, der Key wird durch ein Gleichheitszeichen (=) vom Value getrennt. Ein Key/Value-Paar entspricht einer Variable, wobei Key der Name der Variable ist, und Value der Wert. Sowohl der Key als auch der Value jedes Paares muss so codiert sein, dass alle Zeichen, die nicht im Zeichenbereich [A-Za-z0-9_./-] liegen, durch ihren zweistelligen Hexcode mit vorangestelltem Prozentzeichen % angegeben werden (z.B. wird aus der eckigen Klammer [ der codierte Wert %5B). Wir werden die Regeln im Weiteren noch anhand einiger Beispiele erläutern.

415

Zur Demonstration möchte ich Ihnen einige Beispiele für URIs zeigen: URI

Bedeutung

http://www.siemens.de

Es wird der Webserver des Rechners www.siemens.de über den StandardPort 80 des HTTP-Protokolls angesprochen. Die Angabe mit einem Slash am Ende sagt dem Server: Suche in deinem Rootverzeichnis für Webdokumente nach der Einstiegsseite (Slash am Ende eines URIs bedeutet also, dass ein Verzeichnis und keine Datei gemeint ist). Die erste Angabe wird genauso behandelt, als hätte man am Ende einen Slash angegeben.

http://www.siemens.de/

http://www.siemens.de/ welcome.html

Wie oben, jedoch wird die Datei welcome.html im Rootverzeichnis für Webdokumente angefordert.

http://www.siemens.de/ d1/d2/

Es wird die Einstiegsseite im Unterverzeichnis d1/d2 des Rootverzeichnisses für Webdokumente angefordert.

http://www.no.de:8080/ doc.htm#a1

Der Webserver auf Port 8080 des Rechners www.no.de wird aufgefordert, die Datei doc.htm in seinem Rootverzeichnis für Webdokumente zu liefern. Dem Browser des Clients wird mitgeteilt, dass man an die Textstelle springen möchte, die mit dem Identifier a1 markiert ist.

http://www.no.de/cgi/ mycgi?login=local%20guest

Es wird das Dokument (in diesem Fall ein Skript bzw. Programm) im Unterverzeichnis cgi angesprochen, dabei wird über den Querystring eine Variable namens login übergeben, die als Wert local guest hat. Das Blank muss dabei durch seinen Hexcode 0x20 mit vorangestelltem Fragezeichen angegeben sein. Der Webserver kann zwischen normalen Dokumenten und Programmen unterscheiden (meist liegen Programme in einem anderen Verzeichnis). Variablen im Querystring werden vom Webserver an die Programme weitergereicht, welche diese dann verarbeiten können. Die Regeln für das Aufrufen von Programmen (bzw. Skripts) auf dem Webserver nennt man CGI (Common Gateway Interface).

file:///C|/temp/index.htm

Hierbei handelt es sich um eine lokale Datei des Clients. Das zugehörige Protokoll heißt in diesem Fall file, der Pfad ist /C|/temp/index.htm. Man beachte den führenden Slash und den senkrechten Strich (Bar) anstelle des Doppelpunkts für das Laufwerk. Außerdem wird im URI generell der Slash als Verzeichnistrenner verwendet, anders als in Windows, wo die Datei unter C:\temp\index.htm zu finden wäre.

ftp://user:pwd@ftp. siemens.de/pub/

FTP-Zugriff mit Angabe des Users und des Kennworts auf dem Rechner ftp.siemens.de. Es wird die Einstiegsseite im Unterverzeichnis pub des FTP-Servers geladen.

ftp://ftp.siemens.de

Anonymer FTP-Zugriff ohne Angabe eines Users auf das Rootverzeichnis des FTP-Servers ftp.siemens.de. Implizit sendet der Browser den speziellen Usernamen anonymous und ein leeres Kennwort. Es wird die Einstiegsseite des Rootverzeichnisses geladen.

mailto://[email protected]

Es wird eine E-Mail an die Adresse [email protected] gesendet. Meist wird vom Browser bei einem Verweis dieses Formats ein eigenes E-MailFenster geöffnet, bei dem die To-Adresse mit der angegebenen E-MailAdresse vorbelegt ist.

416

8

CGI

CGI CGI steht für Common Gateway Interface, das es ermöglicht, auf dem Server anstelle von statischen HTML Seiten dynamisch erstellten Inhalt (englisch: »content«) beliebiger Art individuell für jeden Client-Request zusammenzustellen und in der Response zu senden. Die Arbeit des dynamischen Zusammenstellens von Inhalten erledigen so genannte »CGI-Skripts«, das können Binärprogramme, Shell-Skripts oder auch Perl-Skripts sein. Meist liegen CGI-Skripts in einem eigenen Verzeichnisbaum (beim Apache-Webserver ist dies per Default das Unterverzeichnis cgi-bin), grundsätzlich können CGI-Skripts jedoch an beliebigen Stellen des Webservers abgelegt sein. CGI-Skripts sind zum Beispiel immer dann im Spiel, wenn HTML Formulare verarbeitet werden müssen. Hier noch einmal der Unterschied zwischen statischen HTML-Seiten und CGI, damit es sich auch wirklich im Gedächtnis einprägt: Wenn eine statische HTML-Seite angefordert wird, sucht der Webserver die entsprechende Datei im Filesystem und liefert den Inhalt dieser Datei an den Client. Das war’s. Ruft der Client aber ein CGI-Skript auf, dann passiert viel mehr. Neben mehreren Prozessen wie Shell und z.B. Perl-Interpreter wird am Ende der Kette ein Skript ausgeführt, das jetzt einen beliebigen Inhalt an den Client senden kann. Der große Unterschied zu statischen Seiten ist aus der Sicht des Clients, dass der Inhalt, den er zurückbekommt, jedes Mal ein anderer sein kann. Bevor wir die Eingeweide von CGI öffnen, müssen wir einige grundsätzliche Basisbegriffe und Technologien verstehen lernen.

8.1 Das HTTP-Protokoll Wenn ein Webbrowser sich mit einem Webserver unterhält, um z.B. irgendein Dokument von ihm anzufordern, dann müssen beide die gleiche Sprache, sprich dasselbe InternetProtokoll sprechen. In diesem Fall handelt es sich um das Hyper Text Transfer Protocol, kurz HTTP. Anhand des folgenden Schaubildes wollen wir den Aktionsfluss von HTTP genauer betrachten: Zunächst haben sowohl Client als auch Server keinerlei Verbindung zueinander (Zustand 1). Nun baut der Client eine Verbindung zum Server auf und fordert einen URI von diesem an. Diese Anforderung nennt man »Request« (Zustand 2).

Das HTTP-Protokoll

417



 

   

 

   

 

 

 



Abbildung 8.1: Aktionsfluss bei einem Webzugriff mit HTTP

Der Server muss nun entsprechend des URI, der Bestandteil der Anforderung vom Client ist, in seinem Filesystem nach dem Dokument suchen und den Inhalt als Antwort senden. Dies bezeichnet man als »Response« (Zustand 3). Nach dem Versenden der Antwort vom Server bauen beide Partner die Verbindung wieder ab und befinden sich wieder im selben Zustand wie unter Punkt 1. Wichtig für das Verständnis der Kommunikation zwischen Webclient und Webserver ist die Tatsache, dass es sich beim HTTP-Protokoll um ein zustandsloses Protokoll handelt. Client und Server haben nur für die Dauer eines Requests und der Beantwortung dieses Requests in Form einer Response-Verbindung miteinander, anschließend kennen sie sich nicht mehr, d.h., der Server weiß bei einem Folgerequest desselben Clients nicht, dass er diesen bereits einmal bedient hat. Meist hat der Inhalt einer Internetseite viele einzelne Bestandteile (z.B. Bilder), die jeweils in einem eigenen Request übertragen werden. Für den Server besteht also eine solche Seite nicht als einziges Objekt, sondern ist nur eine Anzahl nacheinander eingehender Requests eines Client, die nichts miteinander zu tun haben.

418

8

CGI

Wir werden auf diese Eigenschaft später noch genauer zu sprechen kommen. Seit der Version 1.1 des HTTP-Protokolls kann die Verbindung zwischen Client und Server auch über mehrere Requests aufrecht erhalten werden, dies dient der Performancesteigerung, meist jedoch wird die ältere Variante (pro Request ein Verbindungsaufbau) verwendet. Aber auch in der etwas neueren Variante bleibt die Tatsache gültig, dass jeder Folgerequest ein und desselben Client für den Server eigenständig ist. Die Einzelrequests stehen miteinander nicht in Verbindung. Sowohl der Request vom Client als auch die Response vom Server werden im HTTPProtokoll in zwei Teile gespalten (das ist speziell bei der Antwort des Servers durch ein CGI-Skript von Bedeutung), den HTTP-Header und den Content. Während der Content dem Inhalt des angeforderten Dokuments entspricht (das sieht der Anwender im Browser), enthält der Header so genannte META-Informationen. Das sind Daten, die entweder die Anforderung des Clients oder die Antwort des Servers näher beschreiben. In beiden Fällen wird der Header vom Content durch eine Leerzeile getrennt.

8.1.1 Der Request Der Request wird vom Webclient aktiv gestartet und enthält die Anforderung eines URIs an den Webserver. Jeder Clientrequest muss mindestens folgende Zeile besitzen: Method URI HTTP-Version

Method ist die Aktion, welche der Client ausführen möchte. Die gebräuchlichsten Aktionen sind: 왘 GET Diese Methode sagt: Gib mir bitte den Inhalt des folgenden URIs. Normalerweise wird die Methode GET nur dazu verwendet, ein Dokument vom Server an den Client zu senden, die Datenrichtung geht also in Richtung Client. Sendet der Client mit der GET-Methode jedoch Daten an den Server (Vorsicht bei HTMLFormularen, diese haben als Default-Methode GET, nicht POST), so werden diese im so genannten Querystring mit dem Fragezeichen (?) als Trenner in einer speziellen Codierung an den URI angehängt. Bei HTML-Formularen, die mit der GET-Methode übertragen werden, schreibt der Browser alle Daten des Formulars in die URI-Zeile des Browserfensters. Die Daten sind somit für den Anwender sichtbar. (Wichtig zu wissen, wenn mit dem Formular persönliche Daten, vor allem Kennwörter, abgeschickt werden.) Möchte man zum Beispiel die Parameter login mit dem Wert dummy und pwd mit dem Wert dummypwd an den Server übertragen, so muss man in der URI-Zeile des Brow-

Das HTTP-Protokoll

419

sers hinter dem eigentlichen URI (hier als Beispiel /cgi-bin/t.pl) den Querystring wie folgt anhängen: /cgi-bin/t.pl?login=dummy&pwd=dummypwd

Wie man sieht, werden die einzelnen Querystring-Parameter durch das Zeichen »&« voneinander getrennt. Zwischen Parametername und Parameterwert muss ein Gleichheitszeichen (=) als Trenner stehen. Kommen im Parameternamen oder Parameterwert Sonderzeichen vor (URI-Sonderzeichen sind alle Zeichen außerhalb der Zeichenklasse [\w/.-]), dann müssen diese Zeichen codiert werden, indem statt des Zeichens der zweistellige Hexcode mit vorangestelltem Prozentzeichen (%) angegeben wird. Beispiel: Aus hallo hallo[ wird hallo%20hallo%5B. 왘 HEAD Mit dieser Methode will der Client nicht den Inhalt einer Seite haben, sondern nur Verwaltungsinformation (META-Daten) des URIs, z.B. eine Aussage darüber, ob sich die Seite seit einem bestimmten Zeitpunkt geändert hat. Diese Methode erlaubt in Verbindung mit einem Cache, dass Seiten, die sich nicht geändert haben, nicht noch einmal über die Leitung geschickt werden, wenn der Client die Daten bereits im Cache hat. 왘 POST Diese Methode verwendet der Client, wenn er Formulardaten von HTML-Formularen, die das Attribut TYPE des Formulars auf den Wert POST gesetzt haben, an den Server senden möchte. In den meisten Fällen handelt es sich dann beim URI um ein CGI-Skript, das die gesendeten Daten in Empfang nimmt und verarbeitet. Die Datenrichtung bei POST geht also vom Client zum Server. Per Default (ohne Angabe des Attributs TYPE) verwendet der Browser beim Versand von HTML-Formularen die GET-Methode. URI in der Requestzeile ist entweder ein absoluter URI wie weiter oben beschrieben oder ein absoluter virtueller Pfad (also Teil eines URIs) auf dem Server. Im Falle eines absoluten virtuellen Pfads muss der Client eine zusätzliche Headerzeile senden, in welcher er mitteilt, auf welchen Rechner sich der virtuelle Pfad bezieht. (Dies wird meist in Zusammenhang mit Proxyservern benutzt.) Beispiel für einen absoluten URI: http://www.nowhere.com/de/index.html

Beispiel für einen absoluten virtuellen Pfad: /de/index.html

420

8

CGI

HTTP-Version ist ein String wie z.B. »HTTP/1.1« (für die Protokoll-Version 1.1) und sagt dem Server, welche HTTP-Protokollversion der Client benutzt. Der Client kann weitere Headerzeilen der Form Key: Value

senden, die Zusatzinformationen für den Server enthalten und den Request näher beschreiben. Key ist dabei der Name des Request-Headers, Value der Wert für den angegebenen Request-Header. Zwischen Key und Value muss ein Doppelpunkt stehen. Es darf jeweils nur ein Request Header pro Zeile angegeben sein. Die häufigsten Request-Header sind: 왘 Accept Mit diesem Request Header teilt der Client dem Server mit, welche Art von Dokumenttypen er gerne hätte. Fehlt dieser Request-Header, dann nimmt der Server an, dass der Client alle Dokumenttypen akzeptiert. Dokumenttypen werden als MIME-TYPEs bezeichnet und haben die Form type/ subtype (z.B. text/plain, text/html, image/gif, audio/*). type gibt die generelle Art des Dokuments an, subtype eine spezifische Unterart. text/html heißt also, es handelt sich um ein Textdokument im HTML-Format. Ein Sternchen (»*«) sagt aus, dass an seiner Stelle etwas Beliebiges stehen kann, z.B. bedeutet die Angabe audio/*, dass es sich um irgendeine Art von Audiodaten handelt.

Mehrere MIME-TYPES können durch Kommata getrennt hintereinander angegeben sein. Nach einem MIME-TYPE kann, durch ein Semikolon getrennt, ein so genannter Qualityfactor stehen, der in einer relativen Prozentzahl angibt, welche Qualität der MIME-TYPE haben muss. Z.B. sagt audio/wav; q=0.4, audio/*; q=0.2, audio/basic aus: Ich hätte gerne den MIME-TYPE audio/basic; wenn dieser MIME-TYPE nicht vorhanden ist, dann gib mir audio/wav, und wenn dieser Typ auch nicht vorhanden ist, dann gib mir irgendetwas, das nach audio aussieht. Wenn kein Qualityfactor angegeben ist, dann gilt q=1. 왘 Accept-Charset Mit diesem Request-Header informiert der Client den Server, welche Zeichencodierungen er in dem Antwortdokument akzeptiert. Beispiel: Accept-Charset: iso-8859-1, unicode-1-1;q=0.8 bedeutet:

Am liebsten wäre mir der ISO-Zeichensatz, aber ich gebe mich auch mit Unicode zufrieden.

Das HTTP-Protokoll

421

왘 Accept-Encoding Mit diesem Request-Header teilt der Client dem Server mit, welche speziellen Codierungen er für die Antwort akzeptiert, z.B.: Accept-Encoding: compress, gzip;q=0.5 bedeutet:

Ich akzeptiere vorwiegend Daten im »compress«-Format, komme aber auch mit gzip klar. 왘 Accept-Language Damit sagt der Client aus, welche Sprachvarianten er in der Antwort des Servers akzeptiert, z.B.: Accept-Language: de, en, en-gb; q=0.8 bedeutet:

Ich bevorzuge Deutsch oder Englisch, akzeptiere aber auch britisches Englisch. 왘 If-Modified-Since Dieser Request-Header sagt dem Server: Schicke mir den Inhalt des URIs nur dann, wenn er sich seit dem angegebenen Zeitpunkt verändert hat, z.B.: If-Modified-Since: Thu, 18 Oct 2001 13:17:03 GMT

Der Server sendet nur dann den Inhalt des gewünschten Dokuments, wenn dieser sich seit dem 18.10.2001, 13:17 Uhr und 3 Sekunden verändert hat. Die Zeitzone ist dabei immer GMT (Greenwich Mean Time), diese hinkt der deutschen Winterzeit um 1 Stunde nach, der deutschen Sommerzeit um 2 Stunden. Für den Fall, dass sich das Dokument seit diesem Zeitpunkt nicht verändert hat, sendet der Server einen entsprechenden Response-Header, um dies dem Client mitzuteilen. Das Datum muss für den Wochentag und für den Monat die englische Schreibweise verwenden (z.B. »DEC« für Dezember, »THU« für Donnerstag). 왘 User-Agent Dieser Request-Header identifiziert den Client (in den meisten Fällen ist das der Webbrowser) durch einen String. Der Netscape Navigator z.B. meldet sich mit User-Agent: Mozilla... Der genaue Text hängt vom Browser und der Version ab, hier möchte ich nur andeuten, dass beim Netscape Navigator nicht das Wort Netscape im Identifizierungsstring vorkommt, sondern der String »Mozilla«. 왘 Cookie Dieser Header wird verwendet, um eine persistente Session zwischen Client und Server aufzubauen. Siehe hierzu die Beschreibung von Cookies weiter unten. Nachdem der Client alle Request-Header gesendet hat, muss er den Request abschließen, indem er eine Leerzeile sendet.

422

8

CGI

Querystring Wenn der Client mit der HTTP-Methode GET Daten an der Server sendet, zum Beispiel beim Abschicken eines HTML-Formulars oder durch explizite Eingabe im URI-Eingabefeld des Browsers durch Anhängen eines Fragezeichens an den URI, dann müssen alle Zeichen, die nicht der Zeichenklasse [A-Za-z0-9.-/]

angehören, mit dem zweistelligen Hexcode des Zeichens und vorangestelltem Prozentzeichen (%) codiert sein. Die Daten im Querystring bestehen aus Key/ValuePaaren (Variablenname bzw. Wert der Variablen), die durch ein Kaufmännisches Und (&) voneinander getrennt sind. Der Key einer CGI-Variable wird durch ein Gleichheitszeichen (=) vom Value getrennt. Beispiel: Der Anwender möchte dem URI »/cgi-bin/myScript.pl« auf dem Webserver localhost (das ist der eigene Rechner) folgende Formularfelder mit der Request-Methode GET übergeben: firstname: "Egon Hugo", lastname: "Müller-Überflieger".

Die URI-Zeile des Browsers muss wie folgt aussehen (aufgrund der Länge des Strings kann es im Buch zu einem Zeilenumbruch kommen): http://localhost/cgi-bin/myScript.pl?firstname=Egon%20Hugo&lastname=M%FCller%DCberflieger

Der Server erhält die vom Client gesendeten Daten in der Umgebungsvariable QUERY_STRING und muss die Daten erst decodieren, bevor sie verwendet werden können. Weiter unten werden wir sehen, wie dies vom Perl-Modul CGI einfach und elegant erledigt wird.

8.1.2 Die Response Nachdem der Webserver den Request vom Client ausgewertet hat, weiß er, welche Aktion er mit dem angegebenen URI durchführen soll. Seine Antwort auf den Request besteht immer aus mindestens einem HTTP-Header, dem optional der Content folgen kann (so wird z.B. bei der Request-Methode HEAD kein Content, sondern nur ein Header an den Client zurückgeschickt). Der HTTP-Header wird durch eine Leerzeile vom eigentlichen Content getrennt. Mehrere HTTP-Header werden durch je einen Zeilenvorschub voneinander getrennt. Zwischen dem letztem HTTP-Header und dem Content der Response muss genau eine Leerzeile stehen.

Das HTTP-Protokoll

423

Der Server muss mindestens folgende Zeile an den Client zurücksenden: HTTP-Version Statuscode Status-String

In der Antwort an einen Browser muss zusätzlich mindestens folgender Header enthalten sein: Content-Type contentType

Hinweis: contentType muss ein gültiger MIME-TYPE sein, z.B. text/html. HTTP-Version ist die Version des vom Webserver benutzten HTTP-Protokolls, heute in den meisten Fällen die Version 1.1, also lautet der String »HTTP/1.1«. Statuscode ist ein dreistelliger numerischer Code für den Status der Antwort vom Server. Status-String ist ein Text, der den numerischen Code kurz beschreibt, z.B. »OK«. Die Codes sind jeweils in Hundertergruppen mit folgenden Bedeutungen unterteilt: 왘 1xx Codes von 100 bis 199 sind rein informeller Natur und bedeuten, dass der Server den Request entgegengenommen hat und ihn weiterbearbeitet. 왘 2xx Codes von 200 bis 299 bedeuten, dass der Request erfolgreich bearbeitet wurde. 왘 3xx Codes von 300 bis 399 bedeuten, dass der Request nicht direkt beantwortet werden kann, sondern eine Umleitung auf einen anderen URI erforderlich ist (dies wird Redirect genannt). Der Client muss daraufhin einen neuen Request mit dem in der Redirect-Response angegebenen URI durchführen. 왘 4xx Codes von 400 bis 499 bedeuten einen Clientfehler, der entweder durch eine falsche Syntax im Request zustande gekommen ist oder dadurch, dass der Request nicht bearbeitet werden kann. 왘 5xx Codes von 500 bis 599 kennzeichnen einen Serverfehler. Der Clientrequest ist aber in Ordnung. Liste der allgemein üblichen Statuscodes und deren Beschreibung: 왘 200 OK Der Request wurde erfolgreich bearbeitet.

424

8

CGI

왘 301 Moved Permanently Das angegebene Dokument befindet sich jetzt permanent unter einem anderen URI. 왘 302 Found Das angeforderte Dokument wurde gefunden. 왘 304 Not Modified Das angeforderte Dokument hat sich seit dem angegebenen Datum nicht geändert. 왘 307 Temporary Redirect Das angegebene Dokument ist zeitweilig unter einem anderen URI erreichbar. 왘 400 Bad Request Der Client-Request ist fehlerhaft. 왘 401 Unauthorized Der Client hat keine ausreichenden Privilegien für das angegebene Dokument. 왘 403 Forbidden Der Zugriff auf das angeforderte Dokument wurde verweigert. 왘 404 Not Found Es wurde kein Dokument für den angegebenen URI gefunden. 왘 405 Method Not Allowed Die angegebene Request-Methode ist nicht erlaubt. 왘 500 Internal Server Error Allgemeiner Serverfehler (tritt meist bei CGI-Skripts mit Fehlern auf und ist während der Entwicklungsphase von CGI-Skripts wohl der »beliebteste« von allen). 왘 501 Not Implemented Der Server kann mit der angegebenen Request-Methode nichts anfangen. 왘 503 Service Unavailable Der Server ist im Moment zu sehr ausgelastet oder wird gerade gewartet. Zusätzlich zur Statuszeile kann der Webserver weitere Response-Header senden, jeweils einen Header pro Zeile. Die Syntax ist dieselbe wie beim Request-Header: Key: Value

Im Folgenden wollen wir einige der wichtigsten Response-Header erläutern: 왘 Content-Length Dieser Response-Header gibt die Größe des Inhalts der angeforderten Seite in Bytes an.

Das HTTP-Protokoll

425

왘 Content-Type Dieser Response-Header entspricht dem Request-Header Accept und gibt den MIME-TYPE des angeforderten Dokuments an (z.B. »text/html«). 왘 Content-Encoding Dieser Response-Header entspricht dem Request-Header Accept-Encoding. 왘 Content-Language Dieser Response-Header entspricht dem Request-Header Accept-Language 왘 Expires Der Wert dieses Response-Headers enthält einen Zeitstempel, der besagt ab wann der Inhalt der angeforderten Seite ungültig ist und nicht mehr aus einem Zwischenspeicher (Cache) des Client entnommen werden darf, sondern neu vom Webserver anzufordern ist. Das angegebene Datum muss in dem Format Tue, 15 Nov 1994 08:12:31 GMT

sein (englische Abkürzung für den Wochentag, ebenso englische Abkürzung für den Monat). 왘 Last-Modified Mit diesem Response-Header informiert der Webserver den Client (bzw. dazwischen liegende Zwischenspeicher in Form von Proxyservern), wann der Inhalt der angeforderten Seite zuletzt geändert worden ist. Das angegebene Datum muss in dem Format Tue, 15 Nov 1994 08:12:31 GMT

sein (englische Abkürzung für den Wochentag, ebenso englische Abkürzung für den Monat). 왘 Location Diesen Response-Header verwendet der Server, wenn er dem Client mitteilen möchte, dass die angeforderte Seite einen neuen URI erhalten hat. Der Wert des Headers muss ein absoluter URI sein. 왘 Cache-Control Die am häufigsten verwendeten Werte dieses Response-Headers sind private und no-cache. Der Unterschied besteht darin, dass no-cache generell eine Zwischenspeicherung des Inhalts der angeforderten Seite verhindert (z.B. von Proxyservern), während private ein Caching der Information in bestimmten Fällen zulässt, wenn es sich um einen Cache handelt, der nur diesem Client zugeordnet ist.

426

8

CGI

왘 Set-Cookie, Set-Cookie2 Diese Response-Header werden verwendet, um eine persistente Session zwischen Client und Server aufzubauen. Siehe hierzu die Beschreibung von Cookies weiter unten. Nachdem der Server alle Response-Header übermittelt hat, muss er vor dem eigentlichen Inhalt eine Leerzeile an den Client senden, damit dieser zwischen HTTP-Header und Content unterscheiden kann.

MIME-TYPEs Wenn der Server in der Response Daten an den Client zurückschickt, dann muss dieser wissen, um welche Art von Daten es sich handelt (z.B. Text, Audio oder Video). Zu diesem Zweck schreibt der Server im HTTP-Header Content-Type einen MIME-TYPE, der genau angibt, um welche Art von Daten es sich handelt. MIME-TYPEs werden zunächst grob spezifiziert (z.B. text, image, audio, video, application etc.). Nach dieser Grobspezifikation wird eine genauere Angabe (durch einen Slash getrennt) gemacht (z.B. gif, html, plain etc.). Existiert keine genauere Beschreibung, dann enthält diese Angabe nur ein Sternchen (»*«). So sagt z.B. der Content-Type-Header image/* dem Client, dass es sich hier um ein Bild handelt, aber nicht, ob es ein GIF-, ein JPEG- oder ein anderes Bild ist. Für HTML-Text ist der MIME-TYPE text/html anzugeben, für reinen ASCII-Text der MIME-TYPE text/plain. Es liegt letztlich jedoch in der Macht des Browsers, ob er den angegebenen MIMETYPE akzeptiert. So verwenden die meisten Browser oft die Datei-Endung, um die Art der vom Server gesendeten Daten festzustellen. Die Browser besitzen zur Erkennung der Serverdaten eine Liste von MIME-TYPEs sowie von Datei-Endungen. Sendet der Server einen MIME-TYPE, der in dieser Liste enthalten ist, dann kann der Browser die Daten entweder selbst anzeigen, ein Plug-In (ein externes Programm wird verwendet, aber die Daten werden innerhalb des Browsers angezeigt) oder ein externes Programm für die Darstellung der Daten verwenden, oder den Anwender fragen, was mit den Daten geschehen soll. Letzteres ist immer dann der Fall, wenn der MIME-TYPE nicht bekannt ist. Einige häufig benutzte MIME-TYPEs sind: MIME-TYPE

Beschreibung

text/plain

normaler Text ohne META-Auszeichnungen wie zum Beispiel bei HTML üblich

text/html

HTML-Dokument

image/gif

GIF-Bild

Cookies

427

MIME-TYPE

Beschreibung

image/jpg

JPG-Bild

image/*

beliebiges Bild

audio/*

beliebige Audiodaten

8.2 Cookies 8.2.1 Notwendigkeit von Cookies Wie wir zu Beginn festgestellt haben, handelt es sich beim HTTP-Protokoll um ein zustandsloses Internet-Protokoll, das heißt, der Webserver kann sich einzelne Requests nicht merken, um für einen bestimmten Webclient eine Art Historie aufzubauen. Jeder neue Request desselben Clients ist für den Server dasselbe wie ein Request irgendeines anderen Clients. Beim Design des zustandslosen Protokolls wurde bedacht, dass zwischen einzelnen Requests eines bestimmten Clients alles Mögliche passieren kann, angefangen vom Abrauchen des Clients über Leitungsstörungen bis hin zum Serverausfall. In einem solchen Fall hat das Mitführen einer Clienthistorie wenig Sinn, zudem würden Unmengen von Speicherplatz auf dem Server für die Speicherung des Clientzustands benötigt. Wer schon einmal einen Webserver mit Zugriffsschutz konfiguriert hat, weiß, was das bedeutet: Bei geschützten Seiten hat der Client nur dann Zugriffsberechtigungen, wenn er sich mit einem Usernamen und einem Kennwort beim Webserver anmeldet. Nehmen wir an, wir greifen zum allerersten Mal auf eine geschützte Seite eines Webservers zu. Der Webbrowser bekommt über den Response-Header 401 (Unauthorized) vom Server mitgeteilt, dass für diese Seite ein Username und ein Kennwort notwendig sind. Der Browser öffnet daraufhin ein Popupfenster auf dem Client-PC, über das der Anwender die notwendigen Daten eingibt, und schickt die Anmeldungsdaten an den Webserver, indem er denselben Request noch einmal sendet, diesmal mit dem speziellen Request-Header »Authorization«, dessen Wert den Usernamen und das Kennwort enthält. Falls beides korrekt ist, erhält er vom Webserver die angeforderte Seite. Doch woraus besteht in der heutigen Zeit eine Internetseite? Im Zeitalter von ISDN Verbindung oder sogar DSL-Anschluss spielen Grafiken keine große Rolle mehr, also pflastern die Webdesigner eine Seite meist mit Grafiken voll. Jedes einzelne Bild einer Seite stellt aber aus der Sicht des Webservers einen neuen Request dar, da die Verbindung ja zustandslos ist.

428

8

CGI

Im schlimmsten Fall bedeutet dies, dass pro angeforderter Seite 30-, 40-, 100-mal die Logindaten des Users vom Browser (der sich die einmal eingegebenen Daten natürlich merkt) über die Leitung geschickt werden, im Falle einer Basic Authentication auch noch im Klartext. Zum einen müssen deshalb mehr Daten gesendet werden als nötig, zum anderen ist vor allem die Übermittlung von Logindaten im Klartext äußerst gefährlich. Die Situation wird umso schlimmer, je mehr Zustandsdaten vom Client an den Server übermittelt werden müssen. Aus diesem Grund wurde über eine Umgehung des zustandslosen HTTP-Protokolls nachgedacht, und man fand diese in Form von so genannten »Cookies«, das sind Erweiterungen der HTTP-Header vom Server bzw. vom Client. Warum ich an dieser Stelle den Server zuerst nenne, obwohl doch der Client Initiator einer Webverbindung ist, hat einen ganz bestimmten Grund, den wir sogleich erläutern werden:

8.2.2 Arbeitsweise von Cookies Wenn wir beim Beispiel einer geschützten Seite bleiben, dann muss sich der User am Server anmelden, bevor er von diesem eine Antwort bekommt. Dies geschieht wie vorher beschrieben, nur mit dem Unterschied, dass nicht der Webbrowser die Anmeldung initiiert, sondern der Server (z.B. dadurch, dass er einen Redirect auf eine Anmeldeseite an den Client schickt, falls der Client noch nicht angemeldet ist). Nachdem die Anmeldung erfolgreich verlaufen ist, sendet der Webserver die angeforderte Seite, fügt aber in den HTTP-Header einen neuen Response-Header ein, dessen Name »Set-Cookie« (Netscape-Implementierung) bzw. »Set-Cookie2« (neuere StandardImplementierung) lautet und dessen Inhalt zunächst für den Client unerheblich ist. Der Client ist also zwar der Initiator der Webverbindung, der Server jedoch initiiert Cookies! Das einzige, worauf es ankommt, ist, dass der Client den empfangenen Cookie entweder im Hauptspeicher oder auf der Festplatte speichert und mit jedem weiteren Request in dem ebenfalls neuen Request-Header »Cookie« an den Server übermittelt (nicht unbedingt mit jedem Request, doch hierzu später Genaueres). Der Server erhält also im Folgenden mit (fast) jedem Clientrequest den Cookie-Header, dessen Inhalt genau dem entsprechen muss, was er dem Client zuvor in seinem SetCookie-Header geschickt hat. Jetzt kann man sich fragen, wo denn nun der Vorteil der Cookies liegt, wenn doch zusätzliche Header-Informationen über die Leitungen gehen. Nun, bei der ursprünglichen Methode wurden immer Username und Kennwort übermittelt, ein Cookie kann aber alles Mögliche enthalten, im einfachsten Fall eine numerische ID, unter welcher die Clientdaten in einer Datenbank des Servers gespeichert sind. Das einmal eingegebene Kennwort muss also nicht bei jedem Request im Klartext übertragen werden.

Cookies

429

Und mehr noch, stellen wir uns vor, dass bei der Anmeldung nicht nur Username und Kennwort übergeben wurden, sondern zusätzlich noch persönliche Daten wie Vorname, Nachname, Geschlecht..., die Liste ließe sich beliebig erweitern. All diese Daten müssten bei der herkömmlichen Art der Übertragung bei jedem einzelnen Request gesendet werden, während dies bei Cookies nur ein einziges Mal notwendig ist. Danach übertragen sowohl Server als auch Client nur noch eine ID für die auf dem Server gespeicherten Daten. Natürlich wird im richtigen Web-Leben nicht eine einfache ID über die Leitung geschickt, sondern der Server verpackt die ID so geschickt durch Verschlüsselung und Prüfsummen, dass man nichts mehr davon erkennen kann (falls man z.B. Hacker ist). So kann schnell aus der numerischen ID »17« ein String der Form ___A_-5.8.3.fsurzgdffavvynvAFESDS0xd7755 werden. Einmal angemeldet, besteht zwischen Client und Server eine so genannte »Session« (zu deutsch: »Sitzung«), die persistent über alle weiteren Clientrequests besteht, bis sich der Client entweder abmeldet oder die Session abläuft, weil längere Zeit kein Request mehr abgesetzt worden ist. Solange die Session aktiv ist, sendet der Client mit jedem Request das Cookie an den Server, der daraufhin in einer Liste der Sessions den Client identifizieren kann und somit kennt. Mit diesem Mechanismus wird das zustandslose HTTP-Protokoll sozusagen überlistet.

Ablaufdiagramm für Clientrequests mit Cookies Das Schaubild zeigt die Abläufe bei einem Clientzugriff auf eine geschützte Seite des Servers. 1: Der Client sendet einen Request für eine Seite, die auf dem Server geschützt ist und somit ein Login erfordert. 2: Der Server überprüft, ob im Clientrequest ein gültiges Cookie enthalten ist, das den Client identifiziert und autorisiert. Nachdem der Client kein Cookie im HTTP-Header gesendet hat, schickt der Server einen Redirect auf ein Loginformular. 3: Der Anwender füllt das Formular aus und schickt es an den Server. 4: Der Server prüft die Logindaten, erstellt im Erfolgsfall ein Cookie und sendet die Antwort auf den ursprünglichen URI an den Client inklusive des HTTP-Headers, der das Cookie enthält. Falls das Login fehlschlug, sendet er nochmal das Loginformular an den Client. 5: Bei allen weiteren Client-Requests sendet der Client im HTTP-Header »Cookie« den Namen und den Value des vom Server erstellten Cookies mit.

430

8

       

CGI

 

      

 

   "   

 

    !   

 

    !   

# 

Abbildung 8.2: Clientrequest mit Cookie

8.2.3 Netscape-Cookies Ursprünglich wurden Cookies von Netscape als proprietäre Erweiterung des HTTPProtokolls entwickelt. Diese Erweiterungen fanden so großen Anklang in der Gemeinde der Webprogrammierer, dass sie bald als Quasi-Standard galten. Parallel dazu wurde aber eine neue Spezifikation für Cookies vom WWW-Konsortium erstellt, die es allerdings bis heute nicht geschafft hat, die Netscape-Cookies zu verdrängen. Nichtsdestotrotz werde ich die Spezifikation der neuen Cookie-Generation weiter unten beschreiben. Die Netscape-Header-Erweiterungen sehen wie folgt aus: Syntax des Response Header für das Setzen eines Cookies durch den Server (Angaben in eckigen Klammern sind wie immer optional). Der Backslash am Ende der ersten Zeile wurde von mir aus Platzgründen im Buch eingefügt und bedeutet, dass beide Zeilen zusammengehören: Set-Cookie: name=value \ [;path=path][;domain=domain][;expires=expires][;secure]

Cookies

431

Die kursiven Wörter bedeuten, dass an deren Stelle aktuelle Werte eingesetzt werden müssen, während die normal geschriebenen Wörter literal sind. Wir werden das bald anhand von Beispielen sehen. Der Wert eines Set-Cookie-Headers besteht aus einem oder mehreren Attributen, von denen bis auf das Attribut secure alle sowohl einen Namen als auch einen Value besitzen, getrennt durch ein Gleichheitszeichen. Die Attributnamen sind case-insensitive, es wird nicht zwischen Klein- und Großbuchstaben unterschieden. Bis auf das Attribut name ist die Reihenfolge der einzelnen Attribute beliebig. Für die einzelnen Attribute gelten folgende Regeln: 왘 name Bei diesem Attribut ist sowohl der Attributname als auch der Wert dynamisch einzusetzen, d.h., es handelt es sich beim Attributnamen nicht um den festen String »name«, sondern, genauso wie beim Attributwert, um einen vom Cookie-Designer vorgegebenen dynamischen String, z.B. »loginId«. Der Name sollte wirklich nur aus Buchstaben, Ziffern, dem Bindestrich oder dem Unterstrich bestehen. Der Wert des Attributs ist ein beliebiger String in URI-Codierung, d.h., Zeichen wie z.B. ein Blank, hier im Speziellen natürlich das Semikolon, dürfen nicht im Wert stehen, sondern müssen wie beim URI durch Voranstellen eines Prozentzeichens gefolgt vom zweistelligen Hexcode des Zeichens codiert sein. Beispiel: loginId= Herbert%20Meier

왘 domain Mit dem Attribut »domain« kann man eine Internet-Domain angeben, für welche das Cookie gelten soll. Ohne Angabe einer Domain sendet der Client das Cookie mit jedem weiteren Request nur an denjenigen Server, der das Cookie erzeugt hat. Wenn eine Internet-Domain angegeben ist, dann muss sie mit einem Punkt beginnen (z.B.: ».mydomain.mycompany.de«, ».mycompany.de« oder ».de«). 왘 path Mit der Angabe dieses Attributs kann man den Versand des Cookie durch den Client weiter einschränken, so dass er nur noch dann vom Client gesendet wird, wenn der virtuelle Pfad im URI des Requests nicht oberhalb des angegebenen Pfads liegt. Das kann man so weit treiben, dass das Cookie nur für eine einzige Seite gültig ist. Entgegen der Spezifikation, nach welcher dieses Attribut optional ist, sollte man es immer angeben. Will man, dass der Client das Cookie für alle Requests sendet, dann kann man dies durch die Angabe des Pfads »/« erreichen. Wenn ein virtueller Pfad Blanks enthält (obwohl man das tunlichst vermeiden sollte), dann muss man um den Attributwert unbedingt doppelte Anführungszeichen setzen.

432

8

CGI

Beispiele: path=/cgi-bin/private/de

Der Client sendet das Cookie nur dann mit dem HTTP-Header, wenn der URI für den Request unterhalb des virtuellen Pfades liegt. Bei dem Request auf den URI /cgi-bin/private/myScript.pl zum Beispiel würde er das Cookie nicht senden, weil es nicht unterhalb des im Cookie angegebenen Verzeichnisses liegt. path=/

Der Client sendet das Cookie mit jedem Request, egal, welcher virtuelle Pfad angefordert wird. Einzige Voraussetzung ist, dass es sich beim Server entweder um denjenigen handelt, der das Cookie erzeugt hat (Domain nicht angegeben) oder innerhalb der Internet-Domain ist, die mit dem Attribut »domain« angegeben ist. path=/cgi-bin/myScript.pl

Der Client sendet nur für diesen einen URI das Cookie. 왘 expires Mit diesem Attribut kann man dem Cookie ein Verfalldatum geben, ab welchem der Client das Cookie nicht mehr senden darf. Außerdem muss der Client das Cookie bei Erreichen des Verfalldatums aus seinem Speicher löschen. Der Server kann durch ein Verfalldatum, das in der Vergangenheit liegt, explizit dafür sorgen, dass der Client dieses Cookie löscht. Dieses Feature ist notwendig, weil es clientseitig zwei verschiedene Arten der Cookie-Speicherung gibt: Cookies ohne Verfalldatum werden vom Browser nur im Hauptspeicher abgelegt und sind somit nur gültig, bis man den Browser beendet (oder dieser sich durch einen Absturz verabschiedet, was nicht selten vorkommt). Cookies mit Verfalldatum werden vom Browser im Filesystem des Clientrechners in einer Datei gespeichert, die nach einem Neustart des Browser gelesen wird. Solche Cookies sind also nicht flüchtig und damit über die Laufzeit des Browserprogramms hinweg persistent. Während also Cookies ohne Verfalldatum sozusagen während der Browser-Lebensdauer nie verfallen, verlieren Cookies mit Verfalldatum automatisch nach einer bestimmten Zeit ihre Gültigkeit, weil der Browser vor jedem Request dieses Datum prüft. Ist das Verfalldatum erreicht, wird das Cookie aus dem Speicher gelöscht. Das Format für das Verfalldatum ist leider nicht konform zum Datumsformat in HTTP-Headern, sondern muss die Form WDAY, DD-MONTH-YYYY HH:MM:SS GMT haben. WDAY ist die englische Abkürzung des Wochentages (z.B. »Thu«), MONTH ist die englische Abkürzung des Monats (z.B. »Dec«). Ein gültiges Datum wäre also z.B. Fri, 19-Oct-2001 09:13:40 GMT

Cookies

433

왘 secure Wenn dieses Attribut (das als einziges keinen Wert besitzt) angegeben ist, dann darf der Client das Cookie nur dann senden, wenn es sich um eine sichere Verbindung über SSL handelt (HTTPS-Protokoll). Der Client sendet vom Server erzeugte Cookies mit jedem Request im HTTP-Header »Cookie« zurück (solange sie nicht verfallen sind). Dabei schickt er nicht pro Cookie eine Header-Zeile, sondern verpackt alle Cookies in einen Cookie-Header, jeweils durch einen Strichpunkt getrennt. Für jedes Cookie wird nur der Name und der Wert des Cookies übertragen, z.B.: Cookie: id=17; firstname=rudi; lastname=Meier

Im Beispiel werden also drei verschiedene Cookies mit dem Request gesendet. Eine Client-Server-Kommunikation mit Cookies könnte z.B. so aussehen:

1        

) 

     

     





        

3 

    !"#$ %# & #'$( )*+  !!) ), -!! ./0

 1   

   !"

 

        

" 

 1   

   !"

, 

        

4 

    !"#$ %# & #'$/( !)2 !!) !!!!!! ./0

 1   

* 

Abbildung 8.3: Beispiel einer Cookie-Kommunikation

434

8

CGI

Im Schritt 4 sehen wir, welchen HTTP-Header der Server nach erfolgreichem Login des Clients für das Cookie an den Client mit der Antwort sendet. In Schritt 5 sendet daraufhin der Client den HTTP-Header »Cookie« mit dem Request für eine weitere geschützte Seite. Schritt 6: Der Server sendet kein Cookie, weil der Client durch das Cookie, das er an den Server gesendet hat, bereits autorisiert ist. In Schritt 8 hat sich der Anwender abgemeldet bzw. die Lebensdauer der Session, für die das Cookie zuständig war, ist abgelaufen, deshalb sendet er dasselbe Cookie noch einmal, allerdings mit einem Zeitstempel, der in der Vergangenheit liegt. Schritt 9: Der Client hat aufgrund des Zeitstempels das Cookie aus seiner Verwaltung gelöscht und sendet nun den nächsten Request ohne Cookie-Header.

8.2.4 Cookies gemäß Internet-Draft-Spezifikation Nachdem Netscape Cookies eingeführt hatte und viele Programmierer dieses neue Feature in der Praxis benutzten, begann man, einen ordentlichen Internet-Draft in Form eines RFC (Request for Comment) daraus zu entwickeln, worin Cookies genau spezifiziert sind. Da die neue Spezifikation von Cookies nicht kompatibel zur Netscape-Implementierung ist, hat man sich entschlossen, aus Gründen der Rückwärtskompatibilät einen neuen Namen für den HTTP-Header »Set-Cookie« zu vergeben: Er heißt nun »Set-Cookie2«. Alle Belange im WWW, unter anderem auch RFCs, werden zentral von einem Konsortium verwaltet, deren Homepage unter dem URI http://www.w3c.org zu erreichen ist. Mit dem Set-Cookie2-Header kann der Server nun mehr als nur ein Cookie an den Client senden. Die einzelnen Cookies müssen durch Kommata voneinander getrennt sein. Auch sind neue Attribute hinzugekommen, und das »expires«-Attribut ist dem neuen Attribut »max-age« gewichen, welches einfacher zu handhaben ist, da hier die Lebensdauer des Cookie nicht eine absolute, sondern als relative Zeitangabe ist. Es dürfen Blanks vor bzw. nach dem Gleichheitszeichen eines Attributs stehen. Manche Attributwerte müssen, andere können in doppelte Anführungszeichen gesetzt werden. Im Folgenden sind die Bestandteile des Headers beschrieben: 왘 name Für dieses Attribut gilt dasselbe wie bei der Netscape-Implementierung. 왘 path Auch hier gilt die Netscape-Implementierung

Cookies

435

왘 domain siehe Netscape-Implementierung 왘 version Der Wert für dieses Attribut ist zurzeit konstant und muss »1« sein. 왘 max-age Dieses Attribut entspricht in etwa dem Attribut expires der Netscape-Implementierung. Es gibt die Anzahl Sekunden an, die dieses Cookie gültig ist, gerechnet ab der aktuellen Zeit. Gibt man hier die Zahl 0 an, dann verfällt das Cookie sofort. Es besteht eine Abhängigkeit mit dem Attribut discard (siehe unten). 왘 port Der Wert dieses Attributs ist immer in doppelte Anführungszeichen zu setzen (»"«) und enthält eine Liste von Serverports, für welche das Cookie gesendet werden darf. Damit kann man zusätzlich zu den Attributen domain und path eine weitere Einschränkung für den Cookie-Versand erzielen. Gibt man mehrere Ports an, dann müssen die einzelnen Ports durch Kommata getrennt sein (z.B. 80,8088). 왘 comment Mit diesem Attribut kann man einen Kommentar für das Cookie schreiben, um z.B. dem Client mitzuteilen, wofür das Cookie verwendet wird. Der Attributwert muss in doppelten Anführungszeichen (") stehen. 왘 commentURL Mit diesem Attribut kann man einen URI angeben, um den Client zu informieren, welcher URL das Cookie erzeugt hat oder für welchen URI es bestimmt ist. Der Attributwert ist in doppelte Anführungszeichen (") zu setzen. 왘 discard Wenn dieses Attribut (das keinen Wert besitzt) vorhanden ist, dann muss der Client das Cookie löschen, wenn er sich beendet, auch dann, wenn mit dem Attribut »max-age« das Cookie zum Zeitpunkt des Programm-Endes noch gültig sein sollte. 왘 secure Siehe Netscape-Implementierung. Wie auch bei der Netscape-Implementierung sendet der Client Cookies an den Server. Die Syntax unterscheidet sich ein bisschen von der Netscape-Implementierung und wird im Folgenden beschrieben (alle Angaben in eckigen Klammern sind optional). Wie wir sehen, scheint sich auch bei Cookies die Perl-Notation von Variablennamen durchgesetzt zu haben ...

436

8

CGI

Im Übrigen gilt: Wörter in Kursivschrift müssen als Platzhalter für tatsächliche Werte betrachtet werden, während alle Zeichen in Normalschrift so geschrieben werden müssen, wie sie hier gezeigt sind (literal): Cookie: $Version=version[; name=value[; $Path=path[; \ $Domain=domain[; $Port=port]]]]

Wie wir an den vielen eckigen Klammern sehen, kann der Client durchaus auch einen Cookie-Header ohne eine Angabe des Cookies senden; nur die Version ist angegeben. In diesem Fall teilt er dem Server mit, welche Version von Cookies er versteht. Im Gegensatz zur Netscape-Implementierung beginnen bis auf den Cookie-Namen alle Attributnamen mit einem Dollarzeichen ($). Schon aus diesem Grund darf der Name des Cookies kein Dollarzeichen enthalten. Normalerweise wird der Client aber zumindest ein Cookie unter Angabe des Namens und des Wertes liefern, z.B.: Cookie: $Version=1; loginId=15

Es könnten aber auch zwei unterschiedliche Cookies gesendet werden, die durch einen Strichpunkt voneinander getrennt werden: Cookie: $Version=1; id=3; $Path="/"; firstname=horst; \ $Domain = ".de"

Das erste Cookie (id) wird nur an denjenigen Server geschickt, welcher das Cookie erzeugt hat. Das zweite Cookie (firstname) wird an alle Server der Internet-Domain de geschickt (z.B. www.tagesschau.de, www.sport.de, aber nicht an www.oracle.com).

8.2.5 Cookie-Beschränkungen Da die Verwaltung von Cookies speziell beim Client Systemressourcen in Anspruch nimmt, gibt die Internet-Spezifikation folgende Grenzwerte vor: 왘 Jeder Client muss insgesamt mindestens 300 Cookies gleichzeitig verwalten können. 왘 Jeder Client muss mindestens eine Gesamtlänge von 4096 Bytes pro Cookie unterstützen. 왘 Jeder Client muss mindestens 20 Cookies je Server bzw. Domain gleichzeitig verwalten können. Leider ist diese Vorgabe für Webprogrammierer auch gleichzeitig das obere Ende der Fahnenstange, das heißt, man kann nicht automatisch davon ausgehen, dass ein Webclient 301 Cookies verwalten kann.

CGI-Umgebung

437

8.3 CGI-Umgebung Ein CGI-Skript wird nicht aus der Shell (Kommandozeilen-Interpreter) heraus interaktiv aufgerufen, sondern aus dem Prozess des Webservers heraus gestartet. Dieser stellt einem CGI-Skript eine besondere Umgebung in Form einiger zusätzlicher Umgebungsvariablen zur Verfügung, damit das Skript mit dem Client kommunizieren kann. Im Folgenden sind einige wichtige Umgebungsvariablen aufgeführt, die vom Webserver vor dem Starten eines CGI-Skripts gesetzt werden. Die Übersicht gilt für den Apache-Webserver, bei anderen Webservern kann sich die Liste der Umgebungsvariablen von der hier aufgeführten Liste unterscheiden. Umgebungsvariable

Bedeutung

DOCUMENT_ROOT

Physisches Verzeichnis auf der Festplatte des Servers, auf das der virtuelle Pfad »/« zeigt. Der Wert dieser Variable wird bei der Konfiguration des Webservers festgelegt.

GATEWAY_INTERFACE

Diese Variable kennzeichnet die Version der CGI-Umgebung und kann für die Prüfung benutzt werden, ob das Skript in einer Webserver-Umgebung aufgerufen worden ist oder nicht.

HTTP_HOST

Der Rechnername des Webservers

HTTP_USER_AGENT

Der String, welcher vom Browser als Identifikation gesendet wird.

QUERY_STRING

Wenn ein Request mit der HTTP-Methode GET gesendet wird, enthält diese Variable alle vom Client gesendeten CGI-Variablen. Bei der HTTPMethode POST ist diese Variable nicht gesetzt.

REMOTE_ADDR

Die IP-Adresse des Clientrechners (oder eines dazwischen geschalteten Proxyservers, was oft zu Problemen führt, da ein und derselbe Client für unterschiedliche Requests verschiedene IP-Adressen haben kann, d.h., im Skript kann man einen Client nicht anhand der IP-Adresse identifizieren).

REQUEST_METHOD

Die verwendete HTTP-Methode für einen Request (meist GET oder POST)

REQUEST_URI

Der virtuelle Pfad des URIs für den aktuellen Clientrequest. Mit dieser Variable kann man im Skript seinen eigenen virtuellen Pfad auslesen. Das ist sehr praktisch, wenn das Skript HTML-Formulare dynamisch an den Client schickt, weil dann der URI für das Attribut ACTION des HTML-Tags nicht fest verdrahtet ist. Beispiele hierfür folgen weiter unten.

SCRIPT_FILENAME

Der physische Pfad des CGI-Skripts im Filesystem auf der Festplatte des Servers, der dem URI entspricht.

SCRIPT_NAME

siehe REQUEST_URI

SERVER_NAME

Der Name, unter dem der Webserver angesprochen wird

SERVER_PORT

Der Port, auf dem der Webserver läuft

SERVER_PROTOCOL

Der Name und die Version des Protokolls für den aktuellen Request

438

8

CGI

Genug der Theorie, nun wollen wir uns CGI in der Praxis ansehen. Anhand eines einfachen CGI-Skripts wollen wir die Grundlagen der CGI-Programmierung erarbeiten. Das Skript soll die Umgebungsvariablen im Browserfenster ausgeben. Ausgabeformat ist eine HTML-Tabelle. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

#!D:/Perl/bin/perl.exe -w use strict; # Bevor Daten an den Browser gesendet werden können, # muss gemäß HTTP-Protokoll ein gültiger # HTTP-Header ausgegeben werden. # Fehlt der Header, so gibt der Browser die # Fehlermeldung 500 (Internal Server Error) aus. # In der Fehlermeldungsdatei des Webservers kann # man nachlesen, was passiert ist. print( "Content-Type: text/html\n\n" ); print( { "path" }; }

Templates

453

sub getTemplate { my $self = shift( @_ ); return $self->{ "template" }; } sub setPath { my $self = shift( @_ ); my $arg = shift( @_ ); unless ( $arg and ( -f $arg ) ) { return undef; } $self->{ "path" } = $arg; return 1; } sub setTemplate { my $self = shift( @_ ); my $arg = shift( @_ ); $self->{ "template" } = $arg; return 1; } sub read { my $self = shift( @_ ); my $path = $self->getPath(); my $fh = new FileHandle( $path, "r" ); unless ( $fh ) { return undef; } $self->setTemplate( join( "", ) ); $fh->close(); return 1; } 1;

Erläuterungen: Die grundsätzliche Struktur des Packages sollte vom Kapitel über objektorientierte Programmierung her bekannt sein. Im Konstruktor sind zunächst nur zwei Attribute definiert (path und template), die mit Getter- bzw. Setter-Methoden gelesen und gesetzt werden können. Der Konstruktor erwartet den Pfadnamen einer Templatedatei als Pflichtargument und gibt den undefWert zurück, wenn die angegebene Datei nicht gelesen werden kann. Bei Erfolg enthält das Attribut template den Inhalt der Templatedatei.

454

8

CGI

In einem Skript kann das Package nun wie folgt benutzt werden: ... use Template (); my $templ = new Template( "/templates/myTempl.html" ); unless ( $templ ) { # Fehler } ...

Ersetzen von einfachen Templatevariablen Nun wollen wir im gelesenen Template alle einfachen Templatevariablen durch die tatsächlichen Werte ersetzen. Dazu müssen wir den Inhalt des Attributs template nach allen Vorkommnissen von $$(...) (... steht für einen beliebigen Variablennamen) durchsuchen und diese Zeichenketten durch die tatsächlich vom CGI-Skript zur Laufzeit ermittelten Daten ersetzen. Es ist also eine neue Methode (nennen wir sie substitute) für das Ersetzen der Platzhalter im Template zu implementieren. Am einfachsten ist die Verwendung eines Hashs als Parameter für die Methode, in welchem die zu ersetzenden Variablennamen als Keys, die einzusetzenden Werte als Values vorhanden sind. Das Hash wird der Ersetzungsmethode als Referenz übergeben. Ein kurzes Beispiel, wie man das Hash für die Ersetzungen füllt: my %subs = ( "firstname" => $firstname, "lastname" => $lastname, ); # Und hier der Aufruf der Ersetzungsroutine: replace( \%subs );

Das Template selbst könnte z.B. so aussehen: Hallo, $$(firstname) $$(lastname). Vielen Dank, dass Sie uns heute besuchen.

Der Programmcode für die Ersetzungsroutine sieht in etwa so aus: sub substitute ... # Hash für # Referenz my $href =

{ die Ersetzungen wird vom Aufrufer als übergeben shift( @_ );

Templates

455

# Im Attribut "template" steht der Inhalt der # Templatedatei my $templ = $self->getTemplate(); # Suche nach allen Vorkommnissen von $$(...) und # ersetze sie durch die Werte, # die sich dynamisch vom CGI-Script ergeben haben my $pat = '\$\$\(\s*(.+?)\s*\)'; while ( $templ =~ m~$pat~gs ) { my $varname = $1; my $pos = pos( $templ ); $pat = '\$\$\(\s*\Q' . $varname . '\E\s*\)'; if ( defined( $href->{ $varname } ) ) { my $replace = $href->{ $varname }; $templ =~ s~$pat~$replace~gs; } else { $templ =~ s~$pat~~gs; } pos( $templ ) = $pos; } }

Erläuterungen: Das Suchpattern \$\$\(\s*(.+?)\s*\) bedeutet: Suche nach der Zeichenkette $$(. Danach können beliebig viele Leerzeichen, Tabs oder Zeilenvorschübe kommen, sie können aber auch entfallen. Dann suche weiter, bis du die nächste schließende runde Klammer findest, vor der beliebig viel »white space« stehen kann. Alles, was zwischen öffnender und schließender runder Klammer steht, speichere in der Variable $1 ab (ohne »white space«). Wir sind also fehlertolerant, das heißt, wenn jemand im Template die Variable myVar in folgenden Variationen angibt: $$( myVar ) $$(myVar) $$( myVar) $$(myVar ) $$( myVar )

dann ist das kein Problem für uns, wir erkennen in jedem Fall die Variable myVar.

456

8

CGI

Beachte: wird das Fragezeichen für Minimal-Matching nach dem Quantifier + vergessen, dann wird bereits beim ersten Treffer bis einschließlich zur letzten schließenden runden Klammer im Template alles, was dazwischen steht, in den Match und damit in die Variable $1 aufgenommen. Siehe hierzu auch das Kapitel »Pattern Matching«. Die Option g muss angegeben werden, um in einer Schleife alle Vorkommnisse des Suchstrings zu finden, nicht nur das erste. Die Option s könnte hier entfallen, zumindest dann, wenn man davon ausgeht, dass der Name von Variablen keine Zeilenende-Zeichen enthält. Ich persönlich gebe die Option s immer an, wenn der zu durchsuchende String aus mehreren Zeilen besteht und das Zeilenende-Zeichen nicht als Sonderzeichen interpretiert werden und somit durch das Metazeichen . einen Treffer erzielen soll. Bei jedem einzelnen Match wird überprüft, ob im als Referenz übergebenen Hash ein Element vorhanden ist, das denselben Key hat wie der gefundene Variablenname, und ob der Wert dieses Elements einen definierten Wert besitzt. Wenn ja, dann werden alle Vorkommnisse dieser Variable in einer Ersetzung durch den Wert des Hash-Elements substituiert (zu deutsch »ersetzt«), ansonsten wird die Variable einfach entfernt. Der Ausdruck \Q$varname\E bedeutet, dass der Inhalt der Variable »$varname« literal interpretiert werden soll, auch wenn er Sonderzeichen im Sinne von Pattern Matching enthält (zum Beispiel *). Beachte: Nach jeder Ersetzung beginnt die nächste Suche wieder am Anfang des Strings, da ein beliebiger Ersetzungsvorgang die Position innerhalb des Suchstrings auf 0 zurücksetzt. Deshalb wird nach jedem Treffer zunächst die aktuelle Position des Treffers in der Variable $pos gespeichert. Nach einer Ersetzung wird die Position mit der Funktion pos() neu gesetzt. Dadurch beginnt die nächste Suche nicht von vorne und ist somit schneller. Siehe hierzu auch die Beschreibung der Funktion pos() in Anhang C. Wenn wir nun in einem CGI-Skript den Benutzer zum Beispiel als »Egon Sapperlott« identifiziert haben, könnte unser Programmcode für das CGI-Skript folgendermaßen aussehen: ... use Template (); # Benutzer identifizieren und Vorname bzw. Nachname # in ein Hash schreiben my %subs = ( "firstname" => $firstname, "lastname" => $lastname, );

Templates

457

my $templ = new Template( "myTemplate.txt" ); unless ( $templ ) { # Fehler } my $out = $templ->substitute( \%subs ); print( { $varname } ) { $templ =~ s~$pat~$1~gs; } else { $templ =~ s~$pat~~gs; } pos( $templ ) = $pos; } }

Erläuterungen für das Ersetzen von Bereichsvariablen: Das Suchpattern \$\$\[\s*(.+?)\s*\].+?\$\$\[\s*/\s*\1\s*\]

bedeutet: Suche nach der Zeichenkette $$[ und von dort weiter bis zur nächsten schließenden eckigen Klammer. Alles, was innerhalb der eckigen Klammern steht, speichere in der Variable $1 ab, mit Ausnahme von eventuell führendem »white space« und solchem am Ende vor der schließenden eckigen Klammer.

Templates

459

Suche danach weiter, bis das entsprechende schließende Tag gefunden wird, das dadurch gekennzeichnet ist, dass nach der öffnenden eckigen Klammer ein Slash »/« steht und der Variablenname wie zuvor lautet (durch den Ausdruck »\1« gekennzeichnet, siehe auch »Rückwärtsreferenzen« im Kapitel »Pattern Matching«). Bei jedem Treffer wird überprüft, ob der gefundene Variablenname als Key im als Referenz übergebenen Hash vorkommt und der Value des Hash-Elements den logischen Zustand TRUE hat. In diesem Fall werden öffnendes und schließendes Tag der Variable entfernt, andernfalls wird zusätzlich der Text dazwischen ebenso gelöscht. Zur Verbesserung der Suchperformance wird die Position für die nächste Suchoperation explizit neu gesetzt, da sonst die nächste Suche wieder am Beginn des zu durchsuchenden Strings beginnen würde. Sehen wir uns den Code für unser Beispiel mit dem 1000000-sten Besucher an: # Feststellen, ob es sich um den 1000000-sten # Besucher handelt my $accessCount = getAccessCount(); # Die Methode "getAccessCount()" wird irgendwo # definiert, sie interessiert hier weniger my %subs = ( "gratulation" => 0; ); if ( $accessCount == 1000000 ) { $subs{ "gratulation" } = 1; } my $templ = new Template( "greeting.html" ); ... print( $templ->substitute( \%subs ) );

Ersetzen von Templatevariablen für Includes Vor allen anderen Templatevariablen müssen die Includevariablen ersetzt werden. Dies liegt daran, dass ein inkludiertes Template wiederum Variablen enthalten kann, im schlimmsten Fall sind neue Includevariablen darunter. Beispiel: # Template A mit dem Pfad D:/templates/templateA.html ... Heute ist der $$(date).
$$

460

8

CGI

# Template B mit dem Pfad D:/templates/templateB.html ... Es ist jetzt genau $$(time) Uhr

Es muss sogar der Fall bedacht werden, dass Template B ein weiteres Template, nennen wir es Template C, inkludiert. Dieses inkludiert dann wiederum Template A, was unweigerlich zu einer Endlosschleife führt und deshalb im Programmcode abgefangen werden muss. In jedem Fall handelt es sich beim Inkludieren von weiteren Templates um einen rekursiven Vorgang. # Template A mit dem Pfad D:/templates/templateA.html ... Heute ist der $$(date).
$$ # Template B mit dem Pfad D:/templates/templateB.html ... $$ # Beispiel einer Endlos-Schleife # Template C mit dem Pfad D:/templates/templateC.html # Der folgende Include führt zu einer Endlos-Schleife $$

Ersetzt man andere Templatevariablen vor den Includevariablen, dann würden die Templatevariablen der inkludierten Templatedatei nicht ersetzt. Das erste Beispiel würde nach Ersetzung aller Variablen dann wie folgt aussehen (Kommentare sind hier weggelassen): Heute ist der 29.12.2001.
Es ist jetzt genau $$(time) Uhr

Anhand des folgenden Beispiels soll der rekursive Ersetzungsvorgang für Includevariablen verdeutlicht werden. Zunächst ein kleines Struktogramm, da die Programmlogik bei rekursiven Funktionen immer ein bisschen schwierig ist:

Templates

461

        

  



 



 

      

!"""           !""" #

   

    $        

 % #

  &  '      

Abbildung 8.4: Struktogramm für Template-Includes

Und jetzt sehen wir uns den Programmcode an: sub include { # $href ist eine Referenz auf das Hash, das die # Elemente für die Variablenersetzungen enthält. # Die Keys entsprechen den Variablennamen # $level ist eine Variable, mit welcher eine # Endlosschleife verhindert # wird. Falls nicht angegeben (beim erstmaligen # Aufruf), wird sie # mit 0 initialisiert. # $maxLevel stellt die maximale Anzahl von # Rekursionsebenen ein, die # nicht überschritten werden können. # Dies verhindert Endlosschleifen my ( $href, $level ) = @_; unless ( $level ) { $level = 0; } my $maxLevel = 10; if ( $level > $maxLevel ) {

462

8 return undef; } # Im Attribut "template" steht der Inhalt der # Templatedatei my $templ = $self->getTemplate(); # Schleife, in welcher alle Include. # Templatevariablen gesucht # und ersetzt werden my $pat = '\$\$'; while ( $templ =~ /$pat/gs ) { my $origName = $1; my $name = $1; # $name kann entweder den Dateipfad als # konstanten String # oder eine bzw. mehrere einfache # Variable(n) enthalten. # in diesem Fall müssen erst die # einfachen Variablen # ersetzt werden. $pat = '\$\$\(\s*(.+?)\s*\)'; while ( $name =~ /$pat/gs ) { my $vn = $1; if ( defined( $href->{ $vn } ) ) { my $val = $href->{ $vn }; $pat = '\$\$\(\s*\Q' . $vn . '\E\s*\)'; $name =~ s~$pat~$val~gs; } else { $name =~ s~$pat~~gs; } } # Falls die Include-Templatevariable keinen # gültigen Dateipfad enthält, werden alle # Vorkommnisse dieser # Templatevariable gelöscht. unless ( -f $name ) { $pat = '\$\$'; $templ =~ s~$pat~~gs; next; } # Nun muss der Inhalt des durch die # Templatevariable angegebenen # Pfades gelesen werden

CGI

Templates

463 my $fh = new FileHandle( $name, "r" ); unless ( $fh ) { return undef; } my $incData = join( "", ); $fh->close(); # Alle Vorkommnisse der Templatevariable # werden ersetzt $pat = '\$\$'; $templ =~ s~$pat~$incData~gs; # Rekursiver Aufruf derselben Funktion, # allerdings mit einer um # 1 erhöhten Rekursionsebene my $ret = include( $href, ( $level + 1 ) ); unless ( $ret ) { return undef; }

} return 1; }

Ersetzen von Loop-Templatevariablen Wer schon mit Templates gearbeitet hat, kennt die Problematik, dass bestimmte Inhalte nicht nur einmal, sondern mehrfach in einer Schleife ausgegeben werden müssen (um z.B. SELECT-Listen anzuzeigen). Die übliche Praxis, in einem solchen Fall das Template in mehrere Dateien aufzuspalten, hat allerdings mehrere Nachteile. Man benötigt bei einer Schleife bereits drei Dateien, z.B. header, record und footer. In record steht der Teil des Templates, der mehrfach ausgegeben werden soll. Mit mehreren Dateien wird das Ganze schnell unübersichtlich. Am unangenehmsten dabei ist, dass man nun nicht mehr den gesamten Template-Inhalt als Ganzes betrachten kann, weil er ja in mehrere Dateien aufgespalten ist. Das macht auch die Arbeit für den Designer des Layouts schwieriger. Mit Templatevariablen für Loops ist es möglich, den gesamten Inhalt in nur einer einzigen Templatedatei zu halten. Loop-Templatevariablen kennzeichnen Bereiche, die mehrfach in einer Schleife ausgegeben werden sollen. Enthält ein Template einen Loopbereich, dann wird der Inhalt des Templates automatisch in 3 Teile aufgeteilt: Der erste Teil ist der Text vor dem Loopbereich, der zweite Teil ist der Loopbereich, und der dritte und letzte Teil nach dem Loopbereich wird wiederum nur einmal ausgegeben. Wir erhalten also ein Array mit 3 Elementen. Wenn das Template 2 Loopbereiche enthält, ergibt dies 5 Array-Elemente usw.

464

8

CGI

Beispiel für ein Template mit Schleife: Das ist der erste Teil eines dreiteiligen Templates. Er wird nur einmal ausgegeben. Nun kommt eine ungeordnete Liste, deren List-Items in einer Schleife mehrfach ausgegeben wird:

    $${*}
  • $$(item)$${/*}
Das hier ist wieder Text, der nur einmal ausgegeben werden soll.

Und hier der Programmcode, mit dem man das Aufsplitten des Templates in ein Array durchführt: sub split { # Der Template-Inhalt steht in $template my $pat = '\$\$\{\s*/?\s*\*\s*\}'; my @parts = split( m~$pat~s, $template ); }

Hinweis: Der reguläre Ausdruck in der split()-Funktion enthält den Ausdruck /?. Das Fragezeichen dient hier als Quantifier mit der Bedeutung »Der Slash kann keinmal oder einmal vorkommen«. Damit wird sowohl das öffnende als auch das schließende Tag der Loopvariable erfasst. Mit dem obigen Template enthält @parts nun folgende Elemente: $parts[ 0 ]: Das ist der erste Teil eines dreiteiligen Templates. Er wird nur einmal ausgegeben. Nun kommt eine ungeordnete Liste, deren List-Items in einer Schleife mehrfach ausgegeben werden:
    $parts[ 1 ]:

  • $$(item) $parts[ 2 ]:

Das hier ist wieder Text, der nur einmal ausgegeben werden soll.

Templates

465

Nun wollen wir uns die einzelnen Codefragmente, die wir bisher für die einzelnen Arten von Templatevariablen erarbeitet haben, in einem zusammenhängenden Sourcecode für eine Template-Engine ansehen: package Template; use strict; use FileHandle; ##################################################### ##################################################### # # Packagevariablen # ##################################################### ##################################################### our our our our

$dbgLevel = 0; @errors = (); @messages = (); $packageName = "Template";

##################################################### ##################################################### # # public Packagemethoden # ##################################################### ##################################################### sub cleanErrors { @errors = (); } sub cleanMessages { @messages = (); } sub dbg { my $level = $dbgLevel; if ( @_ and defined( $_[ 0 ] ) and ( $_[ 0 ] =~ m~^\d*$~ ) ) { $level = shift( @_ ); } if ( $level > $dbgLevel ) { return 1; } my $msg = ""; foreach my $arg ( @_ ) { $msg .= ( defined( $arg ) ? $arg : "undef" ); }

466

8 push( @messages, $msg ); return 1;

} sub err { my $msg = ""; foreach my $arg ( @_ ) { $msg .= ( defined( $arg ) ? $arg : "undef" ); } push( @errors, $msg ); return 1; } sub getErrors { return @errors; } sub getMessages { return @messages; } ##################################################### ##################################################### # # private Instanzmethoden # ##################################################### ##################################################### sub _getCurPart { my $self = shift( @_ ); my $curPartIndex = $self->getCurPartIndex(); my $lastPartIndex = $self->getLastPartIndex(); if ( ( $curPartIndex < 0 ) or ( $curPartIndex > $lastPartIndex ) ) { $curPartIndex = 0; $self->_setCurPartIndex( 0 ); } my $parts = $self->{ "parts" }; return $parts->[ $curPartIndex ]; } sub _setCurPartIndex { my $self = shift( @_ );

CGI

Templates

467

my $arg = shift( @_ ); $self->{ "curPartIndex" } = $arg; return 1; } ##################################################### ##################################################### # # Konstruktor # ##################################################### ##################################################### sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $prefix = "${ packageName }::new():"; my $self = { "curPartIndex" "includePostfix" "includePrefix" "includeSubs" "loopPostfix" "loopPrefix" "parts" "path" "sectionPostfix" "sectionPrefix" "simplePostfix" "simplePrefix" };

=> => => => => => => => => => => =>

-1, '>', '$$', '$$ 14 Jahre', 1, 'e', 0, 0, 'alle Kinder über 14 Jahre' ); INSERT INTO grp VALUES ( NULL, 2, 'Studenten', 1, 'e', 0, 0, 'alle Studenten' ); INSERT INTO grp VALUES ( NULL, 2, 'Sonstige', 1, 'e', 0, 0, 'Nicht-Studenten' ); INSERT INTO grp VALUES ( NULL, 6, 'männlich', 2, 'e', 0, 0, 'alle Mannsbuider' ); INSERT INTO grp VALUES ( NULL, 6, 'weiblich', 2, 'e', 0, 0, 'alle Weibsbuider' ); INSERT INTO grp VALUES ( NULL, 7, 'männlich', 2, 'e', 0, 0, 'Männer' ); INSERT INTO grp VALUES ( NULL, 7, 'weiblich', 2, 'e', 0, 0, 'Frauen' );

Wer sich jetzt ein bisschen über die Zahlen für die Parent-IDs wundert: Sehen Sie sich das folgende Schaubild an: Die Datenbank-IDs werden vom Datenbankserver automatisch vergeben, wenn wir beim INSERT für die Spalte grp_id den Wert NULL angeben. Per Default beginnen automatisch vergebene IDs bei 1.

610

9



 

 

 

   

  

 

 

   

  

Das Datenbank-Interface DBI

 



 

 

  

 

  

 

  









       

           

Abbildung 9.14: Internet-Usergruppen mit Datenbank-IDs

Aus dem Bild ist auch ersichtlich, dass die Gruppennamen männlich und weiblich mehrfach vorkommen. Dies führt nicht zu einem Fehler, weil wir zwar einen UNIQUE-Index verwenden, darin aber zusätzlich die Spalte grp_pid angegeben haben. Da die namensgleichen Gruppen aber verschiedene Parents besitzen, tritt keine Regelverletzung auf. Hätten wir aber für jede der Spalten einen getrennten Index angegeben, könnten wir überhaupt keine gleichnamigen Gruppen verwenden: ... INDEX( grp_pid ), UNIQUE( grp_name ), ...

Nun kommen wir zum eigentlichen Problem von hierarchischen Strukturen in der Datenbank: Jede Gruppe hat eine Spalte, in welcher die Datenbank-ID der übergeordneten Gruppe abgelegt ist. Damit kann man mit einem einzelnen SQL-Statement das übergeordnete Objekt aus der Datenbank lesen. Oft benötigt man aber den gesamten Baum aller übergeordneten Objekte (für die Gruppe weiblich der Studenten ergibt sich zum Beispiel der Baum Erwachsene-->Studenten-->weiblich). Um hier den ganzen Baum bis zur obersten Ebene auslesen zu können, müssen wir ausgehend von der Parent-ID der aktuellen Gruppe den Datensatz der

Rekursive Strukturen mit DBI

611

nächsthöheren Gruppe lesen. Dieser Vorgang ist so lange zu wiederholen, bis man in der ersten Ebene der Hierarchie angelangt ist. Man braucht also genau so viele SQL-Statements wie darüber liegende Ebenen vorhanden sind. In unserem Beispieldesign ist das noch kein Beinbruch, weil wir nur drei Ebenen vorgesehen haben. Aber denken Sie sich eine tief geschachtelte Struktur mit mehr als 20 Ebenen, dort kann dies zu einem echten Problem werden. Wenn man z.B. zunächst nur die Datenbank-ID der Gruppe weiblich besitzt, muss man rekursiv so lange die übergeordneten Gruppen aus der Datenbank auslesen, bis die Spalte grp_pid den Wert 0 enthält. Diese Vorgehensweise führt zu mehreren SQL-Statements, die nacheinander ausgeführt werden müssen. Die Problematik kennen wir jetzt. Aber wie sieht die Lösung aus? Manche Programmierer lösen das Problem so, dass sie in der Tabelle für die Gruppe nicht nur eine Spalte grp_pid vorsehen, sondern mehrere davon (z.B. grp_pid1, grp_pid2 usw.). Dieser Ansatz hat zwei gravierende Nachteile: Zum einen begrenzt man die mögliche Schachtelungstiefe der Gruppen, zum anderen muss man bei jeder Gruppe alle Spalten für die übergeordneten Gruppen mit abspeichern, auch wenn die EbenenTiefe nicht voll ausgeschöpft ist. Andere Schlaumeier wiederum verwenden die Spalte grp_pid nicht numerisch, sondern als String, und schreiben dort eine Liste aller Parent-IDs hinein. Meine Meinung dazu ist: Es gibt viele Möglichkeiten, eine Datenbankanwendung künstlich langsam zu machen, und diese gehört mit zu den wirkungsvollsten. Abgesehen davon kann man in den begrenzt langen String natürlich keine unbegrenzte Anzahl von IDs schreiben. Zudem gestaltet sich eine Suche mit diesem Konzept sehr kompliziert. Wir wollen einen anderen Weg beschreiten, indem wir eine neue Datenbanktabelle definieren, in der wir für eine Gruppe alle Datenbank-IDs der übergeordneten Gruppen speichern. Damit vermeiden wir mehrfach auszuführende SQL-Statements, wir müssen nur einen Tabellenjoin in Kauf nehmen. Das Format für die Zusatztabelle grp_parent sieht folgendermaßen aus: CREATE TABLE grp_parent ( grpp_id BIGINT( 15 ) NOT NULL, grpp_pid BIGINT( 15 ) NOT NULL, grpp_level SMALLINT NOT NULL, grpp_plevel SMALLINT NOT NULL, INDEX( grpp_level ), INDEX( grpp_plevel ), UNIQUE( grpp_id, grpp_pid ) );

612

9

Das Datenbank-Interface DBI

Die Linktabelle enthält sowohl die ID einer Child- als auch einer Parent-Gruppe. Damit man die Datensätze nach der hierarchischen Reihenfolge der Parents sortieren kann, ist noch eine Spalte vorhanden, in der die Ebene für den Parent abgespeichert ist. Dasselbe gilt für die Ebene der Kindgruppe. Diese wird in der Spalte grpp_level abgelegt. Für eine beliebige Gruppe sind genau so viele Einträge zu machen, wie Parents vorhanden sind. Guppen in der obersten Ebene der Hierarchie benötigen keinen Eintrag in der Linktabelle. Da diese Aussage wohl ein bisschen schwer zu verstehen ist, nehmen wir ein Beispiel: Die Gruppen »Kinder« und »Erwachsene« besitzen keinen Eintrag, weil sie in der obersten Ebene sind. Die Gruppe »Studenten« hat einen Eintrag: grpp_id = 6, grpp_pid = 2, grpp_level = 1, grpp_plevel = 0

Für die Gruppe »weiblich« von »Sonstige« haben wir zwei Datensätze: grpp_id = 11, grpp_pid = 2, grpp_level = 1, grpp_plevel = 0 grpp_id = 11, grpp_pid = 7, grpp_level = 2, grpp_plevel = 1

Da es bei diesem Design nicht mehr ganz so einfach ist, alle notwendigen SQL-Statements für eine neue Gruppe manuell aufzurufen, wollen wir im Folgenden ein PerlSkript erstellen, das uns diese Arbeit abnimmt. Nennen wir es createGroup.pl. Das Skript soll folgende Argumente unterstützen: [ ]

Ich glaube, die meisten Argumente sind selbsterklärend, sie entsprechen den Attributen unserer Klasse »Group«. Als optionales letztes Argument kann die Datenbank-ID der übergeordneten Gruppe angegeben werden. Für Gruppen in der obersten Ebene der Hierarchie ist dieses Argument nicht erforderlich. Natürlich wäre es besser, wenn wir zuerst das Modul Group.pm implementieren würden, dann könnte unser Skript das API benutzen. Aber ich möchte das Skript explizit zuerst schreiben, damit Sie anhand eines relativ kurzen Programmcodes die Vorgehensweise erlernen: 01 #!D:/Perl/bin/perl.exe -w 02 03 use strict; 04 05 use DBI ();

Rekursive Strukturen mit DBI 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

my my my my my

$dbDriver = "mysql"; $dbName = "test"; $dbUser = ""; $dbPwd = ""; %dbArgs = ( "raiseError" => 0, "printError" => 0,

); my ( $name, $status, $maxMembers, $desc, $pid ) = @ARGV; unless ( $name ) { print( STDERR "ungültiger Gruppenname\n" ); exit( 1 ); } if ( ( $status ne "e" ) and ( $status ne "d" ) ) { $status = "e"; } unless ( $maxMembers =~ /^\d+$/ ) { $maxMembers = 0; } unless ( $pid ) { $pid = 0; } else { unless ( $pid =~ /^[1-9]\d*$/ ) { print( STDERR "ungültige PID\n" ); exit( 1 ); } } my $dbUri = "dbi:$dbDriver:$dbName"; my $dbh = DBI->connect( $dbUri, $dbUser, $dbPwd, \%dbArgs ); unless ( $dbh ) { print( STDERR "Fehler beim Connect\n" ); exit( 1 ); } my @pids = (); if ( $pid ) { my $sql = execute() ) { 64 print( STDERR "Fehler in SQL '$sql':", 65 " $DBI::errstr\n" 66 ); 67 } 68 69 while ( defined( 70 my $aref = $sth->fetchrow_arrayref() 71 ) ) { 72 push( @pids, $aref->[ 0 ] ); 73 } 74 75 $sth->finish(); 76 77 push( @pids, $pid ); 78 } 79 80 my $level = scalar( @pids ); 81 82 my $qname = $dbh->quote( $name ); 83 my $qstatus = $dbh->quote( $status ); 84 my $qdesc = $desc ? $dbh->quote( $desc ) : "NULL"; 85 86 my $sql = { "mysql_insertid" };

9

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135

615

for ( my $plev = 0; $plev quote( $name ); 83 my $qstatus = $dbh->quote( $status ); 84 my $qdesc = $desc ? $dbh->quote( $desc ) : "NULL"

werden alle Stringwerte in Quotes gesetzt. Bei dem Attribut desc müssen wir aufpassen, da es ja auch leer sein kann. In diesem Fall muss im INSERT-Statement nicht etwa ein leerer String angegeben werden (der auch in Quotes gesetzt sein müsste), sondern das Schlüsselwort NULL, das ohne Quotes angegeben sein muss. In den Zeilen 86 bis 97 setzen wir unser INSERT-Kommando auf, und zwar mit dem Operator createGroup.pl Kinder e 0 "alle Kinder bis 18 Jahre" Gruppe 'Kinder' mit ID '1' angelegt

Rekursive Strukturen mit DBI

617

D:\>createGroup.pl Erwachsene e 0 "alle Erwachsenen" Gruppe 'Erwachsene' mit ID '2' angelegt D:\>createGroup.pl "< 10 Jahre" e 0 "alle Kinder bis 10 Jahre" 1 Gruppe '< 10 Jahre' mit ID '3' angelegt D:\>createGroup.pl "10-14 Jahre" e 0 "alle Kinder zwischen 10 und 14 Jahre" 1 Gruppe '10-14 Jahre' mit ID '4' angelegt D:\>createGroup.pl "> 14 Jahre" e 0 "alle Kinder über 14 Jahre" 1 Gruppe '> 14 Jahre' mit ID '5' angelegt D:\>createGroup.pl Studenten e 0 "alle Studenten" 2 Gruppe 'Studenten' mit ID '6' angelegt D:\>createGroup.pl Sonstige e 0 "Nicht-Studenten" 2 Gruppe 'Sonstige' mit ID '7' angelegt D:\>createGroup.pl männlich e 0 "alle Mannsbuider" 6 Gruppe 'männlich' mit ID '8' angelegt D:\>createGroup.pl weiblich e 0 "alle Weibsbuider" 6 Gruppe 'weiblich' mit ID '9' angelegt D:\>createGroup.pl männlich e 0 Männer 7 Gruppe 'männlich' mit ID '10' angelegt D:\>createGroup.pl weiblich e 0 Frauen 7 Gruppe 'weiblich' mit ID '11' angelegt

Nachdem wir nun endlich unsere Datenbank mit der Gruppenhierarchie gefüllt haben, wollen wir uns ansehen, wie man dieselbe wieder ausliest. Denken Sie an einen Administrator, der über eine Web-Oberfläche (Browser) Gruppen hinzufügen, ändern oder löschen soll. Dazu gehört auch die Ausgabe des gesamten Baums der Hierarchie. Der große Unterschied zum bisher gezeigten Code ist, dass wir uns nun nicht von einem bestimmten Punkt aus nach oben »hangeln« und alle übergeordneten Objekte des Baums lesen, sondern den umgekehrten Weg gehen, der weitaus umfangreichere Daten liefern kann: Beginnend bei einem bestimmten Objekt innerhalb der Hierarchie müssen wir nun allen Verästelungen und Verzweigungen nach unten folgen und die Kinder und Kindeskinder des gewählten Objekts lesen. Dieses Problem lässt sich am einfachsten durch Rekursion lösen. Am besten sehen wir uns das Ganze in einem Schaubild an. Nehmen wir zum Beispiel das Objekt mit der Nummer »5«. Der Weg nach oben ist ganz klar: Das übergeordnete Objekt »2« hat wiederum nur ein Parent-Objekt (»1«). Damit ist die Sache bereits zu Ende. Wenn wir von unten nach oben laufen, dann gibt es immer nur einen Parent über dem aktuellen Objekt.

618

9

Das Datenbank-Interface DBI



!

"

#

&

'

$

(





!

"

#

$

&



!

"

#

%

'

$

%

(

%

&

'

!(

Abbildung 9.15: Hierarchische Baumstruktur

Ganz anders wird die Sache, wenn wir den umkekehrten Weg gehen, nämlich von oben nach unten: Wir beginnen wieder bei Objekt »5«. Darunter finden wir zwei Objekte, von denen »11« wiederum ein untergeordnetes Objekt (»14«) hat. Unter diesem geht es noch eine Ebene tiefer und noch eine und noch eine usw. Wir sehen schon, die Sache wird kompliziert und vor allem ressourcenintensiv, wenn wir zum Beispiel beginnend beim obersten Objekt alle Unterobjekte ausgeben wollen. Aber genau das müssen wir tun, wenn wir die Baumstruktur anzeigen wollen. Ich will Ihnen die grundsätzliche Programmlogik für eine rekursive Funktion in einem Pseudocode zeigen: sub printTree { my ( $dbh, $id ) = @_; unless ( $id ) { SQL-Statement für das Lesen aller Objekte der ersten Ebene der Hierarchie erstellen } else { SQL-Statement für das Lesen aller direkten untergeordneten Objekte von $id erstellen }

Rekursive Strukturen mit DBI

619

while ( Datensatz gefunden ) { # $cid enthält die Datenbank-ID des # aktuell gelesenen Datensatzes Gib das aktuelle Objekt aus; printTree( $dbh, $cid ); } }

Erläuterungen: Die Funktion printTree() kann sowohl mit einer Datenbank-ID als zweitem Argument als auch mit nur einem Argument aufgerufen werden. Fehlt die ID in der Liste der Aufrufargumente, dann liest die Funktion nur die Objekte der ersten Ebene in der Hierarchie. Ansonsten werden alle direkten Kind-Objekte von $id gelesen. In der Schleife ruft sich die Funktion selbst noch einmal auf, diesmal mit der Datenbank-ID des aktuell gelesenen Objekts als Argument. Die Programmlogik »hangelt« sich sozusagen in der Baumstruktur jeweils um eine Verästelung nach unten. Die Reihenfolge der Verarbeitung sehen wir uns in einem Schaubild an: 









  









     

















































  







Abbildung 9.16: Reihenfolge der Verarbeitung einer Baumstruktur mit Rekursion

620

9

Das Datenbank-Interface DBI

In welcher Reihenfolge die Datensätze einer Ebene jeweils abgearbeitet werden, hängt von der Sortierung in der Programmlogik ab. Jetzt sind wir soweit, dass wir das Skript printGroups.pl implementieren können, das alle Gruppen in der Datenbank rekursiv ausgibt und dabei die Hierarchie durch Einrückungen deutlich macht. Zunächst der gesamte Programmcode in einem Stück: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

#!D:/Perl/bin/perl.exe -w use strict; use DBI (); my $dbUrl = "dbi:mysql:test"; my $dbUser = ""; my $dbPwd = ""; my %dbArgs = ( "raiseError" => 0, "printError" => 0, ); my $dbh = DBI->connect( $dbUrl, $dbUser, $dbPwd, \%dbArgs ); unless ( $dbh ) { err( "DB connect failed with '", "$DBI::errstr'\n" ); exit( 1 ); } my $id = shift( @ARGV ); if ( defined( $id ) and ( $id =~ /[^\d]/ ) ) { $id = undef; } my $status = printChildren( $dbh, $id ); exit( 0 ); sub err { foreach my $arg ( @_ ) { print( STDERR defined( $arg ) ? $arg : "undef" ); } print( STDERR "\n" ); }

Rekursive Strukturen mit DBI 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94

sub readChildren { my ( $dbh, $pid ) = @_; my $sql = undef; unless ( $pid ) { $sql = [ 0 ] } = {}; my $hr = $grps{ $row->[ 0 ] }; ( $hr->{ "id" }, $hr->{ "pid" }, $hr->{ "name" }, $hr->{ "level" }, $hr->{ "status" }, $hr->{ "maxMemberCount" },

621

622 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135

9

Das Datenbank-Interface DBI

$hr->{ "memberCount" }, $hr->{ "desc" } ) = @{ $row }; } $sth->finish(); return( 1, %grps ); } sub printChildren { my ( $dbh, $id ) = @_; my ( $status, %grps ) = readChildren( $dbh, $id ); unless ( $status ) { return $status; } foreach my $cid ( sort( { $grps{ $a }->{ "name" } cmp $grps{ $b }->{ "name" } } keys( %grps ) ) ) { my $hr = $grps{ $cid }; my $name = $hr->{ "name" }; my $level = $hr->{ "level" }; my $prefix = " " x $level; print( "$prefix$name ( id=$cid )\n" ); printChildren( $dbh, $cid ); } return 1; } END { $dbh->disconnect() if ( $dbh ); } 1;

Erläuterungen zum Skript printGroups.pl: Das Skript kann ohne Argumente aufgerufen werden, dann gibt es die gesamte Hierarchie der Gruppen aus, beginnend in der obersten Ebene. Gibt man die Datenbank-ID einer existierenden Gruppe an, dann beginnt die Ausgabe eine Ebene unterhalb der angegebenen Gruppe.

Rekursive Strukturen mit DBI

623

In den Zeilen 26 my $id = shift( @ARGV ); 27 if ( defined( $id ) and ( $id =~ /[^\d]/ ) ) { 28 $id = undef; 29 }

holt sich das Skript eine eventuell angegebene Gruppen-ID aus der Parameterliste. Wir müssen überprüfen, ob das Argument eine gültige Zahl ist oder ob der Anwender des Skripts etwas Falsches eingegeben hat. Falls keine positive ganze Zahl angegeben wurde, setzen wir $id auf undef. Das hat dieselbe Wirkung, als hätte der Anwender beim Aufruf des Skripts gar kein Argument verwendet. Anschließend rufen wir im Hauptprogramm die Funktion printChildren() auf, das war's. Keine Schleife, nur ein simpler Funktionsaufruf. Sieht einfach aus. Natürlich wird die eigentliche Arbeit in der Funktion printChildren() bzw. der Hilfsfunktion readChildren() erledigt. Der Programmcode von printChildren() sieht aber auch nicht allzu schwierig aus: 105 sub printChildren { 106 my ( $dbh, $id ) = @_; 107 108 my ( $status, %grps ) = 109 readChildren( $dbh, $id ); 110 111 unless ( $status ) { 112 return $status; 113 } 114 115 foreach my $cid ( sort( 116 { $grps{ $a }->{ "name" } cmp 117 $grps{ $b }->{ "name" } } 118 keys( %grps ) 119 ) ) { 120 my $hr = $grps{ $cid }; 121 my $name = $hr->{ "name" }; 122 my $level = $hr->{ "level" }; 123 my $prefix = " " x $level; 124 print( "$prefix$name ( id=$cid )\n" ); 125 printChildren( $dbh, $cid ); 126 } 127 128 return 1; 129 }

Der Funktion wird ein Datenbank-Handle sowie eine Gruppen-ID übergeben. Letztere ist ein optionales Argument und kann auch entfallen. Wir werden weiter unten noch sehen, dass die Funktion readChildren() beide Fälle unterstützt.

624

9

Das Datenbank-Interface DBI

Das Statement in den Zeilen 108 und 109 ruft die Funktion readChildren() mit denselben Argumenten auf, die im Hauptprogramm übergeben wurden. Wie wir sehen, liefert readChildren() sowohl einen Statuscode als auch ein Hash zurück. Wie der Funktionsname schon sagt, liest sie die direkten Untergruppen der nächsten Ebene in der Hierarchie und gibt deren Daten als Hash zurück. Die Keys des Hashs enthalten die Datenbank-IDs der direkten Untergruppen und zeigen jeweils wiederum auf ein Hash, das als anonyme Hash-Referenz definiert werden muss, weil die Elemente eines Hashs Skalare sein müssen (siehe Grundlagen). Die Struktur von %grps sieht also beim ersten Aufruf der Funktion so aus: %grps = ( "1" = { "id" "pid" "name" "level" "status" "maxMemberCount" "memberCount" "desc" }, "2" = { "id" "pid" "name" "level" "status" "maxMemberCount" "memberCount" "desc" },

=> => => => => => => =>

1, 0, "Kinder", 0, "e", 0, 0, "alle Kinder bis 18 Jahre",

=> => => => => => => =>

2, 0, "Erwachsene", 0, "e", 0, 0, "alle Erwachsenen",

)

Interessant wird es ab Zeile 115. In der foreach-Schleife werden die Gruppen lexikalisch sortiert ausgegeben. Als Sortierkriterium habe ich den Gruppennamen verwendet. Der etwas kompliziert aussehende Aufruf der Perl-Funktion sort() ist in Anhang C genauer beschrieben. Für das Verständis unseres Programmcodes ist im Moment nur wichtig, dass in jedem Schleifendurchlauf in der Variable $cid die Datenbank-ID einer Untergruppe steht. Der Ausdruck $grps{ $cid } zeigt also auf eine anonyme Hashreferenz, in der die Gruppendaten wie oben gezeigt abgelegt sind. Damit das Programm leichter lesbar wird, verwende ich die Hilfsvariable $hr, um auf die Gruppendaten zuzugreifen. Die richtige Einrückung der aktuellen Ebene erreichen wir durch eine entsprechende Anzahl von Leerzeichen, die vor den Gruppendaten ausgegeben werden. Im Beispiel habe ich 4 Leerzeichen gewählt, die so oft wiederholt werden, wie die Nummer der Ebene vorgibt. Da diese Nummer in der obersten Ebene

Rekursive Strukturen mit DBI

625

0 ist, erfolgt für deren Gruppen keine Einrückung. Die Daten in der zweiten Ebene werden um 4 Leerzeichen eingerückt, Gruppen der dritten Ebene um 8 Leerzeichen usw. Nachdem die Gruppendaten ausgegeben wurden, ruft sich die Funktion in Zeile 125 selbst noch einmal auf. Diesmal wird die Datenbank-ID der gerade ausgegebenen Gruppe als Argument verwendet. Damit erzielt man eine Ausgabereihenfolge wie in Abbildung 9.16 gezeigt. Nun zum eigentlichen »Worker« des Skripts: Die mühsame Arbeit des Einsammelns der Gruppendaten erledigt die Funktion readChildren(), deren Aufruf wir bereits kennen. Der Funktionsparameter $pid ist optional und entfällt beim Anzeigen der Gruppen in der obersten Ebene. Deshalb müssen wir das SQL-Statement unterschiedlich gestalten, je nachdem, ob eine Gruppen-ID angegeben ist oder nicht. Der SELECT-Befehl für das Auslesen der obersten Gruppen ist noch recht einfach. Wir suchen einfach nach Datensätzen, die in der Spalte grp_level die Zahl 0 stehen haben. Etwas schwieriger gestaltet sich da schon das Lesen von Kindern einer Gruppe: 61 SELECT 62 grp.* 63 FROM 64 grp_parent, 65 grp 66 WHERE 67 grpp_pid = $pid AND 68 grp_id = grpp_id AND 69 grp_level = grpp_plevel + 1

In diesem Statement verwenden wir einen Tabellenjoin, mit dem erreicht wird, dass nicht alle Kinder, sondern nur die direkten Nachfahren gelesen werden, die eine Ebene tiefer liegen. Aus der Tabelle grp benötigen wir alle Spalten, deshalb ist in der Liste ein Sternchen »*« angegeben. Vorsicht: Wenn Sie nur ein Sternchen ohne Tabellennamen angeben: 61 SELECT 62 *

dann haben Sie ein großes Problem, weil das Kreuzprodukt beider Tabellen zurückgeliefert wird. Die Auswirkungen habe ich bereits früher in Abschnitt Joins erläutert. Mit dem SQL-Statement werden zunächst alle Datensätze in der Tabelle grp_parent angesprochen, die in der Spalte grpp_pid den Wert der übergeordneten Gruppe enthalten, der in $pid steht (grpp_pid = $pid). Zusätzlich muss noch die Bedingung erfüllt sein, dass die Ebene des Datensatzes um eins größer ist als die des Parents (grp_level = grpp_plevel + 1). Der eigentliche Join der beiden Tabellen erfolgt mit der UND-Verknüpfung grp_id = grpp_id.

626

9

Das Datenbank-Interface DBI

Nachdem das SQL-Statement aufgerufen wurde, lesen wir in einer Schleife alle gefundenen Datensätze aus der Datenbank mit der Methode fetchrow_arrayref(). Sie ist schneller als fetchrow_array(), weil keine Listenkopie gemacht werden muss: 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

while ( defined( my $row = $sth->fetchrow_arrayref() ) ) { $grps{ $row->[ 0 ] } = {}; my $hr = $grps{ $row->[ 0 ] }; ( $hr->{ "id" }, $hr->{ "pid" }, $hr->{ "name" }, $hr->{ "level" }, $hr->{ "status" }, $hr->{ "maxMemberCount" }, $hr->{ "memberCount" }, $hr->{ "desc" } ) = @{ $row }; }

In $row->[ 0 ] steht die Datenbank-ID des Datensatzes, die wir als Hash-Key verwenden, um ein anonymes Subhash für den gelesenen Datensatz aufzubauen. Die Variable $hr ist nur eine Hilfsvariable, damit wir nicht jedes Mal $grps{ $row->[ 0 ] } schreiben müssen. Die Zeilen 88 bis 97 stellen eine Listenzuweisung dar. Auf der rechten Seite steht das Spaltenarray des Datensatzes, bei dem wir mit @ explizit den Datentyp umwandeln müssen, da $row ja eine Referenzvariable ist. Diese Liste wird der links vom Gleichheitszeichen stehenden zugewiesen, die aus den einzelnen Hash-Elementen des anonymen Subhashes besteht. Am Ende geben wir mit der Anweisung return( 1, %grps );

eine Liste zurück, deren erstes Element der Returnstatus für Erfolg ist. Anschließend folgt das Hash mit den gelesenen Datensätzen. Ich gebe hier keine Referenz auf das Hash zurück, sondern eine Kopie des Hashes. Das ist zeitlich aufwändiger, aber programmiertechnisch sauberer. Grundsätzlich sollte man keine Referenzen auf lokale Variablen einer Funktion an den Aufrufer zurückgeben. Alternativ kann man aber der Funktion aus dem aufrufenden Programmcode eine Referenz als Argument übergeben. Damit sind sowohl saubere Programmierung als auch Performance gewährleistet.

Rekursive Strukturen mit DBI

627

Nun wollen wir uns noch ansehen, was das Perl-Skript ausgibt: D:\>printGroups.pl Erwachsene ( id=2 ) Sonstige ( id=7 ) mõnnlich ( id=10 ) weiblich ( id=11 ) Studenten ( id=6 ) mõnnlich ( id=8 ) weiblich ( id=9 ) Kinder ( id=1 ) 10-14 Jahre ( id=4 ) < 10 Jahre ( id=3 ) > 14 Jahre ( id=5 ) D:\>printGroups.pl 2 Sonstige ( id=7 ) mõnnlich ( id=10 ) weiblich ( id=11 ) Studenten ( id=6 ) mõnnlich ( id=8 ) weiblich ( id=9 )

Die seltsamen Zeichen in der Bildschirmausgabe kommen daher, dass der DOS-Zeichensatz eine andere Codierung hat als in Windows. Ich habe zwei Beispiele ausgesucht: Einmal wird das Skript ohne Argument aufgerufen, einmal mit der ID für die Gruppe der Erwachsenen als Parent. Ohne Argument gibt das Skript die gesamte Hierarchie von der ersten Ebene an, während im zweiten Beispiel nur alle Kindobjekte der Gruppe »Erwachsene« beteiligt sind. Zu guter Letzt möchte ich mit Ihnen noch ein objektorientiertes Modul, nennen wir es Group.pm, entwickeln, das Sie in Zusammenhang mit den Internet-Users verwenden können, die wir bereits früher in diesem Kapitel kennen gelernt haben. Was brauchen wir alles an Methoden? Da wären als Erstes Funktionalitäten für den Administrator zu nennen, damit Gruppen angelegt, geändert oder auch gelöscht werden können. Hier werden wir auch wieder auf die rekursive Ausgabe von Gruppen treffen. Natürlich dürfen der Konstruktor sowie verschiedene Getter- und Setter-Methoden nicht fehlen. Gehen wir also den Konstruktor an: package Group; use strict;

628

9

Das Datenbank-Interface DBI

sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { "desc" "id" "level" "maxMemberCount" "memberCount" "name" "pids" "status" };

=> => => => => => => =>

undef, undef, 0, 0, 0, undef, [], "e",

bless( $self, $class ); return $self; } 1;

Die Attribute in der Hash-Referenz sollten uns aus dem Tabellendesign der Datenbank bestens bekannt sein. Wer sich wundert, warum die Reihenfolge hier anders als in der Datenbank ist, dem sei gesagt: Je länger die Liste von Attributen, desto schwieriger wird es, die Übersicht zu behalten. In solchen Fällen empfiehlt sich die alphabetische Sortierung, weil man dann zum Beispiel sofort sieht, ob ein neu aufzunehmendes Attribut bereits existiert. Bei einer unsortierten Liste kann es schon mal passieren, dass man ein Attribut doppelt definiert, ohne es zu bemerken (und vom Interpreter kommt natürlich keine Fehlermeldung, weil es sich nicht um den Identifier einer Variablen handelt, sondern um einen Hash-Key). Ich würde vorschlagen, diesmal sehen wir nur einen leeren Konstruktor ohne Argumente vor. Das ist besonders für eine administrative Anwendung notwendig, um zunächst einmal eine anfängliche Instanz der Klasse zu erzeugen. Die einzelnen Attribute müssen dann anschließend über Setter-Methoden mit aktuellen Werten belegt werden. Implementieren wir als Nächstes die Getter-und Setter-Methoden. Erst wollen wir uns um die Getter-Methoden kümmern. Bis auf das Attribut status können wir ganz normale Getter-Methoden implementieren: sub getDesc { my $self = shift( @_ ); return $self->{ "desc" }; } sub getId { my $self = shift( @_ );

Rekursive Strukturen mit DBI

629

return $self->{ "id" }; } sub getLevel { my $self = shift( @_ ); return $self->{ "level" }; } sub getMaxMemberCount { my $self = shift( @_ ); return $self->{ "maxMemberCount" }; } sub getMemberCount { my $self = shift( @_ ); return $self->{ "memberCount" }; } sub getName { my $self = shift( @_ ); return $self->{ "name" }; } sub getPid { my $self = shift( @_ ); my $pids = $self->{ "pids" }; return $pids->[ $#{ $pids } ]; } sub getPids { my $self = shift( @_ ); return @{ $self->{ "pids" } }; }

Beachtenswert sind die Methoden getPid() und getPids(). Während getPids() eine Liste aller Datenbank-IDs der übergeordneten Gruppen zurückliefert (von oben nach unten in der Hierarchie), gibt getPid() nur die ID der direkt darüber liegenden Gruppe zurück. Warum hat es keinen Sinn, das Attribut status direkt zurückzugeben? In dem Attibut wird der Status in einem internen Format gespeichert. Erinnern wir uns: e steht für »freigeschaltet«, d für »gesperrt«. Für Außenstehende, die unser Modul benut-

zen, soll das keine Rolle spielen; sie interessieren sich nur dafür, ob eine Gruppe gesperrt oder freigeschaltet ist. Deshalb werden wir Instanzmethoden entwickeln, die den Status der Gruppe auf andere Art und Weise zurückgeben: isDisabled() und isEnabled():

630

9

Das Datenbank-Interface DBI

sub isDisabled { my $self = shift( @_ ); return ( $self->{ "status" } eq "d" ) ? 1 : 0; } sub isEnabled { my $self = shift( @_ ); return ! $self->isDisabled(); }

Die Methode isEnabled() ruft ihrerseits wieder isDisabled() auf und negiert deren Returnstatus. Diese Art zu programmieren ist zum einen sehr effizient, weil man sich Programmcode spart, auf der anderen Seite werden Duplikate von bereits geschriebenem Code vermieden. Das hilft wiederum beim Ändern von bereits geschriebenem Programmcode, weil man nur an einer Stelle ändern muss. Nun kommen wir zu den Setter-Methoden. Das sind nur drei: sub setDesc { my $self = shift( @_ ); my $arg = shift( @_ ); $self->{ "desc" } = $arg; return 1; } sub setMaxMemberCount { my $self = shift( @_ ); my $arg = shift( @_ ); unless ( defined( $arg ) ) { $arg = 0; } if ( $arg =~ /[^\d]/ ) { $arg = 0; } my $m = $self->getMemberCount(); $arg = ( $m > $arg ) ? $m : $arg; $self->{ "maxMemberCount" } = $arg; return 1; }

Rekursive Strukturen mit DBI

631

sub setName { my $self = shift( @_ ); my $arg = shift( @_ ); #unless ( # $arg and ( $arg =~ /^[a-zäöü][\w. -]+$/i ) #) { # return undef; #} if ( length( $arg ) > 255 ) { return undef; } $self->{ "name" } = $arg; return 1; }

In der Methode setMaxMemberCount() muss das Argument überprüft werden. Es kann ja sein, dass es gar keine erlaubte Zahl enthält oder dass es kleiner ist als die Zahl der bereits in der Gruppe enthaltenen User. Die Methode setName() lässt nur Gruppennamen zu, die mit einem Buchstaben beginnen. Ansonsten darf der Name nur Buchstaben, Ziffern, den Unterstrich sowie die Zeichen ». -« enthalten. (Auch Leerzeichen sind also erlaubt.) Beim regulären Ausdruck heißt es aufpassen: Der Ausdruck für den Beginn des Namens ist nicht [\w], da in dieser Zeichenklasse auch der Unterstrich sowie Ziffern enthalten sind. Wir müssen also [a-z] verwenden. Allerdings sind in dieser Zeichenklasse keine deutschen Umlaute enthalten, auch dann nicht, wenn wir die Direktive use locale; verwenden. Deshalb müssen wir die Umlaute explizit in die Zeichenklasse mit aufnehmen. Der Code für die Prüfung des Gruppennamens ist hier auskommentiert, da er sonst bei unseren Beispielgruppen für Kinder Probleme bereiten würde, die teilweise mit einem ungültigen Zeichen beginnen. Ich möchte Ihnen aber dennoch zeigen, wie man Namen im Allgemeinen identifiziert. »Ein bisschen wenig Setter-Methoden, oder?«, werden Sie denken. Aber ich möchte Ihnen Stück für Stück erkären, warum für die anderen Attribute keine direkten SetterMethoden benötigt werden bzw. sogar sinnlos sind: 왘 setId() Merken Sie sich: So eine Methode gibt es nicht für Außenstehende! Wann benötigt man die Datenbank-ID? Doch nur, wenn man eine Gruppe lesen möchte. Dann übergibt man die ID der read()-Methode. Eine Alternative wäre es, die DatenbankID im Konstruktor anzugeben, um den Datensatz dort automatisch aus der Daten-

632

9

Das Datenbank-Interface DBI

bank zu lesen, aber sie explizit zu setzen, ist blanker Unsinn. Natürlich gibt es zu jeder Regel eine Ausnahme, wie wir bald feststellen werden, allerdings werden wir die Methode zum direkten Setzen der Datenbank-ID nur privat in der Instanzmethode read() verwenden. 왘 setLevel() Wann wird das Setzen der Ebene einer Gruppe interessant? Doch nur dann, wenn wir eine neue Gruppe anlegen oder eine bereits existierende Gruppe aus der Datenbank lesen. Beide Aktionen werden von Packagemethoden ausgeführt, nicht von außerhalb des Packages liegendem Programmcode. Beim Anlegen einer neuen Gruppe benötigen wir die Nummer der Ebene in der Hierarchie nicht, weil sie bereits durch die Angabe der ID der Parent-Gruppe bekannt ist. Lesen wir einen Datensatz, dann meist durch Angabe der ID der Gruppe. Damit ist aber die Nummer der Ebene in der Hierarchie ebenfalls festgelegt. Also ist die Funktion zum Setzen der Ebene völlig unnötig. Intern wird sie natürlich durch die Methode read() benutzt, aber davon möchte ich später berichten. 왘 setMemberCount() Diese Methode hat überhaupt keine Daseinsberechtigung als öffentliche SetterMethode. Die Anzahl der aktuellen User einer Gruppe ergibt sich nicht dadurch, dass man das Attribut von außerhalb des Packages liegendem Programmcode ansetzt, sondern dadurch, dass man einen neuen User der Gruppe zuordnet (oder auch aus der Gruppe entfernt). 왘 setStatus() Das direkte Setzen dieses Attributs von außen ergibt keinen Sinn, weil in diesem Attribut eine interne Codierung benutzt wird, die nach außen hin gekapselt wird. Programmcode, der unser Package benutzt, wird eine Gruppe nur »sperren« oder »freischalten« wollen, Ich glaube nicht, dass es Sinn hat, beim Sperren einer Gruppe explizit zu sagen: »Setze den Status auf d«. Natürlich werden wir eine intern benutzte Methode _setStatus() benötigen. Da wir keine direkten Setter-Methoden für den Status haben, implementieren wir zwei Methoden für das Sperren bzw. Freischalten einer Gruppe: sub disable { my $self = shift( @_ ); if ( $self->isDisabled() ) { return 1; } $self->{ "status" } = "d"; return 1; }

Rekursive Strukturen mit DBI

633

sub enable { my $self = shift( @_ ); if ( $self->isEnabled() ) { return 1; } $self->{ "status" } = "e"; return 1; }

Nun wollen wir uns eine der wichtigsten Methoden ansehen, nämlich create(). Erst mit dieser Methode können wir überhaupt Gruppen in der Datenbank anlegen. Zuerst der Code für die Methode: sub create { my $self = shift( @_ ); my ( $dbh, $pid ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } if ( $self->getId() ) { return undef; } my $name = $self->getName(); unless ( $name ) { return undef; } if ( defined( $pid ) and ( $pid !~ /^[1-9]\d*$/ ) ) { return undef; } my ( $status, @pids ) = _readParentIds( $dbh, $pid ); return undef unless( $status ); push( @pids, $pid ) if ( $pid ); my $plevel = $#pids; my $level = $plevel + 1;

634

9 my $qname = $dbh->quote( $name ); my $qdesc = defined( $self->getDesc() ) ? $dbh->quote( $self->getDesc() ) : "NULL"; my $qstatus = $dbh->quote( $self->_getStatus() ); my $mmc = $self->getMaxMemberCount(); $pid = 0 unless ( defined( $pid ) );

my $sql = { "mysql_insertid" }; $self->_setId( $id ); foreach $pid ( reverse( @pids ) ) { my $sql = _setId( $id ); $self->_setLevel( $level ); $self->_setPids( @pids ); return 1; }

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI

635

Und nun zu den Erklärungen für die Methode create(): Sie hat ein Pflichtargument, das ist das Datenbank-Handle. Das zweite Argument ist optional und muss die Datenbank-ID der übergeordneten Gruppe enthalten, wenn man eine Kindgruppe anlegen möchte. Es entfällt also, will man eine Gruppe in der obersten Ebene der Hierarchie erzeugen. Wir haben in den Grundlagen weiter vorne in diesem Buch gelernt, dass sowohl die Anzahl als auch der Typ von Funktionsargumenten in Perl beliebig sein kann. Es wäre also möglich, im zweiten Argument pid der Methode create() wahlweise eine Datenbank-ID oder eine Objektreferenz für die Parentgruppe zuzulassen. Hier der Code, wenn man das zweite Argument von create() flexibel gestaltet: my ( $dbh, $pidOrParent ) = @_; ... my $pid = undef; if ( $#_ > 0 ) { if ( ref( $pidOrParent ) ) { $pid = $pidOrParent->getId(); } else { if ( $pidOrParent and ( $pidOrParent =~ /^[1-9]\d*$/ ) ) { $pid = $pidOrParent; } } } ...

Der Programmcode unterstützt nun beide Varianten des Aufrufs: ... # Parentgruppe mit der ID "5" lesen my $grp = new Group(); $grp->read( $dbh, 5 ); # Kindgruppe instanzieren und initialisieren my $childGrp = new Group(); ... # Kindgruppe neu anlegen # Erste Variante: Datenbank-ID wird übergeben $childGrp->create( $dbh, $grp->getId() ); # Zweite Variante: Objekt wird übergeben $childGrp->create( $dbh, $grp );

636

9

Das Datenbank-Interface DBI

Wer schon mit anderen objektorientierten Programmiersprachen Erfahrung gesammelt hat, der kennt diesen Mechanismus unter dem Namen »Overloading«. Dabei werden zwei unterschiedliche Funktionen mit demselben Namen definiert, bei denen sich allerdings die formalen Funktionsparameter unterscheiden. In Perl gibt es nur eine einzige Funktion, hier jedoch sind die Aufrufparameter unterschiedlich. Nach diesem kurzen Ausflug in die Welt der Perl-Mystik nun weiter im Programmcode von create(): Zunächst wird geprüft, ob das erste Argument ein Datenbank-Handle ist und ob die aktuelle Objektinstanz vielleicht schon eine gültige Datenbank-ID besitzt (in diesem Fall kann die Methode create() nicht verwendet werden, weil das Objekt ja bereits existiert), dann wird der Gruppenname aus dem Attribut gelesen. Interessant wird es mit: if ( defined( $pid ) and ( $pid !~ /^[1-9]\d*$/ ) ) { return undef; }

Der Code überprüft nur dann die Datenbank-ID der Parentgruppe, wenn das zweite Argument beim Aufruf der Methode vorhanden ist. Der reguläre Ausdruck ^[1-9]\d*$ ist zwar etwas komplizierter als ^\d+$, führt aber eine genauere Prüfung durch, denn man könnte als ID ja auch 0 oder sogar 00000 angeben. Diese Werte würden durch das zweite Pattern nicht als fehlerhaft erkannt. Als Nächstes lesen wir die Datenbank-IDs aller übergeordneten Gruppen: my ( $status, @pids ) = _readParentIds( $dbh, $pid );

Die Funktion _readParentIds() werden wir noch implementieren. Sie ist eine private statische Methode des Packages und liefert eine Liste aller Parent-IDs für die angegebene Datenbank-ID. Da wir die Gruppe selbst noch nicht angelegt haben, rufen wir die Funktion mit der ID unserer Parentgruppe auf. Das zurückgelieferte Array enthält also die Parent-ID nicht, weil ja die Eltern unseres Parents gelesen werden. Deshalb muss mit der Zeile push( @pids, $pid ) if ( $pid );

unsere Parent-ID in die Liste aufgenommen warden (falls überhaupt eine Parent-ID angegeben war).

Rekursive Strukturen mit DBI

637

In den Zeilen my my my my

$plevel = $#pids; $level = $plevel + 1; $qname = $dbh->quote( $name ); $qdesc = defined( $self->getDesc() ) ? $dbh->quote( $self->getDesc() ) : "NULL"; my $qstatus = $dbh->quote( $self->_getStatus() ); my $mmc = $self->getMaxMemberCount(); $pid = 0 unless ( defined( $pid ) );

bereiten wir lokale Variablen für das SQL-Statement my $sql = _setPids( @pids );

Nachdem wir bereits einige private Methoden benutzt haben, wollen wir sie auch implementieren. Zunächst die statische Packagemethode für das Auslesen der Parent-IDs einer Gruppe: sub _readParentIds { my ( $dbh, $id ) = @_; my @pids = (); return ( 1, @pids ) unless ( $id ); my $sql = execute() ) { return undef; } while ( defined( my $row = $sth->fetchrow_arrayref() ) ) { push( @pids, $row->[ 0 ] ); } $sth->finish(); return ( 1, @pids ); }

Wie wir sehen, funktioniert die Methode auch dann, wenn keine oder eine ungültige Datenbank-ID angegeben wurde. In diesem Fall liefert sie eine leere Liste zurück.

Rekursive Strukturen mit DBI

639

Die Funktion _readParentIds() gibt zwei Parameter zurück, einen skalaren Statuscode und ein Array der gelesenen Parent-IDs. Hier kommt es auf die Reihenfolge an. Würde man zuerst das Array angeben, hätte man ein Problem: return ( @pids, 1 );

weil der zweite Parameter für den Statuscode für den aufrufenden Programmcode leer wäre: my ( @pids, $status ) = _readParentIds( $dbh, $pid );

Der Statuscode wird in diesem fehlerhaften Beispiel nämlich als letztes Element in das Array @pids gepackt. $status wäre demnach undef. Nun sehen wir uns noch die restlichen privaten Methoden an: sub _getStatus { my $self = shift( @_ ); return $self->{ "status" }; } sub _setId { my $self = shift( @_ ); $self->{ "id" } = shift( @_ ); return 1; } sub _setLevel { my $self = shift( @_ ); $self->{ "level" } = shift( @_ ); return 1; } sub _setMemberCount { my $self = shift( @_ ); $self->{ "memberCount" } = shift( @_ ); return 1; } sub _setPids { my $self = shift( @_ ); $self->{ "pids" } = [ @_ ]; return 1; } sub _setStatus { my $self = shift( @_ ); $self->{ "status" } = shift( @_ ); return 1; }

640

9

Das Datenbank-Interface DBI

Die Methoden werden ausschließlich von anderen Funktionen innerhalb des Moduls aufgerufen und sind so einfach, dass ich sie Ihnen ohne weiteren Kommentar präsentieren kann. Nur ein kleiner Hinweis: In der Methode _setPids() verwende ich eckige Klammern bei der Zuweisung: $self->{ "pids" } = [ @_ ];

Das ist deshalb notwendig, weil das Hash-Element pids als Referenz auf ein Array definiert ist. Noch fehlen ein paar Methoden, um das Package Group fertig zu stellen. Bisher sind wir nur in der Lage, neue Gruppen anzulegen, wir benötigen aber auch eine Methode read() zum Lesen einer bereits existierenden Gruppe. Hier ist sie: sub read { my $self = shift( @_ ); my ( $dbh, $id ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } unless ( $id and ( $id =~ /^[1-9]\d*$/ ) ) { return undef; } my $sql = execute() ) { return undef; } my $row = $sth->fetchrow_arrayref(); unless ( defined( $row ) ) { return 0; }

Rekursive Strukturen mit DBI

641

my $i = 0; $self->_setId( $row->[ $i++ ] ); my $pid = $row->[ $i++ ]; $self->setName( $row->[ $i++ ] ); $self->_setLevel( $row->[ $i++ ] ); $self->_setStatus( $row->[ $i++ ] ); $self->setMaxMemberCount( $row->[ $i++ ] ); $self->_setMemberCount( $row->[ $i++ ] ); $self->setDesc( $row->[ $i++ ] ); $sth->finish(); my ( $status, @pids ) = _readParentIds( $dbh, $id ); unless ( $status ) { return undef; } $self->_setPids( @pids ); return 1; }

Die Funktion ist recht einfach, oder? Ich glaube, die meisten Dinge haben wir bereits besprochen. Nur ein paar Anmerkungen möchte ich machen: Die Funktion gibt drei mögliche Returnwerte zurück: TRUE für den Erfolgsfall, d.h., wenn die Gruppe aus der Datenbank gelesen werden konnte. undef im Fehlerfall, wenn irgendetwas schief gelaufen ist. Der dritte Returnwert ist 0, der dann zurückgegeben wird, wenn die Gruppe nicht gefunden werden konnte. In diesem Fall trägt der aufrufende Programmcode die Verantwortung für das, was zu tun ist. Unsere Methode muss diesen Fall jedoch gesondert durch einen eigenen Returnwert behandeln. Der nächste Unterschied zu den anderen Methoden unseres Package findet sich in den Zeilen my $i = 0; $self->_setId( $row->[ $i++ ] ); my $pid = $row->[ $i++ ]; $self->setName( $row->[ $i++ ] ); $self->_setLevel( $row->[ $i++ ] ); $self->_setStatus( $row->[ $i++ ] ); $self->setMaxMemberCount( $row->[ $i++ ] ); $self->_setMemberCount( $row->[ $i++ ] ); $self->setDesc( $row->[ $i++ ] );

Hier wird eine Laufvariable benutzt, um auf die Array-Elemente der Referenzvariablen zuzugreifen, die wir von fetchrow_arrayref() erhalten. Die Elemente werden direkt als Parameter für die Setter-Methoden verwendet. Auf diese Weise spart man sich Hilfsvariablen, und der Programmcode wird sehr schnell. Außerdem können auf diese Art und Weise leichter nachträglich Tabellenspalten hinzugefügt werden (allerdings nur am Ende hinter allen anderen Spalten).

642

9

Das Datenbank-Interface DBI

Schließlich werden noch die Parent-IDs für die Gruppe gelesen. Ich habe für das Setzen von Attributen ausschließlich private Setter-Methoden verwendet, und das aus gutem Grund: Wenn wir die Daten aus der Datenbank lesen, müssen wir die gelesenen Werte nicht mehr überprüfen, weil dies ja bereits beim Anlegen der Datensätze gemacht worden ist. Würden wir jetzt noch einmal eine Prüfung durchführen, dann geht das zu Lasten der Performance. Jetzt benötigen wir noch die Methode write(), um bestehende Datensätze ändern zu können: sub write { my $self = shift( @_ ); my ( $dbh ) = @_; my $id = $self->getId(); unless ( $id ) { return undef; } my $name = $self->getName(); my $status = $self->_getStatus(); my $mmc = $self->getMaxMemberCount(); my $desc = $self->getDesc(); my $qname = $dbh->quote( $name ); my $qstatus = $dbh->quote( $status ); my $qdesc = defined( $desc ) ? $dbh->quote( $desc ) : "NULL"; my $sql = getId(); unless ( $id ) { return undef; }

Dies ist deshalb notwendig, weil man mit dem Default-Konstruktor ja auch Instanzen anlegen kann, die es in der Datenbank noch gar nicht gibt. Hinweis für Performance-Freaks: Natürlich kann man noch eine Logik einbauen, die überprüft, ob sich der Datensatz überhaupt geändert hat. Wenn nicht, würde die Methode dann gar nichts tun. Aber dies würde einen ziemlichen Aufwand bedeuten, denn dann müssten alle Setter-Methoden ein Flag setzen, wenn sich der Wert gegenüber dem vorherigen ändert. Zu guter Letzt fehlt noch die in allen objektorientierten Modulen obligatorische Methode toString(): sub toString { my $self = shift( @_ ); return "id='" . ( $self->getId() ? $self->getId() : "" ) . "'\npids=[" . ( $self->getPids() ? join( " ", $self->getPids() ) : "" ) . "]\nname='" . ( $self->getName() ? $self->getName() : "" ) . "'\nlevel='" . $self->getLevel() . "'\nstatus='" . $self->_getStatus() . "'\nmaxMemberCount='" . $self->getMaxMemberCount() . "'\nmemberCount='" . $self->getMemberCount() . "'\ndesc='" . ( defined( $self->getDesc() ) ? $self->getDesc() : "" ) . "'"; }

Sieht unübersichtlich aus. Aber dafür ist sie relativ kurz. Bei einigen Attributen wie zum Beispiel desc oder name müssen wir überprüfen, ob der Wert definiert oder undef ist. Man kann den Code natürlich schöner schreiben, dafür wird er allerdings länger: my $id = $self->getId() || ""; my @pids = $self->getPids() ? $self->getPids() : (); my $pids = "[" . join( " ", @pids ) . "]"; ...

Sieht schöner aus, macht aber mehr Schreibarbeit.

644

9

Das Datenbank-Interface DBI

Wenn Sie folgenden Code verwenden: my @pids = $self->getPids() || ();

dann haben Sie zwar eine sehr nette Abkürzung geschrieben, sie funktioniert nur leider nicht. Das liegt daran, dass das Ergebnis des Aufrufs $self->getPids() vom Interpreter in einen skalaren Kontext umgewandelt wird. Bei Arrays hat dies zur Folge, dass der sich daraus ergebende Wert die Anzahl der Array-Elemente ist. Dieser Wert nun wird auf logisch TRUE geprüft und zurückgegeben, wenn das ArrayElemente enthält. Ich glaube, das war dann wohl doch nicht im Sinne des Erfinders. Damit Sie den Programmcode unseres Moduls in einem Stück sehen, hier noch einmal alle Einzelteile zusammengefasst: package Group; use strict; ##################################################### # # Private Packagemethoden # ##################################################### sub _readParentIds { my ( $dbh, $id ) = @_; my @pids = (); return ( 1, @pids ) unless ( $id ); my $sql = execute() ) { return undef; } while ( defined( my $row = $sth->fetchrow_arrayref() ) ) {

Rekursive Strukturen mit DBI push( @pids, $row->[ 0 ] ); } $sth->finish(); return ( 1, @pids ); } ##################################################### # # Öffentliche Packagemethoden # ##################################################### ##################################################### # # Private Instanzmethoden # ##################################################### sub _getStatus { my $self = shift( @_ ); return $self->{ "status" }; } sub _setId { my $self = shift( @_ ); $self->{ "id" } = shift( @_ ); return 1; } sub _setLevel { my $self = shift( @_ ); $self->{ "level" } = shift( @_ ); return 1; } sub _setMemberCount { my $self = shift( @_ ); $self->{ "memberCount" } = shift( @_ ); return 1; } sub _setPids { my $self = shift( @_ ); $self->{ "pids" } = [ @_ ]; return 1; }

645

646

9

sub _setStatus { my $self = shift( @_ ); $self->{ "status" } = shift( @_ ); return 1; } sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { "desc" "id" "level" "maxMemberCount" "memberCount" "name" "pids" "status" };

=> => => => => => => =>

undef, undef, 0, 0, 0, undef, [], "e",

bless( $self, $class ); return $self; } ##################################################### # # Getter-Methoden # ##################################################### sub getDesc { my $self = shift( @_ ); return $self->{ "desc" }; } sub getId { my $self = shift( @_ ); return $self->{ "id" }; } sub getLevel { my $self = shift( @_ ); return $self->{ "level" }; } sub getMaxMemberCount { my $self = shift( @_ ); return $self->{ "maxMemberCount" }; }

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI sub getMemberCount { my $self = shift( @_ ); return $self->{ "memberCount" }; } sub getName { my $self = shift( @_ ); return $self->{ "name" }; } sub getPid { my $self = shift( @_ ); my $pids = $self->{ "pids" }; return $pids->[ $#{ $pids } ]; } sub getPids { my $self = shift( @_ ); return @{ $self->{ "pids" } }; } ##################################################### # # Setter-Methoden # ##################################################### sub setDesc { my $self = shift( @_ ); my $arg = shift( @_ ); $self->{ "desc" } = $arg; return 1; } sub setMaxMemberCount { my $self = shift( @_ ); my $arg = shift( @_ ); unless ( defined( $arg ) ) { $arg = 0; } if ( $arg =~ /[^\d]/ ) { $arg = 0; } my $m = $self->getMemberCount(); $arg = ( $m > $arg ) ? $m : $arg;

647

648

9 $self->{ "maxMemberCount" } = $arg; return 1;

} sub setName { my $self = shift( @_ ); my $arg = shift( @_ ); #unless ( # $arg and ( $arg =~ /^[a-zäöü][\w. -]+$/i ) #) { # return undef; #} if ( length( $arg ) > 255 ) { return undef; } $self->{ "name" } = $arg; return 1; } ##################################################### # # Öffentliche Instanzmethoden # ##################################################### sub create { my $self = shift( @_ ); my ( $dbh, $pid ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } if ( $self->getId() ) { return undef; } my $name = $self->getName(); unless ( $name ) { return undef; }

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI if ( defined( $pid ) and ( $pid !~ /^[1-9]\d*$/ ) ) { return undef; } my ( $status, @pids ) = _readParentIds( $dbh, $pid ); return undef unless( $status ); push( @pids, $pid ) if ( $pid ); my my my my

$plevel = $#pids; $level = $plevel + 1; $qname = $dbh->quote( $name ); $qdesc = defined( $self->getDesc() ) ? $dbh->quote( $self->getDesc() ) : "NULL"; my $qstatus = $dbh->quote( $self->_getStatus() ); my $mmc = $self->getMaxMemberCount(); $pid = 0 unless ( defined( $pid ) ); my $sql = { "mysql_insertid" }; $self->_setId( $id ); foreach $pid ( reverse( @pids ) ) { my $sql = _setId( $id ); $self->_setLevel( $level ); $self->_setPids( @pids ); return 1; } sub disable { my $self = shift( @_ ); if ( $self->isDisabled() ) { return 1; } $self->{ "status" } = "d"; return 1; } sub enable { my $self = shift( @_ ); if ( $self->isEnabled() ) { return 1; } $self->{ "status" } = "e"; return 1; } sub isDisabled { my $self = shift( @_ ); return ( $self->{ "status" } eq "d" ) ? 1 : 0; } sub isEnabled { my $self = shift( @_ ); return ! $self->isDisabled(); }

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI sub read { my $self = shift( @_ ); my ( $dbh, $id ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } unless ( $id and ( $id =~ /^[1-9]\d*$/ ) ) { return undef; } my $sql = execute() ) { return undef; } my $row = $sth->fetchrow_arrayref(); unless ( defined( $row ) ) { return 0; } my $i = 0; $self->_setId( $row->[ $i++ ] ); my $pid = $row->[ $i++ ]; $self->setName( $row->[ $i++ ] ); $self->_setLevel( $row->[ $i++ ] ); $self->_setStatus( $row->[ $i++ ] ); $self->setMaxMemberCount( $row->[ $i++ ] ); $self->_setMemberCount( $row->[ $i++ ] ); $self->setDesc( $row->[ $i++ ] ); $sth->finish(); my ( $status, @pids ) = _readParentIds( $dbh, $id ); unless ( $status ) { return undef; } $self->_setPids( @pids );

651

652

9 return 1;

} sub write { my $self = shift( @_ ); my ( $dbh ) = @_; my $id = $self->getId(); unless ( $id ) { return undef; } my $name = $self->getName(); my $status = $self->_getStatus(); my $mmc = $self->getMaxMemberCount(); my $desc = $self->getDesc(); my $qname = $dbh->quote( $name ); my $qstatus = $dbh->quote( $status ); my $qdesc = defined( $desc ) ? $dbh->quote( $desc ) : "NULL"; my $sql = getId() ? $self->getId() : "" ) . "'\npids=[" . ( $self->getPids() ? join( " ", $self->getPids() ) : "" ) . "]\nname='" . ( $self->getName() ? $self->getName() : "" ) . "'\nlevel='" . $self->getLevel() . "'\nstatus='" . $self->_getStatus() .

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI

653

"'\nmaxMemberCount='" . $self->getMaxMemberCount() . "'\nmemberCount='" . $self->getMemberCount() . "'\ndesc='" . ( defined( $self->getDesc() ) ? $self->getDesc() : "" ) . "'"; } 1;

Bevor wir darangehen, anhand eines CGI-Beispiels den Umgang mit unserem Modul zu lernen, möchte ich Ihnen noch einmal kurz »Factories« vorstellen, die wir bereits im Kapitel über objektorientierte Programmierung kennen gelernt haben. Das Modul Group.pm enthält alle Funktionalitäten, die für die Instanz einer Gruppe notwendig sind. Wie wir aber am Beispiel printGroups.pl gesehen haben, benötigen wir auch Funktionen, die nicht nur ein Objekt der Klasse behandeln, sondern sich auf mehrere Instanzen beziehen. Wenn wir weiter an User denken, kommen auch Funktionen hinzu, die sowohl Gruppen- als auch Userdaten verarbeiten. Für diese übergreifenden Aktionen implementiert man Factories. Eine solche möchte ich Ihnen in Form des Moduls GroupFactory kurz vorstellen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

package GroupFactory; use strict; use Group; sub new { my $proto = shift( @_ ); my $class = ref( $proto ) || $proto; my $self = { }; bless( $self, $class ); return $self; } sub readChildren { my $self = shift( @_ ); my ( $dbh, $grp ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } my %children = ();

654 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

9 my @cids = (); my $sql = undef; unless ( $grp and ref( $grp ) ) { $sql = getLevel() + 1; $sql = execute() ) { return undef; } while ( defined( my $row = $sth->fetchrow_arrayref() ) ) { push( @cids, $row->[ 0 ] ); } $sth->finish(); foreach my $cid ( @cids ) { my $g = new Group(); unless ( $g->read( $dbh, $cid ) ) { return undef; } $children{ $g->getId() } = $g; } return ( 1, %children );

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI 81 } 82 83 sub 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 } 114 115 1;

655

readParents { my $self = shift( @_ ); my ( $dbh, $grp ) = @_; unless ( $dbh and ( $dbh =~ /dbi/i ) ) { return undef; } unless ( $grp and ref( $grp ) ) { return undef; } my @parents = (); unless ( $grp->getLevel() ) { return ( 1, @parents ); } foreach my $pid ( $grp->getPids() ) { my $g = new Group(); unless ( $g->read( $dbh, $pid ) ) { return undef; } push( @parents, $g ); } return ( 1, @parents );

Wie wir am Konstruktor new() sehen, ist auch die Factory objektorientiert. Obwohl das nicht notwendig ist (wie wir am leeren Hash und damit an den fehlenden Attributen erkennen können), finde ich es in der heutigen Zeit besser, so viel wie möglich auf prozedurale Programmierung zu verzichten. Außerdem könnte man später ja doch Attribute für die Factory vorsehen, und dann hat man mit dem objektorientierten Ansatz sowieso bessere Karten. Unsere Factory hat zwei Methoden, eine zum Lesen der Parent-Objekte, die andere zum Lesen der direkten Kind-Objekte einer Gruppe. Während wir im Gruppenmodul immer mit Datenbank-IDs hantiert hatten, werden in der Factory stattdessen Gruppenobjekte übergeben. Die Methode readChildren() erhält also ein Gruppenobjekt als Argument und gibt auch ein Hash bestehend aus Gruppeninstanzen zurück.

656

9

Das Datenbank-Interface DBI

Bei der Methode readParents() ist das nicht anders, nur wird hier ein Array von Gruppenobjekten zurückgeliefert, dessen Elemente nach ihrer Ebene in der Hierarchie sortiert sind. Beide Methoden benutzen wiederum unsere Klasse »Group«, um die Instanzen zu erzeugen und die Datensätze einzulesen.

CGI-Beispiel Jetzt haben wir alle Grundsteine gelegt, um eine kleine CGI-Anwendung zur Administration von Gruppen schreiben zu können. Wir nennen das CGI-Skript grpAdmin.pl. Gemeinsam mit den beiden Modulen Group.pm und GroupFactory.pm kommt es in das CGI-Verzeichnis des Webservers (bei Apache also ins Verzeichnis cgi-bin). Um das Beispiel übersichtlich zu halten, habe ich nur ein paar Funktionalitäten implementiert, und zwar das Browsen durch die Hierarchie und das Anlegen einer neuen Gruppe. Sehen wir uns als Erstes die Oberfläche der Anwendung an. Die Datenbank enthält die Gruppen, die wir bereits früher angelegt haben: Das erste Bild zeigt die Ausgabe des Skripts, wenn es ohne CGI-Argumente über den Browser aufgerufen wird:

Abbildung 9.17: Ursprüngliche Ausgabe von grpAdmin.pl

Rekursive Strukturen mit DBI

657

Ganz oben erhalten wir die Information, wo wir uns befinden, nämlich in der obersten Ebene der Hierarchie. Darunter kommt eine Tabelle aller Gruppen der ersten Ebene. Ganz unten erhalten wir die Möglichkeit, eine neue Gruppe anzulegen. Wenn wir nun die Gruppe der Erwachsenen anklicken und anschließend auf »Sonstige«, dann gehen wir mit jedem Klick eine Ebene tiefer und erhalten folgende Ausgabe:

Abbildung 9.18: Ausgabe von grpAdmin.pl für die Gruppe »Sonstige«

Wenn wir uns nicht in der obersten Ebene der Hierarchie befinden, enthält die Seite auch einen Button, damit man eine Stufe nach oben navigieren kann.

658

9

Das Datenbank-Interface DBI

Nun gehen wir noch eine Ebene tiefer und klicken auf »männlich« (diese Gruppe ist bisher noch leer, d.h., sie enthält keine Untergruppen). Wir bekommen als Ausgabe:

Abbildung 9.19: Ausgabe von grpAdmin.pl für eine Gruppe ohne Kinder

Rekursive Strukturen mit DBI

659

Jetzt legen wir eine neue Gruppe mit folgenden Daten an:

Abbildung 9.20: Ausgefülltes Formular für eine neue Gruppe

Wenn wir auf den Button GRUPPE ANLEGEN klicken, wird die neue Gruppe als Kind von »Erwachsene-Sonstige-männlich« angelegt, und wir erhalten die Ausgabe:

660

9

Das Datenbank-Interface DBI

Abbildung 9.21: Ausgabe von grpAdmin.pl nach dem Anlegen der neuen Gruppe

Wir verwenden natürlich unser bereits früher entwickeltes Modul Template.pm für die Ausgabe. Hier ist das zugehörige Template (im Beispiel muss es in der Datei D:\templates\groupList.html gespeichert sein): 01 02

03 Gruppen 04



$$[isRoot] Oberste Ebene$$[/isRoot] $$[isNotRoot]Kinder der Gruppe $$(parentName)$$[/isNotRoot] bestehende Gruppen $${*} $${/*}$$[upNav] $$[/upNav]
Name Beschreibung Status max. Anzahl Mitglieder aktuelle Anzahl Mitglieder
$$(name) $$(desc) $$(status) $$(mmc) $$(mc)

Neue Gruppe in dieser Ebene anlegen

661

662

9

Das Datenbank-Interface DBI

65 71 72 73 74 76 77 78 81 82
Name
Beschreibung $$(gdesc)
Status 67 freigeschaltet 68

70 gesperrt
max. Anzahl Mitglieder
83

84

Für die Darstellung im Buch sind einige Zeilen umbrochen. In der Datei selbst sollten die Zeilen nur dann umbrochen werden, wenn dadurch keine Layoutveränderung stattfindet. Die Zeilen 04 bis 14 enthalten JavaScript. Darin werden zwei Funktionen definiert: setPid() und setAction(): setPid() wird vom Hypertextlink um den Gruppennamen herum verwendet, um eine Ebene tiefer zu gehen. Sie setzt den Wert des versteckten Formularfeldes »pid« auf die Datenbank-ID der Gruppe, die angeklickt wurde, und schickt anschließend das Formular ab. setAction() wird beim Klick auf den Button zum Anlegen einer neuen Gruppe auf-

gerufen. Sie setzt den Wert des versteckten Feldes act auf den Wert create, um dem CGI-Skript mitzuteilen, dass es eine neue Gruppe anlegen soll. Anschließend schickt es ebenfalls das Formular ab. Da der Name »ACTION« für HTML-Formulare reserviert ist, kann er nicht noch einmal für eigene Formularfelder verwendet werden. Aus diesem Grund habe ich das versteckte Feld act genannt. Für die Templatevariable $$(formAction) wird vom Skript der eigene URI eingesetzt. Für die Variable $$(pid) setzt es die Datenbank-ID der aktuellen Parentgruppe ein.

Rekursive Strukturen mit DBI

663

Für die Überschrift werden zwei Bereichsvariablen verwendet: $$[isRoot] kennzeichnet denjenigen Text, der für die oberste Ebene der Hierarchie ausgegeben werden soll, der Text in $$[isNotRoot] wird nur eingesetzt, wenn man sich mindestens in der zweiten Ebene befindet. In diesem Fall wird statt der Variable $$(parentName) der Name der Parentgruppe eingesetzt. Die Zeilen 35 bis 42 enthalten einen Schleifenbereich, der so oft ausgegeben wird, wie Untergruppen vorhanden sind, und durch die Templatevariable $${*} gekennzeichnet ist. Die einzelnen Templatevariablen dieses Loopteils sind selbsterklärend, für sie setzt das Skript die aktuellen Gruppendaten ein. Um den Gruppennamen wird ein Hypertextlink gelegt, in dem per JavaScript die Funktion setPid() aufgerufen wird, die, wie wir oben schon gesehen haben, das versteckte Feld für die Parent-ID neu setzt und anschließend das Formular abschickt. Anschließend kommt in den Zeilen 43 bis 48 ein ausblendbarer Bereich, gekennzeichnet durch die Templatevariable $$[upNav], der nur dann angezeigt wird, wenn man sich nicht in der obersten Ebene der Hierarchie befindet. Im ausblendbaren Bereich wird für die Variable $$(ppid) die Datenbank-ID der über dem aktuellen Parent liegenden Gruppe eingesetzt. In den Zeilen 43 bis 76 folgen die Formularfelder zum Anlegen einer neuen Gruppe unterhalb der aktuellen Parentgruppe. Der Button zum Anlegen einer neuen Gruppe (Zeile 77 bis 81) enthält im Attribut onclick einen JavaScript-Aufruf der Funktion setAction(), die wir schon weiter oben

besprochen haben. Soweit zum Template. Nun sehen wir uns das Skript grpAdmin.pl an. Die Hashbang-Zeile muss evtl. an Ihre Rechnerkonfiguration angepasst werden. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16

#!D:/Perl/bin/perl.exe -w BEGIN { print( "Content-Type: text/html\n\n" ); } use use use use use use

strict; Group (); GroupFactory (); Template (); CGI qw( :cgi ); DBI ();

our $cgi = new CGI(); our $dbh = DBI->connect( "dbi:mysql:test" );

664 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

9 our $templPath = "D:/templates/groupList.html"; my $pid = $cgi->param( "pid" ); my $grp = undef; if ( $pid and ( $pid =~ /^[1-9]\d*$/ ) ) { $grp = new Group(); unless ( $grp->read( $dbh, $pid ) ) { print( "cannot read group with pid $pid" ); exit( 1 ); } } my %subs = (); my $action = $cgi->param( "act" ); if ( $action and ( $action eq "create" ) ) { my $gname = $cgi->param( "gname" ); my $gdesc = $cgi->param( "gdesc" ); my $gstatus = $cgi->param( "gstatus" ); my $gmmc = $cgi->param( "gmmc" ); if ( defined( $gmmc ) ) { if ( length( $gmmc ) == 0 ) { $gmmc = 0; } if ( $gmmc =~ /[^\d]/ ) { $gmmc = 0; } } else { $gmmc = 0; } my $g = new Group(); unless ( $g->setName( $gname ) ) { print( "Falscher Gruppenname angegeben" ); exit( 1 ); } $g->setDesc( $gdesc ); if ( $gstatus ne "e" ) { $g->disable(); } $g->setMaxMemberCount( $gmmc ); if ( defined( $pid ) and ( $pid eq "0" ) ) { $pid = undef; } unless ( $g->create( $dbh, $pid ) ) { print( "Fehler beim Anlegen der Gruppe" );

Das Datenbank-Interface DBI

Rekursive Strukturen mit DBI 69 exit( 1 ); 70 } 71 } 72 73 $subs{ "enabledChecked" } = " checked"; 74 75 my $fac = new GroupFactory(); 76 my ( $status, %grps ) = 77 $fac->readChildren( $dbh, $grp ); 78 79 unless ( $status ) { 80 print( "Fehler in readChildren\n" ); 81 exit( 1 ); 82 } 83 84 my $templ = new Template( $templPath ); 85 unless ( $templ ) { 86 print( "Fehler beim Lesen von $templPath" ); 87 exit( 1 ); 88 } 89 90 $subs{ "formAction" } = $ENV{ "SCRIPT_NAME" }; 91 $subs{ "pid" } = $pid ? $pid : 0; 92 93 if ( $grp ) { 94 $subs{ "isNotRoot" } = 1; 95 $subs{ "parentName" } = $grp->getName(); 96 $subs{ "upNav" } = 1; 97 $subs{ "ppid" } = $grp->getPid(); 98 } 99 else { 100 $subs{ "isRoot" } = 1; 101 } 102 103 print( $templ->substitute( \%subs ) ); 104 105 $templ->nextPart(); 106 107 foreach my $id ( sort( { $grps{ $a }->getName() cmp 108 $grps{ $b }->getName() } keys( %grps ) ) ) { 109 my $grp = $grps{ $id }; 110 111 $subs{ "id" } = $id; 112 $subs{ "name" } = $grp->getName(); 113 $subs{ "desc" } = $grp->getDesc(); 114 $subs{ "status" } = $grp->isEnabled() ? 115 "freigeschaltet" : "gesperrt"; 116 $subs{ "mmc" } = $grp->getMaxMemberCount(); 117 $subs{ "mc" } = $grp->getMemberCount(); 118 print( $templ->substitute( \%subs ) ); 119 } 120

665

666 121 122 123 124 125 126 127 128 129 130 131

9

Das Datenbank-Interface DBI

$templ->nextPart(); print( $templ->substitute( \%subs ) ); exit( 0 ); END { $dbh->disconnect() if ( $dbh ); } 1;

Als Erstes wird in einem BEGIN-Block der unbedingt notwendige HTTP-Header ausgegeben. In den Zeilen 15 und 17 müssen Sie das Skript anpassen, wenn Sie eine andere Konfiguration haben. In Zeile 19 holt sich das Skript den Wert des Formularfeldes »pid«, das ja die Datenbank-ID derjenigen Gruppe enthält, deren Kinder angezeigt werden soll. Im darauf folgenden Code (Zeile 20 bis 28) wird die Parentgruppe aus der Datenbank gelesen und in $grp abgelegt, falls pid eine gültige ID enthält. Bevor wir Daten ausgeben, müssen wir nachprüfen, ob der Anwender eine neue Gruppe angelegt haben möchte, was durch den Wert create im Feld act gekennzeichnet ist. In diesem Fall muss die neue Gruppe vor dem Auslesen der Daten angelegt werden, da sie sonst nicht in der Liste erscheint. Diese Aktion passiert in den Zeilen 32 bis 71. Zuerst holen wir uns die Formularfelder. In den Zeilen 38 bis 48 müssen wir den Wert von gmmc überprüfen, der die Anzahl maximaler Gruppenmitglieder enthält. Falls das Feld gar nicht vorhanden oder falsch ausgefüllt ist, setzen wir eine 0 ein. Danach instanzieren wir ein neues Gruppenobjekt, setzen die Attribute und rufen die Methode create() zum Anlegen des Datensatzes auf. Erwähnenswert ist: if ( defined( $pid ) and ( $pid eq "0" ) ) { $pid = undef; }

Damit wird verhindert, dass die Parent-ID auf 0 gesetzt wird. Das ist notwendig, weil wir im Package Group ansonsten durch die Prüfung if ( defined( $pid ) and ( $pid !~ /^[1-9]\d*$/ ) ) { return undef; }

einen Fehler zurückgeben würden.

Rekursive Strukturen mit DBI

667

Zeile 73 sorgt dafür, dass der Radiobutton für den Status der neu anzulegenden Gruppe standardmäßig auf »freigeschaltet« gesetzt wird. Nun sind wir mit den Vorarbeiten fertig. In den Zeilen 75 bis 82 holen wir uns eine Instanz von GroupFactory und lesen damit alle Kinder der aktuellen Parentgruppe. Ab Zeile 84 beginnen das Ausfüllen des Templates und die Ausgabe der Daten. Wichtig: Das Template enthält eine Schleifenvariable und besteht damit aus drei Teilen. Im Hash %subs belegen wir die Elemente für die Ersetzungen im Template zunächst mit den Werten für den ersten und letzten Template-Teil. Beide werden ja nur einmalig ausgegeben. In den Zeilen 93 bis 101 werden die ausblendbaren Bereiche behandelt. Kennzeichen für die oberste Ebene ist, dass in diesem Fall die Variable $grp keine gültige Objektreferenz enthält, weil es ja für die erste Ebene keine Parentgruppe gibt. In der Schleife (Zeile 107 bis 128) wird mit jedem Schleifendurchlauf der mittlere Templatebereich ausgegeben. Für all diejenigen, die beim Anblick des »sort()«-Aufrufs die Hände über dem Kopf zusammenschlagen und ausrufen: »Um Himmels Willen, was ist das denn?«, hier eine kurze Anleitung: Die Funktion keys() liefert eine Liste von Datenbank-Ids. Diese wird der Funktion sort() übergeben. sort() wiederum definiert die zwei speziellen Variablen $a und $b für

die Sortierung. Das heißt, dass in diesen beiden Variablen jeweils eine Datenbank-ID gespeichert ist. Wir wollen nun aber nicht die IDs für die Sortierung verwenden, sondern die Gruppennamen, und diese stehen ja im Attribut name des jeweiligen Objekts, das durch die Datenbank-ID referenziert wird. $grps{ $a }

und

$grps{ $b }

enthalten

also

die

Objektreferenzen,

und

$grps{ $a }->getName() liefert den Gruppennamen des ersten zu vergleichenden Ele-

ments für die Sortierung. Das ist zwar immer noch ziemlich kompliziert, aber ich hoffe, es ist nun ein bisschen klarer geworden. Im Package GroupFactory haben wir keinerlei Sortierung beim Lesen von Gruppen vorgesehen und überlassen dies dem Programmcode, der das Package benutzt. Man kann natürlich Erweiterungen implementieren, indem man unterschiedliche Lesemethoden schreibt, die dann selbst eine Sortierung durchführen. Oder man erweitert die Lesemethode so, dass sie über ein Zusatzargument verschiedene Sortieralgorithmen unterstützt. Nun weiter im Programmcode: Mit dem Statement in Zeile 109 verwende ich eine abkürzende Hilfsvariable, damit wir nicht jedes Mal $grps{ $id } schreiben müssen.

668

9

Das Datenbank-Interface DBI

In den Zeilen 111 bis 118 schließlich werden die Ersetzungswerte der Templatevariablen mit den aktuellen Gruppendaten belegt. Mit diesem Beispiel haben wir schon eine kleine Webanwendung geschrieben. Allerdings muss hinzugefügt werden, dass es für eine richtige Anwendung noch viele Punkte gibt, die man beachten muss. So fehlt in unserem Skript jegliche Navigation in einem Menü. Man kann auch keine Gruppen löschen oder ändern. Speziell bei der Frage »Wie soll sich meine Anwendung dem User präsentieren?« gibt es viele Aspekte zu bedenken. Verwendet man nur ein einziges Browserfenster, nutzt man die Möglichkeit von Frames, oder zeigt man unterschiedliche Funktionalitäten in eigenen Browserfenstern an etc. Meist geht man bei umfangreicheren Applikationen so vor, dass zuerst sowohl ein Layoutdesign als auch ein Katalog von Funktionalitäten festgelegt werden, bevor man darangeht, Programmcode zu schreiben. Auf einen Aspekt möchte ich hier noch näher eingehen, der anfangs meist nicht so sehr im Mittelpunkt steht, dafür aber später oft zu umfangreichen Änderungen führen kann: die Datenmenge. Was habe ich schon geflucht, als ich in einem größeren Projekt mit der Oberfläche für die Userverwaltung eines gekauften Produkts zu kämpfen hatte. In der Datenbank waren an die 20.000 User gespeichert. In der Webanwendung waren auch Formulare zum Bearbeiten der Userdaten vorhanden. Damit nicht jedes Mal alle 20.000 User in einem einzigen Formular angezeigt werden, haben sich die Entwickler des teuren Produkts eine Begrenzung auf maximal 50 User pro Seite einfallen lassen, doch eine Suchmöglichkeit war nicht vorgesehen. Noch dazu waren die Datensätze nicht lexikalisch sortiert. Jetzt suchen Sie einmal nach einem bestimmten User, der irgendwo in einem von 400 Formularen versteckt ist. Sie werden bald die Lust verlieren. Bei der Begrenzung der anzuzeigenden Datensätze kann man verschiedene Ansätze wählen. Die schnellste Methode ist dabei sicherlich das Eingrenzen bereits im Datenbankserver. Manche Datenbanken bieten in ihrer SQL-Syntax zu diesem Zweck eigene Direktiven bzw. Schlüsselwörter an (Mysql kennt z.B. das Schlüsselwort »LIMIT«, mit dem die Anzahl der zurückgelieferten Zeilen beschränkt werden kann). Wenn das Einschränken der Datenmenge nicht im Datenbankserver möglich ist, sollte man dies durch eine entsprechende Programmlogik in seinem Modul oder Skript selbst machen. Es hat überhaupt keinen Sinn, zuerst 20.000 Datensätze zu lesen, diese anschließend zu sortieren und dann nur die ersten 100 anzuzeigen. Besser ist es, das SQL-Statement so zu schreiben, dass die Sortierung durch den Datenbankserver erfolgt. Anschließend werden in der Leseschleife nur die ersten 100 Zeilen gelesen. Will man später weitere 100 Datensätze lesen, zum Beispiel, weil der Anwender im Browser auf den Button »Nächste Seite« geklickt hat, dann muss man im Programmcode die ersten 100 Datensätze zwar lesen, aber man speichert sie nicht, sondern »überspringt« sie, und liest anschließend die nächsten 100 Sätze.

Mehrsprachige Datensätze

669

9.5 Mehrsprachige Datensätze In der Zeit von Globalisierung und weltumspannenden Firmennetzen ist man immer mehr gezwungen, seine Web-Inhalte nicht nur in einer Sprache anzubieten, sondern für mehrere Sprachvarianten lokalisierte Texte zu erstellen. Bei statischen Inhalten werden hierfür für jedes Dokument sprachspezifische Varianten erstellt, und diese in Verzeichnissen für die jeweilige Sprache gespeichert. Meist sind diese Verzeichnisse durch das Sprachkürzel im Namen gekennzeichnet, zum Beispiel de für Deutsch, en für Englisch usw. Diese Sprachunterscheidung spiegelt sich in den URIs der einzelnen Dokumenten wider: # URI für eine deutsche Homepage http://my.server.domain/public/de/index.html # Dieselbe Datei in Englisch: http://my.server.domain/public/en/index.html

Bei einem solchen festen Schema ist die vom Anwender benutzte Sprache anhand eines Verzeichnisses im URI erkennbar. Sollen nun mit Hilfe von CGI -Skripts dynamische Inhalte ebenfalls mehrsprachig unterstützt werden, dann muss man in der Datenbank für jeden Datensatz alle Texte, die in der Browseroberfläche sichtbar sind, ebenfalls in mehreren Sprachvarianten speichern. Das Problem ist, dass man nicht von vorneherein weiß, welche Sprachvarianten vorkommen, denn meist beginnt man mit nur zwei Sprachen, erweitert die Liste der unterstützten Locales dann aber durch weitere Einträge. Es ist also nicht einfach möglich, die lokalisierten Texte in den Datensatz selbst mit aufzunehmen, z.B. wie folgt: CREATE TABLE product ( p_id BIGINT( 15 ) NOT NULL AUTO_INCREMENT PRIMARY KEY, p_label_de VARCHAR( 255 ) BINARY, p_label_en VARCHAR( 255 ) BINARY, p_label_fr VARCHAR( 255 ) BINARY # Natürlich kommen hier in der Praxis # weitere Spalten ... );

Will man später weitere Sprachvarianten hinzufügen, dann muss die Struktur der Datenbanktabelle geändert werden, was zwar möglich, aber nicht besonders geschickt ist.

670

9

Das Datenbank-Interface DBI

Wesentlich eleganter ist die Verwendung einer eigenen Tabelle für die lokalisierten Texte: CREATE TABLE product ( p_id BIGINT( 15 ) NOT NULL AUTO_INCREMENT PRIMARY KEY # natürlich kommen hier in der Praxis # weitere Spalten ... ); CREATE TABLE product_label ( pl_p_id BIGNINT( 15 ) NOT NULL, pl_locale VARCHAR( 16 ) NOT NULL, pl_text LONGBLOB NOT NULL, UNIQUE( pl_p_id, pl_locale ) );

Mit dieser Methode liest man einen Datensatz für eine bestimmte Sprachvariante zum Beispiel so aus: my readProduct { my ( $dbh, $id, $locale, $href ) = @_; # $dbh ist das Datenbank-Handle, # $id ist die Datenbank-ID des Produkts, # $locale ist die Sprachvariante, # z.B. "de", "en" etc. # $href ist eine Hash-Referenz, die für das # Zurückgeben der Daten verwendet wird. # Hilfsvariable, in welcher der Locale# String für die Datenbank in Quotes gesetzt wird my $qlocale = $dbh->quote( $locale ); # # # # # # # #

Hinweis: Im folgenden SQL-Statement wird der Alias "p" für die Tabelle "product" nur deshalb verwendet, weil man sonst alle Spalten beider Tabellen erhält, wir benötigen aus der Tabelle "product_label" aber nur die Spalte 'pl_text'. Wir kennen das Problem mitterweile bestens unter dem Namen "Kreuzprodukt" bei Tabellenjoins.

my $sql = execute() ) { return undef; } my $row = $sth->fetchrow_arrayref(); # $row ist eine Referenz auf ein Array und # enthält alle Spalten der Zeile von # "product" sowie die Spalte "pl_text" der # Tabelle "product_label" $sth->finish(); # Abspeichern des Datensatzes in der # Hash-Referenz (hier nur beispielhaft gezeigt) $href->{ "id" } = $row->[ 0 ]; ... return 1; }

In einem CGI -Skript kann man bei der Verwendung des obigen LokalisierungsSchemas (der Locale-String ist Bestandteil des URIs) den Locale-String mit folgendem Programmcode ermitteln: my $myUri = $ENV{ "SCRIPT_NAME" }; my ( $locale ) = $myUri =~ m~/([a-z]{2})/~; # Locale-Strings bestehen immer # aus 2 Kleinbuchstaben, z.B. 'de', 'en'. # oder: my ( $locale ) = $myUri =~ m~/([a-z]{2}_[A-Z]{2})/; # Locale-Strings haben die Form xx_YY # z.B. "de_DE" oder "en_EN".

Im gezeigten Pattern Matching ist der Slash ein ganz normales Zeichen, da ich den Matchingoperator m~ verwendet habe. Es wird also nach einem Teil-URI aus zwei Buchstaben gesucht, z.B. /en/. Mehrsprachigkeit erfordert, dass auch CGI -Skripts mehrfach unter verschiedenen Verzeichnissen vorhanden sind, da der Locale-String ja im URI gespeichert wird und somit für jede Sprachvariante ein eigenes Verzeichnis vorhanden sein muss. Im Unterschied zu statischen Dokumenten haben aber die einzelnen CGI -Skripts immer denselben Inhalt, es können also entweder Kopien der Dateien verwendet werden, oder (unter UNIX) symbolische Links statt Dateikopien. (Hier muss man dem Webserver mit einer Konfigurationsdirektive mitteilen, dass er symbolische Links auflösen soll, zum Beispiel durch Options +FollowSymLinks beim ApacheWebserver).

672

9

Das Datenbank-Interface DBI

Alternativ kann man den Locale-String auch im Querystring jedes einzelnen Requests (ineffizient) oder in einem Cookie speichern. Allerdings kann der Anwender in diesem Fall nicht einfach die Sprachvariante wechseln, indem er zum Beispiel in der URI-Zeile des Browsers aus einem de ein en macht, sondern muss dies über einen speziellen URI tun (bei einem CGI -Skript, das nach der Auswahl der verfügbaren Sprachvarianten das Cookie für den Locale-String neu setzt).

10 Perl/Apache-Integration Wie der Titel dieses Kapitel schon vermuten lässt, stehen die Begriffe »Perl« und »Apache« im Mittelpunkt. Und das hat seinen Grund: Der Webserver von Apache ist mit Abstand der weltweit am weitesten verbreitete. Aus meiner persönlichen Erfahrung heraus wäre eine andere Konstellation auch undenkbar, weil dieser Webserver der wohl stabilste und flexibelste ist, den man finden kann. Obwohl manche größeren Firmen versuchen, diese Tatsachen zu vertuschen, indem sie ihren Webserver als besonders intelligente Eigenentwicklung anpreisen und für gutes Geld zu verkaufen suchen, versteckt sich hinter den teuer erkauften Produkten nichts anderes als ein meist »verschlimmbesserter« Apache-Webserver. Wir haben uns im Kapitel CGI bereits mit der Erzeugung dynamischer Web-Inhalte mit Hilfe von CGI -Skripts beschäftigt. Dabei lag unser Augenmerk auf der Programmierung in einem CGI-Umfeld, die internen Abläufe zwischen Webserver und Perl-Interpreter wurden dabei aber weitgehend außer Acht gelassen. Auf gut Deutsch heißt das: Das CGI-Skript muss laufen, egal, wie schlecht die Performance ist. Auch die etwas spärliche Kommunikation zwischen Client und Server nehmen wir dabei »billigend« in Kauf. Natürlich haben wir gelernt, dass der Aufruf eines CGI-Skripts relativ teuer ist, weil der Webserver für jeden CGI-Aufruf dafür einen neuen Prozess in einer eigenen Shell starten muss. Wird Perl als CGI-Schnittstelle verwendet, dann bedeutet dies, dass der Webserver eine Shell startet, in der wiederum ein Perl-Interpreter gestartet wird. Dieser schließlich führt dann das eigentlich angeforderte Perl-Skript aus. Nun wissen wir, warum CGI teuer ist. Ich werde Ihnen diesen Prozess anschließend gleich noch einmal erklären, damit das Wissen wirklich sitzt. Mit dem Apache-Webserver hat man nämlich die Möglichkeit, diese Kette von Prozessen um einiges zu verkürzen. Konsequent durchgeführt machen Sie damit Ihre CGISkripts so schnell, wie Sie es nie vermutet hätten. Sie werden gar nicht merken, dass ein CGI-Skript aufgerufen wurde, so schnell erscheint im Browser die Antwort, selbst dann, wenn auf eine Datenbank zugegriffen wird. So, Sie glauben mir nicht? Nun, wir werden sehen. Zunächst decken wir die Abläufe auf, die sich bei einem normalen CGI Aufruf zwangsläufig einstellen:

674

10

Perl/Apache-Integration

10.1 Standard-CGI Bei der herkömmlichen CGI-Technik definiert man in der Konfiguration des Webservers bestimmte Verzeichnisse, in denen CGI-Skripts abgelegt sind. Die Skripts selbst können beliebige ausführbare Dateien sein, zum Beispiel Binärprogramme, ShellSkripts oder auch Perl-Skripts. Alternativ kann man auch bestimmte Datei-Endungen für Skripts definieren. So ist es zum Beispiel möglich, über eine Direktive in der Konfigurationsdatei dem Webserver zu sagen: Du hör mal, alle URIs, die mit dem String .pl aufhören, sind Perl Skripts. Gib also nicht den Inhalt der Datei an den Client weiter, sondern führe das Skript aus. Der Webserver weiß aufgrund der Konfigurationsdirektive, dass er bei URIs, die auf solche CGI Bereiche zeigen, nicht den Inhalt der Dateien an den Browser schicken darf, sondern die Dateien ausgeführt werden müssen, die dann für die Kommunikation zum Browser selbst verantwortlich sind. Beispiel für eine Apache CGI Direktive: # Auszug aus der Konfigurationsdatei # "httpd.conf" des Apache-Webservers: # mit der Direktive 'ScriptAlias' wird dem Webserver # mitgeteilt, dass alle Dateien # unterhalb des URI '/cgi-bin' (Verzeichnis # '/apacheProject/Apache/cgi-bin') # auszuführende Scripts sind. ScriptAlias /cgi-bin/ "/apacheProject/Apache/cgi-bin/" # Mit der folgenden Direktive können weitere # Zusatzeinstellungen vorgenommen werden

AllowOverride None Options +ExecCGI Order allow,deny Allow from all

Im Gegensatz zu normalen Perl Skripts muss jedes CGI Skript eine »Hashbang Zeile« enthalten. Darunter versteht man den Dateipfad des Perl Interpreters, der in der allerersten Zeile des Skripts mit dem Prefix #! angegeben sein muss, z.B.: #!D:/Perl/bin/perl.exe -w

Bei jedem Clientrequest auf ein CGI Skript startet der Webserver einen neuen Shell Prozess, der zunächst den Perl Interpreter ausführt, welcher wiederum das Perl Skript übersetzt und Zeile für Zeile ausführt. Das bedeutet, dass jeder CGI Aufruf eines

Standard-CGI

675

Browsers zu einem eigenständigen Perl Prozess im System führt, der solange am Leben ist, bis das CGI-Skript sich beendet. Auch der Webserver Prozess bzw. Thread, der den Clientrequest entgegengenommen hat, ist für diese Zeitspanne belegt und kann somit keine weiteren Clients bedienen. Man kann sich leicht vorstellen, welche nachteiligen Auswirkungen bei hoher Last durch viele CGI-Aufrufe auftreten. Zum einen muss das Betriebssystem viele eigenständige Perl Prozesse verwalten, zum anderen dauert die Abarbeitung des Skripts relativ lange, da allein für das Lesen des Perl Interpreters Datenmengen zwischen 500KB und mehreren MB auftreten (so groß ist das Binärprogramm des Interpreters). Zudem muss ein CGI-Skript immer wieder übersetzt werden, auch wenn es bereits viele Male aufgerufen wurde, weil ja mit jedem erneuten Aufruf ein eigener Perl Prozess gestartet wird, der von dem Vorgänger nichts weiß. Diese Vorgänge kann man überprüfen, indem man parallel zu einem CGI-Aufruf über den Browser eine Prozessliste ausgibt (unter UNIX zum Beispiel eine Schleife mit dem »ps«-Kommando; unter Windows verwendet man den Task-Manager). Hier ein Beispielskript, das nichts anderes tut, als eine gewisse Zeit lang Zahlen auszugeben. Damit hat man genügend Zeit, sich parallel die Prozesse im System anzeigen zu lassen. Am besten, sie speichern den Code als CGI-Skript loop.pl im Verzeichnis cgi-bin des Webservers ab: #!D:/Perl/bin/perl.exe -w use strict; # Die folgenden zwei Zeilen sorgen dafür, dass der # Ausgabepuffer nach einer Skriptausgabe sofort geleert # wird. # Hinweis: In manchen Systemen kann es trotzdem # vorkommen, dass die gesamte Ausgabe # erst erfolgt, wenn sich das Script beendet. use IO::Handle; STDOUT->autoflush( 1 ); print( "Content-Type: text/plain\n\n" ); for ( my $i = 1; $i cd Apache C:\Apache>Apache.exe -D SSL -k start Apache/1.3.20 (Win32) mod_perl/1.25_01-dev mod_ssl/2.8.4 OpenSSL/0.9.6a running...

Je nach Version des heruntergeladenen Binaries kann die Ausgabe beim Start des Webservers unterschiedlich sein. Der Schalter -D SSL bewirkt, dass man sowohl HTTP-als auch HTTPS-Verbindungen zum Webserver aufbauen kann. Benötigt man nur HTTP, kann der Schalter entfallen.

mod_perl

679

Der Webserver kann auch als NT-Service gestartet werden. Hierzu muss zunächst der Service eingerichtet werden: C:\>cd Apache C:\Apache>Apache.exe -n Apache -k install

Anschließend kann man den Service entweder über die Dienstverwaltung von Windows starten und beenden, oder man ruft folgende Kommandos in einer DOS-Box auf: C:\>cd Apache REM Dienst starten Apache.exe -n Apache -k start REM Dienst stoppen Apache.exe -n Apache -k stop

Falls sich der Webserver nicht starten lässt, muss man die Konfiguration unter Zuhilfenahme der README-Dateien im Verzeichnis C:\readmes überprüfen. In der Datei C:\readmes\README.txt stehen weitere nützliche Hinweise und Links zu Informationen über mod_perl und anderen relevanten Themen. Zum Abschluss sollte man mit einem Browser prüfen, ob der Webserver ordnungsgemäß arbeitet. In die URI-Zeile des Browsers gibt man zum Beispiel ein: http://localhost

Statt »localhost« kann man auch den voll qualifizierten Rechnernamen angeben, falls dieser vernetzt ist.

Dateistruktur der mod_perl-Installation Nach der Installation des Perl-SSL-Apache-Webservers erhält man im Dateisystem folgende Struktur: C:\Apache asp bin cgi-bin conf embperl htdocs icons include lib libexec logs

# Verzeichnis für Standard-CGI # Konfigurationsverzeichnis # Rootverzeichnis für statische # HTML-Seiten

# Verzeichnis für Log-Dateien (error_log # ist für uns die wichtigste)

680

10

Perl/Apache-Integration

mason mod_perl # Verzeichnis für mod_perl-CGI modules # Verzeichnis für Apache-Module proxy

Nun kann man beliebige CGI-Skripts sowohl unter cgi-bin als auch unter mod_perl einstellen, über den Browser einmal mit dem URI /cgi-bin/script.pl, ein anderes Mal mit dem URI /mod_perl/script.pl aufrufen (der Dateiname script.pl wurde von mir natürlich beispielhaft gewählt) und die Antwortzeiten der CGI-Skripts vergleichen. Wir werden feststellen, dass dasselbe CGI-Skript unter /mod_perl um Größenordnungen schneller läuft als im Standard-CGI-Verzeichnis cgi-bin. Das zweite, was Sie erfreut feststellen werden: Wenn Sie z.B. unser Skript »loop.pl«, das wir weiter oben bereits einmal als Beispiel benutzt hatten, im Verzeichnis mod_perl wiederverwenden und gleichzeitig mit dem Task-Manager die Prozesse verfolgen, sehen Sie, dass kein einziger neuer Prozess gestartet wird.

UNIX-Installation Die Installation unter UNIX ist etwas komplizierter, allerdings wird auch heute noch von UNIX-Anwendern mehr Wissen verlangt als von Windows-Benutzern. Sie müssen neben dem Apache-Webserver (tar-Archiv des Sourcecodes) auch die Sourcen von mod_perl und mod_ssl herunterladen. Informationen darüber erhalten Sie vom Server http://httpd.apache.org. Anschließend packen Sie alle drei tar-Archive in einem gemeinsamen Verzeichnis aus und lesen dann aufmerksam die INSTALL-Dateien von mod_perl und mod_ssl. Wie üblich unter UNIX, mündet dann das Ganze in einen Aufruf des Skripts configure im mod_perl-bzw. mod_ssl-Verzeichnis, gefolgt von einem make und make install, um alles in einem Zielverzeichnis zu installieren. Beim Aufruf des Skripts configure werden Sie vermutlich einige Argumente angeben müssen, die in der INSTALL-Datei beschrieben sind. Der Clou bei dieser Installation ist, dass in den meisten Fällen die Installation von Apache aus dem mod_perl-Verzeichnis heraus gestartet wird. Darüber steht, wie gesagt, genügend Information in der Datei INSTALL.

10.3 Apache-Module in Perl Wie bereits eingangs erwähnt, bietet die Perl/Apache-Integration mit mod_perl weit mehr Möglichkeiten als nur eine Performancesteigerung. Das wohl interessanteste Feature stellen vollständig in Perl implementierte Apache-Module dar, die vollen Zugriff auf interne Webserverdaten haben. Wir werden im weiteren Verlauf ein solches Modul selbst implementieren, mit dem eine Authentifizierung von Webclients möglich ist.

Apache-Module in Perl

681

Bevor wir jedoch Perl-Code schreiben können, müssen wir uns ein wenig Hintergrundinformation aneignen. Zunächst wollen wir die Frage beantworten, wie ein PerlModul zu einem Apache-Modul wird. Wenn ein Client einen beliebigen URI des Webservers anfordert, wird ein Request initiiert, der verschiedene Phasen durchläuft, bevor die Response an den Client gesendet wird. In nahezu jeder Phase der Verarbeitung kann man durch entsprechende Konfigurationsdirektiven in der Datei httpd.conf Module einhängen. Daraufhin ruft der Webserver eine bestimmt Funktion der angegebenen Module auf. Die aufgerufenen Module können beliebigen Einfluss auf die weitere Verarbeitung bis hin zum sofortigen Abbruch oder sofortigen Versand der Response nehmen. Apache-Module müssen ein vorgeschriebenes API implementieren. So sollte jedes Modul eine Funktion für die Verarbeitung des Requests bereitstellen. Der Name der Funktion ist standardmäßig handler(), kann jedoch umkonfiguriert werden. Ebenfalls standardmäßig übergibt der Webserver beim Aufruf der Funktion eine Objektreferenz auf das Request-Objekt. Generell sieht der Perl-Code eines Apache-Moduls etwa so aus: ... # Diese Zeile darf in Apache-Modulen niemals fehlen! use strict; # Wir werden vordefinierte Konstanten verwenden, # deshalb laden wir das entsprechende Modul, # in dem die Konstanten definiert sind. use Apache::Constants; # Evtl. weitere Apache-Perl-Module importieren, # zum Beispiel: use Apache::File; ... sub handler { # Entgegennehmen des Request-Objekts my $r = shift( @_ ); # Verarbeiten des Request ... # Im Erfolgsfall OK (ist in Apache::Constants # definiert) zurückgeben. # Der Request wird eventuell von weiteren Modulen # verarbeitet. return OK;

682

10

Perl/Apache-Integration

# oder: # Der Request wird nicht mehr weiterverarbeitet. return DONE; # Oder ein Fehler: return SERVER_ERROR; # Oder ein Redirect: return REDIRECT; } 1;

Wir interessieren uns hier für ein Apache-Modul, das nur dann aktiviert wird, wenn der angeforderte URI geschützt ist. Nennen wir es Apache::AuthCookie. (Dieses Modul gibt es übrigens wirklich im Internet, allerdings werden wir ein eigenes Modul gleichen Namens implementieren.) Zunächst sehen wir uns an, was in der Apache-Konfiguration definiert sein muss, um das Modul bekannt zu machen. Die Datei httpd.conf enthält bei einer Standard-Installation die Zeile: PerlRequire /Apache/conf/startup.pl

/Apache/conf/startup.pl ist ein Pfadname im Filesystem, der von Ihrer Perl-ApacheInstallation abhängt und gegebenenfalls angepasst werden muss. Die Direktive PerlRequire führt dazu, dass beim Start des Webservers das Perl-Skript C:\Apache\conf\startup.pl ausgeführt wird (Standardinstallation unter Windows). Dieses Skript wird dazu verwendet, anfänglich weitere Module zu laden oder persistente Datenbankverbindungen aufzubauen, und hat nach der Installation folgenden Inhalt: #!/Perl/bin/perl -w # to load this file when the server starts, # add this to httpd.conf: # PerlRequire /path/to/startup.pl # make sure we are in a sane environment. $ENV{GATEWAY_INTERFACE} =~ /^CGI-Perl/ or die "GATEWAY_INTERFACE not Perl!"; use Apache::Registry; use Apache::Status; use Apache::DBI; #use Apache::AuthDBI; use strict; # optional configuration for Apache::DBI.pm:

Apache-Module in Perl

683

# choose debug output: 0 = off, 1 = quiet, 2 = chatty #$Apache::DBI::DEBUG = 2; # configure all connections which should be established during server startup. # keep in mind, that if the connect does not succeeed, your server won't start # until the connect times out (database dependent) ! # you may use a DSN with attribute settings specified within #Apache::DBI->connect_on_init("dbi:driver(AutoCommit=>1):database", "userid", "passwd"); # configure the ping behavior of the persistent database connections # you may NOT not use a DSN with attribute settings specified within # $timeout = 0 -> always ping the database connection (default) # $timeout < 0 -> never ping the database connection # $timeout > 0 -> ping the database connection only if the last access # was more than timeout seconds before #Apache::DBI->setPingTimeOut("dbi:driver:database", $timeout);

# optional configuration for Apache::AuthDBI.pm: # choose debug output: 0 = off, 1 = quiet, 2 = chatty #$Apache::AuthDBI::DEBUG = 2; # set lifetime in seconds for the entries in the cache #Apache::AuthDBI->setCacheTime(0); # set minimum time in seconds between two runs of the handler which cleans the cache #Apache::AuthDBI->setCleanupTime(-1); # use shared memory of given size for the cache #Apache::AuthDBI->initIPC(50000); 1;

Für die Darstellung im Buch sind einige Zeilen umbrochen. Diese erkennt man daran, dass das Kommentarzeichen »#« am Beginn einer Zeile fehlt. Indem wir nun die folgende Zeile use Apache::AuthCookie;

hinzufügen, wird mit jedem Start des Webservers unser Modul automatisch geladen. Nach Perl-Notation muss das Modul in der Datei AuthCookie.pm in einem Verzeichnis Apache gespeichert sein. (Aus dem doppelten Doppelpunkt wird im Filesystem bekanntlich ein Verzeichnistrenner.)

684

10

Perl/Apache-Integration

Grundsätzlich kann man die Datei inklusive des Verzeichnisses an beliebiger Stelle im Verzeichnisbaum ablegen, jedoch muss der Perl -Interpreter in der Lage sein, die Datei zu finden. Man kann entweder die vordefinierte Variable »@INC« im Skript startup.pl so erweitern, dass sie auch dasjenige Verzeichnis enthält, in dem das Unterverzeichnis Apache liegt, oder man lädt das Internet-Modul Apache::AuthCookie herunter, editiert die Sourcedatei und installiert anschließend das Modul im standardmäßigen Perl-Baum für Module. Jetzt wissen Sie, warum ich unserem Modul genau diesen Namen gegeben habe. Es vereinfacht die Installation. Natürlich können Sie auf den Internetseiten von Perl und der Perl/Apache-Integration auch nachlesen, wie man selbst Module installiert. Dieses Verfahren bedarf allerdings einiger tiefer gehender Kenntnisse, deshalb werden wir hier den einfachen Weg gehen und das Verzeichnis C:\Apache\conf\Apache anlegen. Anschließend erstellen wir in diesem Verzeichnis die Datei AuthCookie.pm und erweitern das Skript startup.pl um folgenden Code: BEGIN { unshift( @INC, "/Apache/conf" ); }

Wie gesagt, der Pfad muss von Ihnen entsprechend der Installation eventuell angepasst werden. Der Programmcode sollte möglichst am Anfang der Datei stehen (obwohl dies nicht unbedingt nötig ist). Dadurch findet der Perl-Interpreter das Modul, wenn es anschließend mit der use-Direktive geladen wird, auch dann, wenn es nicht im Perl-Baum integriert ist. Jetzt wird das Modul beim Start des Webservers zwar automatisch geladen, aber noch nicht bei Clientrequests aktiviert. Wir müssen dem Webserver noch mitteilen, für welche URIs die Funktion handler() des Moduls aufgerufen werden soll. Auch hierfür gibt es mehrere Möglichkeiten. Erweitert man die Konfigurationsdatei httpd.conf mit folgenden Zeilen:

AuthName AuthCookie AuthType Apache::AuthCookie PerlAuthenHandler Apache::AuthCookie PerlSetVar AuthCookieDebug 5 require valid-user

und startet den Webserver neu, dann ruft der Webserver bei jedem Request, der mit /private/ beginnt, die Funktion handler() des Moduls Apache::AuthCookie auf.

Apache-Module in Perl

685

Mit der Direktive AuthName legt man einen eindeutigen Namen für die Authentifizierung fest, mit AuthType den Authentifizierungs-Typ. Bei beiden Direktiven können Sie im Prinzip eigene Namen verwenden. Mit der Direktive PerlAuthenHandler wird das Package angegeben, das in der Authentifizierungsphase eines Requests verwendet werden soll. Der Webserver ruft standardmäßig die Funktion handler() dieses Packages auf. Die Direktive PerlSetVar dient dazu, dem Package zur Laufzeit Variablen zur Verfügung zu stellen. Damit kann man das Modul über die Konfigurationsdatei anpassen, ohne den Programmcode ändern zu müssen. Die Direktive kann beliebig oft vorhanden sein. Zu guter Letzt noch die Direktive require. Sie gibt an, worauf sich die Authentifizierung bezieht. Einzelheiten sind in der Dokumentation von Apache und mod_perl nachzulesen. Wir werden grundsätzlich für diese Direktive nur den Wert valid-user verwenden. Ich habe den URI /private gewählt, um damit auszudrücken, dass sich unterhalb dieses URIs ein geschützter Bereich befindet. Natürlich können Sie im Prinzip jeden beliebigen URI verwenden. Da das Verzeichnis private noch nicht unter C:\Apache\htdocs existiert, müssen wir es anlegen und darin HTML-Seiten einstellen. Der Schutz ist rekursiv und beschränkt sich nicht nur auf URIs in diesem Verzeichnis, sondern wird ebenso von allen darunter liegenden URIs geerbt (/private/myDir/ dir1/index.html wäre also zum Beispiel ebenfalls geschützt). Nachteil dieser Methode ist, dass man nach einer Änderung der Konfiguration den Webserver neu starten muss, sonst wirkt sich die Änderung nicht aus. Deshalb kann man in diesem Fall einen anderen Weg gehen: Wir erstellen im Verzeichnis, das geschützt werden soll, eine Datei namens .htaccess und schreiben die Konfigurationsdirektiven in diese Datei. Der Dateiname .htaccess beginnt mit einem Punkt. AuthName AuthCookie AuthType Apache::AuthCookie PerlAuthenHandler Apache::AuthCookie PerlSetVar AuthCookieDebug 5 require valid-user

686

10

Perl/Apache-Integration

Diese Methode hat dieselbe Wirkung, jedoch kann man den Inhalt der Datei ändern, ohne den Webserver neu starten zu müssen. Damit die Datei .htaccess vom Webserver verwendet wird, muss in der Konfigurationsdatei httpd.conf der folgende Eintrag enthalten sein:

AllowOverride AuthConfig

Nun wollen wir noch einmal die einzelnen Direktiven in einer Zusammenstellung erläutern: 왘 AuthName Mit dieser Direktive gibt man einen frei wählbaren Namen für die Authentifizierung an. 왘 AuthType Diese Direktive definiert, um welche Art von Authentifizierung es sich handelt (Voreinstellung ist basic). 왘 PerlAuthenHandler Hier muss man das Perl-Modul angeben, das verwendet werden soll. Implizit ruft der Webserver die Funktion handler() des angegebenen Moduls auf. Man kann jedoch auch einen anderen Funktionsnamen verwenden. In diesem Fall muss der gewählte Name mit einem doppelten Doppelpunkt getrennt nach dem Modulnamen stehen, zum Beispiel PerlAuthenHandler Apache::AuthCookie::auth

In diesem Fall wird die Funktion auth() des Moduls aufgerufen. 왘 PerlSetVar Diese Direktive kann mehrfach angegeben werden und dient dazu, das Modul dynamisch zu konfigurieren. In unserem Beispiel wird der Wert für den DebugLevel damit eingestellt. Wir werden weiter unten noch sehen, wie man im Programm-Code des Moduls auf die dynamischen Variablen zugreifen kann. 왘 require Mit dieser Direktive kann man entweder explizit angeben, welche User Zugriff besitzen, oder man stellt allgemein ein, dass nur authorisierte User (valid-user) Zugriff haben.

Apache-Module in Perl

687

Wer mehr über die Konfiguration von Apache wissen möchte, sei auf die mitinstallierte Dokumentation im Verzeichnis C:\Apache\htdocs\manual verwiesen. Auch für mod_perl gibt es Detailinformationen: C:\Perl\bin\perldoc mod_perl

gibt genauere Auskunft (man kann auch die READMEs unter C:\readmes lesen). Wir wollen unser Augenmerk nun auf den eigentlichen Perl-Code werfen, den wir im Apache-Modul implementieren müssen, um eine User-Authentifizierung durchzuführen. Hier unser AuthCookie-Programmskelett: package Apache::AuthCookie; use strict; # Modul mod_perl importieren # (ohne Version >= 1.25 läuft nichts) use mod_perl qw( 1.25 StackedHandlers MethodHandlers Authen Authz ); # Die wichtigsten Konstanten (z.B. 'OK') aus # Apache::Constants importieren use Apache::Constants qw( :response ); # CGI-Standard-Funktionen importieren use CGI ( ':standard' ); use CGI::Cookie; # Debug-Level einstellen our $debugLevel = 0; # Für Debug-Ausgaben use IO::Handle; STDERR->autoflush( 1 ); sub handler { # Request-Objekt holen my $r = shift( @_ ); # Präfix für unsere Funktion als # Identifikation im Logfile my $prefix = "Apache::AuthCookie():"; # Für Debug-Zwecke eine Ausgabe, # die in error_log vom Webserver landet dbg( "$prefix hallo" );

688

10 # OK zurückgeben; damit wird der Request # weiterbearbeitet return OK;

} # Funktion für Debug- und Log-Meldungen, # die in der Datei 'error_log' des Webservers landen sub dbg { my $level = $debugLevel; # Wenn das erste Argument aus einer Zahl besteht, # nehmen wir an, dass es sich um # den aktuellen Debug-Level handelt. # Die Meldung wird in diesem Fall nur dann # ausgegeben, wenn der aktuelle Debug-Level # nicht grösser ist als der voreingestellte Wert # (siehe auch $debugLevel). # Ansonsten wird die Meldung in jedem Fall # ausgegeben. if ( @_ and defined( $_[ 0 ] ) and ( $_[ 0 ] =~ /^\d+$/ ) ) { $level = shift( @_ ); } # Falls die Ausgabe so detailliert ist, dass sie # über den voreingestellten Wert # des Debug-Levels hinausgeht, geben wir nichts aus. if ( $level > $debugLevel ) { return; } # Jede Ausgabe enthält das aktuelle Datum in der # Form '[JJJJ-MO-TT ST:MI:SE]'. my @date = localtime(); $date[ 5 ] += 1900; $date[ 4 ]++; my $dateStr = sprintf( "[%d-%02d-%02d %02d:%02d:%02d] ", reverse( @date[ 0 .. 5 ] ) ); print( STDERR $dateStr ); foreach my $arg ( @_ ) { STDERR->print( defined( $arg ) ? $arg : "undef" ); } STDERR->print( "\n" ); } 1;

Perl/Apache-Integration

Apache-Module in Perl

689

Wir können überprüfen, ob das Modul aufgerufen wird, indem wir zunächst den Webserver neu starten (damit die Änderungen in der Moduldatei vom Webserver übernommen werden), und im Browser einen URI aufrufen, der durch die oben gezeigte Datei .htaccess geschützt ist. Nach jedem Zugriff muss in die Logdatei C:\Apache\logs\error_log eine Zeile mit dem aktuellen Datum und dem String »hallo« geschrieben werden. Das wirklich Interessante im obigen Code ist $r (das Request-Objekt). Über dieses Objekt können sowohl Client- als auch interne Serverdaten gelesen und manipuliert und sogar auch die Response erstellt werden. Dazu gehören die eingangsseitigen HTTP-Header vom Client und die HTTP-Header, die vom Server an den Client gesendet werden. Sehen wir uns nun die wichtigsten Methoden des Request-Objekts für die Verarbeitung der Clientdaten an (eine vollständige Liste erhält man mit dem Kommando perldoc Apache): 왘 is_initial_req() Diese Methode gibt einen TRUE-Wert zurück, wenn der Request vom Client ein Hauptrequest ist. Handelt es sich um einen Teilrequest, liefert die Methode FALSE. (Apache kennt die Notation von Haupt- und Teilrequests. Ein einzelner Request vom Client kann in mehrere Teile aufgetrennt werden, nur der Hauptrequest kommt direkt vom Client.) Wir interessieren uns in unserem Modul nur für den Hauptrequest, deshalb beenden wir unsere handler()-Funktion, wenn es sich um einen Teilrequest handelt: return OK unless ( $r->is_initital_req() );

왘 method([ method ]) Diese Methode liefert die HTTP-Methode zurück, zum Beispiel GET oder POST. Man kann aber auch explizit durch ein Argument die HTTP Methode aus der handler()Funktion heraus ändern! 왘 header_only() Diese Methode liefert einen TRUE-Wert zurück, wenn der Client einen HEADRequest gesendet hat (viele Browser benutzen die HTTP-Methode HEAD, um vor dem Abrufen des eigentlichen URI zu prüfen, ob er den Inhalt vom Server lesen muss oder den Inhalt aus seinem Cache verwenden kann). 왘 protocol() Mit dieser Methode kann man das vom Client verwendete HTTP-Protokoll abfragen (zum Beispiel »HTTP/1.0« oder »HTTP/1.1«). 왘 uri([ uri ]) Diese Methode liefert den URI des Requests (ohne evtl. Querystring). Durch Angabe eines Arguments kann man den URI in der handler()-Funktion auch verändern.

690

10

Perl/Apache-Integration

왘 args([ query_string ]) Ohne Argument liefert die Methode im skalaren Kontext den gesamten URI-codierten Querystring zurück, im List-Kontext ein Hash, das die Querystring-Parameter als Hash-Elemente enthält. Durch Angeben eines Arguments kann man explizit Querystring-Parameter setzen. Dies ist zum Beispiel bei einem Redirect eines POSTRequests nützlich. Beispiel: Der Client URI /private/a.html?a=b&c=hello%20world

liefert im skalaren Kontext: my $args = $r->args(); # $args enthält "a=b&c=hello%20world"

Aufruf im List-Kontext: my %args = $r->args(); # %args enthält die Keys 'a' und 'c' # $args{ "a" } enthält "b" # $args{ "c" } enthält "hello world"

Vorsicht

bei

mehrfachem

Vorkommen

desselben

Query-Parameters,

z.B.

?a=1&a=2&a=3

Im List-Kontext wird nur der letzte Wert (3) in das Hash-Element übernommen. Hier sollte man besser das CGI-Modul verwenden. my $args = $r->args(); my $query = new CGI( $args );

왘 headers_in() Diese Methode liefert alle vom Client gesendeten HTTP-Header als Hash-Elemente zurück (der Name des jeweiligen Headers entspricht dem Hash-Key). Beispiel: my %headers = $r->headers_in(); foreach my $headerName ( sort( keys( %headers ) ) ) { dbg( "header '$headerName' = '", $headers{ $headerName }, "'" ); }

Ausgabe im Log-File: [2002-03-29 10:02:37] header 'Accept-Language' = 'de'

Apache-Module in Perl [2002-03-29 10:02:37] header 'User-Agent = Mozilla/4.78 [de]C-CCK-MCD DT

691 (Windows

NT 5.0; U)'

왘 header_in( name[, value ]) Mit dieser Methode kann man den Wert eines bestimmten HTTP-Headers abfragen (oder auch setzen!). Äußerst interessant wird die Funktion, wenn man nach einer erfolgreichen Authentifizierung des Webclients durch das Apache-Modul ein Cookie erzeugt und in die Client-Header einfügt! CGI-Skripts (oder auch Java-Servlets bzw. JSP-Seiten, falls jemand eine JSP-Engine mit dem Webserver verheiratet hat), die durch den URI eigentlich das Ziel des Requests sind, merken dabei gar nicht, dass ihnen das Cookie vom Modul sozusagen »untergeschoben« wurde. Damit lässt sich die Clientauthentifizierung an einer zentralen Stelle implementieren. Beispiel für header_in(): my $language = $r->header_in( "Accept-Language" ); my $cookies = $r->header_in( "Cookie" );

왘 connection() Diese Methode liefert eine Objektreferenz zurück, mit der weitere Informationen über die Clientverbindung verwaltet werden können. Beispiele: my $c = $r->connection(); # IP-Adresse des Clients lesen (Achtung: Viele Clients # verwenden Proxyserver der # Service-Provider, in diesem Fall erhält man die # IP-Adresse des Proxyservers). my $clientIP = $c->remote_ip(); # Bei Basic Authentication kann man hier den # Loginnamen des Users lesen... my $user = $c->remote_user(); # ... oder auch gezielt setzen: $c->remote_user( "anonymous" ); # Dies hat zur Auswirkung, dass in der Logdatei # 'access_log' des Webservers # der Loginname mit protokolliert wird.

692

10

Perl/Apache-Integration

Im Folgenden sind die wichtigsten Methoden beschrieben, mit denen man ServerInformationen verwalten kann: 왘 dir_config( key ) Dies ist wohl die wichtigste Methode, denn damit kann man das Apache-Modul über die Datei .htaccess oder über Location-bzw. Directory-Direktiven in der Konfigurationsdatei httpd.conf über die Direktive PerlSetVar anpassen. key ist der Name einer Variable, die mit PerlSetVar definiert werden kann. Sie ist

case-insensitive. Beispiel: # Datei .htaccess bzw. httpd.conf-Direktive. # Es wird die Variable 'AuthCookieDebugLevel' # auf den Wert '5' gesetzt. PerlSetVar AuthCookieDebugLevel 5 # Apache-Modul my $debugLevel = $r->dir_config( "AuthCookieDebugLevel" ); # Wenn die Variable nicht vorhanden ist, wird der # Pseudowert "undef" zurückgegeben. # Deshalb muss dieser Wert abgefangen werden. unless ( defined( $debugLevel ) ) { $debugLevel = $defaultDebugLevel; }

왘 dir_config->get( key ) Diese Methode wird ähnlich wie dir_config() verwendet, liefert jedoch ein Array zurück. Damit lassen sich Multi-Value-Variablen über die Direktive PerlAddVar verwalten. Beispiel: # Datei .htaccess bzw. httpd.conf-Direktive PerlAddVar AuthCookieHosts host1 PerlAddVar AuthCookieHosts host2 # Apache-Modul my @hosts = $r->dir_config->get( "AuthCookieHosts" ); # @hosts enthält ( "host1", "host2" )

Neben den gezeigten Methoden gibt es eine Reihe weiterer, mit denen man unter anderem den Namen und Port des Servers sowie andere interne Konfigurationseinstellungen auslesen kann; siehe hierzu die Apache-Dokumentation (Kommando perldoc Apache).

Apache-Module in Perl

693

Wir kommen jetzt zu den Methoden, mit denen man Responsedaten verwalten kann. Auch hier werden nur die wichtigsten erläutert: 왘 status( status ) Das Argument status muss ein Integerwert des Webserverstatus für diesen Request sein. Man kann symbolische Konstanten aus dem Modul Apache::Constants verwenden. Der Statuswert sollte mit dem Rückgabestatus der handler()-Funktion übereinstimmen. Beispiel: # Es werden alle Konstanten der Gruppe # 'response' übernommen. # Dazu gehören allgemeine sowie responsespezifische # Konstanten. use Apache::Constants( ':response' ); ... sub handler { my $r = shift( @_ ); ... if ( # error ) { $r->status( SERVER_ERROR ); return SERVER_ERROR; } ... # Erfolg return OK; }

왘 content_type([ type ]) Beim Erstellen der Response wird mit dem Argument »type« der Inhalt des HTTPHeaders Content-Type angegeben. 왘 header_out( name, value ) Mit dieser Methode kann man einen beliebigen HTTP-Header neu anlegen oder einen bereits vorhandenen Header ändern. Hinweis: Im Argument name wird nur der Name des Headers, nicht aber der Doppelpunkt angegeben. Beispiel: $r->header_out( "Content-Type", "text/plain" );

694

10

Perl/Apache-Integration

왘 headers_out->add( name, value ) Wenn man einen HTTP-Header mit mehreren Values zum Client übertragen möchte, kann man nicht die Methode header_out() verwenden, da jeder erneute Aufruf der Funktion mit einem bereits existierenden Headernamen diesen überschreibt (bei Cookies hat man dieses Problem dann, wenn man mehrere Cookies übertragen möchte). In diesem Fall kann man die Methode add() des headers_outObjekts verwenden. 왘 err_header_out( name, value ) Diese Methode arbeitet wie header_out(), jedoch werden die erzeugten HTTPHeader auch bei internen Redirects weitergereicht. 왘 err_headers_out->add( name, value ) Es gilt dasselbe wie für headers_out->add(), jedoch werden die erzeugten HTTPHeader auch bei internen Redirects weitergereicht. 왘 send_http_header([ contentType ]) Mit dieser Methode kann man im Apache-Modul selbst die HTTP-Header an den Client senden (normalerweise übernimmt dies der Webserver). Doch Vorsicht: $r->send_http_header(); return OK;

Dieser Code führt dazu, dass die HTTP-Header zweimal ausgegeben werden, einmal durch das Apache-Modul, ein zweites Mal vom Webserver, weil der Handler sich mit dem Returnstatus OK beendet, worauf der Request alle weiteren Phasen durchläuft. Mit dem Returnstatus DONE kann man dies verhindern: return DONE;

Nun wird die Weiterverarbeitung des Requests abgebrochen; für den Server ist der Request vollständig abgearbeitet. Natürlich muss man zwischen dem Versand der HTTP-Header und dem return-Statement Daten ausgeben, sonst erhält man die Meldung Document contains no data im Browser. Siehe hierzu auch die Methode print(). 왘 print( list ) Mit dieser Methode kann man nach dem Versand der HTTP-Header (send_http_header()) selbst Daten an den Client schicken. list muss eine Liste von auszugebenden Daten sein. Normalerweise enthält die Liste nur Skalare (zum Beispiel Strings), es können jedoch auch Referenzvariablen übergeben werden. Diese werden im Gegensatz zur Perl-Funktion print() vor der Benutzung dereferenziert.

Authentifizierungs-Modul

695

Beispiel: # Referenzvariable auf den String "hallo" my $var = \"hallo"; # Ausgabe der Daten an den Client $r->print( $var ); # Es wird der String "hallo" ausgegeben, # nicht "SCALAR(0xABAA00770)"

왘 internal_redirect( uri ) Mit dieser Methode kann man den Request intern auf einen anderen URI des Webservers umleiten, ohne dass der neue URI im Browser sichtbar wird. Beispiel: # Interne Umleitung auf /public/hello.html $r->internal_redirect( "/public/hello.html" ); # Nach dem internen Redirect muss DONE zurückgegeben # werden, sonst # gibt der Server die HTTP-Header nach der Ausgabe der # HTML-Seite noch einmal aus. return DONE;

Wenn im Redirect-URI relative Hypertextlinks verwendet sind, können diese nicht richtig aufgelöst werden, wenn die zum URI gehörende Datei in einem anderen Verzeichnis steht als die Datei des aktuellen Requests. 왘 internal_redirect_handler( uri ) Diese Methode arbeitet genauso wie internal_redirect(), jedoch wird der Handler des Request-Objekts beibehalten.

10.4 Authentifizierungs-Modul Nachdem wir nun die Grundlagen von Apache-Modulen näher kennen gelernt haben, können wir uns daran machen, eine Authentifizierung von Webclients zu implementieren. Hierfür verwenden wir das weiter oben erarbeitete Programmskelett. Die Authentifizierung erfolgt über ein Cookie. Wir benötigen eine Funktion, mit der ein Login über den Browser durchgeführt werden kann. Hierfür kommen mehrere Möglichkeiten in Betracht.

696

10

Perl/Apache-Integration

So könnte man zum Beispiel einen Redirect auf ein Login-CGI-Skript programmieren, wenn festgestellt wird, dass der Nutzer nicht angemeldet ist. Allerdings speichert der Browser den URI des Login-Skripts in seiner History, und der Nutzer kann über die Browsernavigation jederzeit zurück zum Anmeldeformular gelangen. Auch ein interner Redirect wäre denkbar. Dies hat den Vorteil, dass der URI nicht verändert wird und der Browser keinen neuen History-Eintrag für das Loginformular speichert. Wir wollen aber den gesamten Mechanismus im Apache-Modul selbst in einer eigenen Methode login() implementieren. Hierzu verwenden wir Netscape-Cookies. Diese Art der Implementierung ist die schwierigste Lösung, bei der wir am meisten lernen können. Deshalb müssen wir uns anhand eines vereinfachten Pseudocodes erst einmal Überblick verschaffen: Client sendet einen Request auf einen geschützten URI (/private/index.html) Der Webserver ruft die Methode handler() des ApacheModuls Apache::AuthCookie auf AuthCookie prüft, ob der Client ein gültiges LoginCookie gesendet hat if ( Cookie gesendet ) { Request normal bearbeiten, d.h. return OK } else { Prüfen, ob Formulardaten des Loginformulars gesendet wurden if ( keine Formulardaten ) { Loginformular ausgeben } else { Formulardaten prüfen if ( Prüfung OK ) { Cookie-Header erzeugen Ursprünglichen Request des Clients beantworten } else { Loginformular ausgeben } } }

Wir wollen, unabhängig vom angeforderten URI, den Versand des Loginformulars und dessen Verarbeitung im Apache-Modul selbst durchführen, ohne einen Redirect auf ein CGI-Skript zu verwenden. Ziel ist es, dass die URI-Zeile des Browserfensters durch die Loginaktion nicht geändert wird.

Authentifizierungs-Modul

697

Zum besseren Verständnis hier die Standard-Vorgehensweise mit einem Redirect: if ( Nutzer nicht angemeldet ) { Sende Redirect auf ein CGI-Skript, das sowohl die Ausgabe des Loginformulars als auch die Verarbeitung der Formulardaten erledigt. Im Formular ist ein verstecktes Feld vorhanden, das den URI des ursprünglichen Requests enthält. Nach erfolgreicher Prüfung des Logins sendet dieses Skript das Login-Cookie und führt einen Redirect auf den ursprünglichen URI durch }

Der Anwender sieht also zunächst zum Beispiel den (geschützten) URI /private/index.html im Browserfenster. Nach dem Redirect auf das CGI-Skript sieht er dessen URI in der URI-Zeile des Browsers, anschließend wieder den ursprünglichen URI. Der Browser nimmt den URI des CGI-Skripts mit in seine History-Verwaltung auf, und der Anwender kann diesen URI auch aus der History Liste auswählen. Dieses Verhalten wollen wir verhindern. Wenn der Anwender also zum Beispiel den geschützten URI /private/index.html angefordert hat, dann soll nach der gesamten Aktion dieser URI in der URI-Zeile des Browsers unverändert erscheinen. Die Logik für die Authentifizierung sieht recht einfach aus, hinter den Aktionen verbergen sich jedoch tief gehende Problematiken, die wir lösen müssen: So haben wir als erstes Problem zum Beispiel das Loginformular und dessen Verarbeitung: Grundsätzlich gibt es zwei verschiedene HTML-Formulare, die sich aufgrund des Attributs TYPE des -Tags ergeben: Bei POST-Formularen werden die Formulardaten für den Nutzer unsichtbar an den Server übertragen, während der Browser nach dem Abschicken von GET-Formularen alle Formularfelder (und damit zum Beispiel auch Kennwörter) im Klartext in der URI-Zeile anzeigt. Wenn wir die Anzeige der Formularfelder durch den Browser unterdrücken wollen, müssen wir also ein POST-Formular verwenden. Das hat aber wiederum zur Folge, dass man im Formular-Attribut ACTION keine HTML-Seite als URI angeben darf, dies erlaubt der Webserver nicht. Im Attribut ACTION muss irgendein dynamischer URI angegeben werden. Nun, wir wollen dieser Anforderung gerecht werden, indem wir irgendeinen nicht existenten URI unterhalb des geschützten Bereichs für das Formular-Attribut »ACTION« festlegen (zum Beispiel /private/dummyLogin.pl). Der Name kann wirklich frei gewählt werden, er muss nur eine Datei-Endung besitzen, die für den Webserver kein statisches Dokument darstellt.

698

10

Perl/Apache-Integration

In unserem Apache-Modul, das nach dem Absenden des Formulars (das wir selbst zuvor an den Client geschickt haben) ja wiederum aktiviert wird, werden wir den ursprünglich angeforderten URI wiederherstellen, und alle sind zufrieden. Hier die Implementierung des Apache-Moduls: sub handler { my $r = shift; my $prefix = "AuthCookie::handler:"; # Für schnelle Tests empfiehlt sich ein einfaches # Hash für die Nutzerverwaltung (hier wird # eine Hash-Referenz verwendet). # Später werden wir dies durch echte # Datenbankabfragen ersetzen. my $users = { "dummy" => "pwd", "johnny" => "sixpack", }; # Den Debug-Level aus der Konfiguration (.htaccess # bzw. httpd.conf) lesen # (Default ist 0, d.h. kein Debugging) $debug = $r->dir_config( "AuthCookieDebug" ); $debug = 0 unless ( defined( $debug ) ); # Ein Clientrequest kann im Webserver in mehrere # Teilrequests zerlegt werden. # Wir müssen nur den ersten Hauptrequest # bearbeiten. return OK unless $r->is_initial_req(); # Der folgende Code verhindert, dass unsere Seiten # vom Browser oder einem dazwischen # liegenden Proxyserver aus dem Cache gelesen werden. # Ausschalten des Browser-Caches $r->err_headers_out->add( "Pragma" => "no-cache" ); # Ausschalten des Proxy-Caches $r->err_headers_out->add( "Cache-Control" => "private" ); # # # # # # #

Das nächste Statement teilt dem Browser mit, dass sich der Inhalt des angeforderten URIs seit dem letzten Zugriff geändert hat, wenn dieser mit der HTTP-Methode HEAD eine Abfrage startet. Nach dieser Methode suchen viele Webprogrammierer verzweifelt, weil nur sie wirklich gewährleistet,

Authentifizierungs-Modul # dass dynamische Inhalte CGI-Skripts nicht aus # dem Browser-Cache verwendet werden. $r->update_mtime( time() ); my $uri = $r->uri(); dbg( 2, "$prefix uri = '$uri'" ); # Client-Cookies lesen my $cookies = $r->header_in( "Cookie" ); # Den Namen unseres Login-Cookies aus der # Konfiguration dynamisch übernehmen my $cookieName = $r->dir_config( "AuthCookieCookieName" ) || "uid"; # Prüfen, ob der Client über das Login-Cookie # bereits angemeldet ist. if ( $cookies and ( $cookies =~ /$cookieName\s*=\s*([^; ]+)/ ) ) { my $cookieVal = $1; # Hier kommt später noch eine Prüfung des # Login-Cookies dbg( 1, "$prefix authenticated" ); # Im Connection-Objekt den Usernamen belegen, # damit wird er in der Datei access_log # des Webservers mitprotokolliert. $r->connection()->user( $cookieVal ); return OK; } # Willkürlich festgelegter, nicht existenter URI, # den wir für das POST-Formular benötigen. my $fakeUri = "/private/ballaballa.pl"; # Prüfen, ob der Anwender das Loginformular # abgeschickt hat. if ( ( $r->method_number() eq M_POST ) and ( $uri eq $fakeUri ) ) { dbg( 2, "$prefix got login form" ); # Formular und dessen Formularfelder auslesen my %args = $r->content(); my $login = $args{ "login" }; my $pwd = $args{ "pwd" }; my $reqUri = $args{ "reqUri" };

699

700

10 # Bei erfolgreicher Prüfung (die wir später # etwas genauer durchführen werden) # senden wir in der Antwort das # Login-Cookie mit. if ( $login and exists( $users->{ $login } ) and $pwd and ( $pwd eq $users->{ $login } ) ) { $r->err_header_out( "Set-Cookie", CGI::cookie( "-name" => "uid", "-value" => "hemu", "-path" => "/" ) ); } # Ursprünglichen URI per Redirect wieder # anfordern lassen $r->uri( $reqUri ); # Methode auf GET stellen $r->method_number( M_GET ); $r->method( "GET" ); # Redirect-Header erzeugen $r->header_out( "Location", $reqUri ); # Status auf REDIRECT setzen und beenden $r->status( REDIRECT ); return REDIRECT; } # Hier steht fest, dass der User weder durch Login# Cookie noch durch Loginformular # authentifiziert ist. Wir müssen das Loginformular # ausgeben. Normalerweise würde ich hier # das Package "Template" verwenden, das wir # bereits her kennen. # Aber für die vereinfachte Darstellung # der Abläufe reicht ein simples # Formular, das wir direkt ausgeben, # auch aus. dbg( 2, "$prefix sending login form" );

my $html = print( $html ); # Dem Webserver mitteilen, dass der Request # vollständig bearbeitet ist. Damit verhindern # wir, dass der Webserver selbst auch noch einmal # den HTTP-Header schickt. return DONE; } 1;

Nun wollen wir das Login-Cookie ein bisschen sicherer machen. Bisher haben wir ja nur den Loginnamen als Bestandteil des Cookies, der beliebig von Hackern vorgetäuscht werden kann. Zu diesem Zweck müssen wir den Wert des Cookies so verschlüsseln, dass eine Täuschung durch Hacker unmöglich wird. Für die Verschlüsselung selbst verwenden wir das Perl-Modul Digest::MD5 (das Modul ist unter Windows Bestandteil der Perl-Distribution von ActivePERL, kann aber auch aus dem Internet heruntergeladen werden). Das Prozedere der Verschlüsselung ist wie folgt: # Modul für Verschlüsselung importieren use Digest::MD5; # Geheimen Schlüssel definieren my $secretKey = "Das ist mein ganz geheimer " . "Schlüssel, den nur ich kennen darf"; # Wenn der Anwender sich durch das Loginformular # erfolgreich angemeldet hat: # Geheimen String mit Loginnamen verschlüsseln my $ctx = new Digest::MD5; $ctx->add( $secretKey . $login ); # Verschlüsselten Wert in Hex-Notation umwandeln my $digest = $ctx->hexdigest(); # Cookie mit verschlüsseltem Key und Loginnamen # aufbauen und diesen an Client schicken my $cookieVal = "$digest.$login";

702

10

Perl/Apache-Integration

... # Wenn der Client das Login-Cookie sendet: # Cookie lesen und Schlüssel sowie Loginnamen # extrahieren, # den secretKey nochmals verschlüsseln, # den verschlüsselten String aus dem Cookie mit dem # selbst erzeugten String vergleichen. # Sind beide gleich, dann wurde ein gültiges Cookie # gesendet. my $cookies = $r->header_in( "Cookie" ); if ( $cookies and ( $cookies =~ /$cookieName\s*=\s*([^; ]+)/ ) ) { my $cookieVal = $1; my ( $key, $login ) = $cookieVal =~ /^([^\.]+)\.(.+)$/; unless ( $key and $login ) { return SERVER_ERROR; } my $ctx = new Digest::MD5; $ctx->add( $secretKey . $login ); my $digest = $ctx->hexdigest(); if ( $key ne $digest ) { return SERVER_ERROR; } # Loginnamen im Request setzen, damit wird er # in der Datei access_log mitprotokolliert. $r->connection->user( $login ); dbg( "$prefix authenticated" ); return OK; }

Das gezeigte Verfahren verhindert, dass jemand einfach ein »selbstgemachtes« Cookie an den Server sendet und sich somit fälschlicherweise als jemand anderer ausgibt. Dazu müsste er den geheimen Schlüssel kennen, weil der Loginname nicht nur im Klartext, sondern auch mit dem Schlüssel codiert im Cookie steht. Wenn jemand allerdings die zwischen Server und Client über das Netzwerk ausgetauschten Daten »belauscht«, hilft diese Methode nicht weiter. Um solche Betrügereien zu verhindern, muss man sich andere Verfahren ausdenken. So könnte man zum Beispiel die IP-Adresse des Clients mitverschlüsseln. Dann hat man aber an anderer Stelle

Authentifizierungs-Modul

703

ein Problem, nämlich bei Anwendern, die hinter einer Firewall liegen und deren Requests über einen Proxyserver laufen. Dessen IP-Adresse kann sich mit jedem Request ändern, obwohl der Client gleich bleibt. Zusätzlich kann man im Cookie einen Zeitstempel für das Verfalldatum speichern (unser bisheriges Cookie ist bis zum Beenden des Browsers gültig). Aber auch diese Methode ist nicht völlig sicher. Am besten ist es, die gesamte Kommunikation mit SSL durch das HTTPS-Protokoll sicher zu machen. Ein Vorteil der Authentifizierung durch Cookies ist die Möglichkeit des Abmeldens, ohne dass der Browser hierfür beendet werden muss. Man muss nur das Cookie löschen. Sehen wir uns hierzu das CGI-Skript /mod_perl/private/logoff.pl an: #!/Perl/bin/perl.exe -w use strict; use CGI qw( :standard ); use CGI::Cookie; my $cookieName = "uid"; my $cookie = CGI::cookie( $cookieName ); if ( $cookie ) { print( "Set-Cookie: " . CGI::cookie( "-name" => $cookieName, "-value" => "", "-expires" => "-2h" ) . "\n" ); } print( uri(); if ( $uri =~ /logoff$/ ) { $r->header_out( "Set-Cookie", CGI::cookie( "-name" => $cookieName, "-value" => "", "-expires" => "-2h" ) ); } ... }

10.5 Web-Authentifizierung mit DBI Wir haben in unserem bisherigen Code des Apache-Moduls nur eine beispielhafte Authentifizierung der Clients implementiert. Dies wollen wir nun ändern und eine »echte« Authentifizierung mittels DBI durchführen. Zunächst müssen wir eine Datenbanktabelle für unsere User definieren. Unser Augenmerk liegt hier nicht auf den User-Datensätzen, sondern auf der Technik, wie man darauf zugreift. Deshalb verwenden wir eine möglichst einfache Tabelle: CREATE TABLE http_usr ( husr_id BIGINT( 15 ) NOT NULL AUTO_INCREMENT PRIMARY KEY, husr_name VARCHAR( 255 ) BINARY NOT NULL, husr_pwd VARCHAR( 255 ) BINARY NOT NULL, husr_status CHAR( 1 ) NOT NULL, husr_email VARCHAR( 255 ) NOT NULL, UNIQUE( husr_name ) );

Web-Authentifizierung mit DBI

705

Nun müssen wir im Apache-Modul eine Datenbankverbindung mit jedem Request öffnen: ... use DBI (); ... sub handler { ... # Parameter für die Datenbank-Verbindung aus # Konfiguration lesen my $dbUri = $r->dir_config( "AuthCookieDbUri" ); my $dbUser = $r->dir_config( "AuthCookieDbUser" ); my $dbPwd = $r->dir_config( "AuthCookieDbPwd" ); # Parameter prüfen unless ( defined( $dbUri ) and defined( $dbUser ) and defined( $dbPwd ) ) { return SERVER_ERROR; } # Hash-Referenz für Zusatzeinstellungen des # Datenbanktreibers my $attrs = { "RaiseError" => 0, "PrintError" => 0, }; # Datenbankverbindung aufbauen my $dbh = DBI->connect( $dbUri, $dbUser, $dbPwd, $attrs ); unless ( $dbh ) { return SERVER_ERROR; } ... # Nicht vergessen: Das Datenbank-Handle muss in # jedem Fall geschlossen werden mit # $dbh->disconnect(); }

706

10

Perl/Apache-Integration

Wenn im Apache-Modul vergessen wird, das Datenbank-Handle wieder freizugeben, können Sie nach kurzer Zeit den Webserver neu starten, weil keine freien Handles mehr verfügbar sind. Im Apache-Modul müssen wir nun sowohl bei der Prüfung des Loginformulars als auch bei authentifizierten Requests durch ein Cookie vom Client den Datensatz für den angegebenen User aus der Datenbank lesen und prüfen: # Datensatz lesen my $sql = "SELECT * FROM http_usr WHERE husr_name = " . $dbh->quote( $login ); my $sth = $dbh->prepare( $sql ); unless ( $sth and $sth->execute() ) { $dbh->disconnect(); return SERVER_ERROR; } my ( $id, $name, $dbPwd $status, $email ) = $sth->fetchrow_array(); $dbh->disconnect(); # Datensatz prüfen ("e" in $status bedeutet # freigeschaltet, ein "d" bedeutet gesperrt) # Prüfung, wenn Loginformular gesendet wurde if ( $dbPwd ne $pwd ) { # Falsche Anmeldung über das # Loginformular ausgeben } # Prüfung sowohl bei Loginformular als auch, wenn # das Cookie gesendet wurde if ( $status ne 'e' ) { # User ist gesperrt. # Entsprechende Meldung ausgeben. } # Wenn diese Programmstelle erreicht wird, dann ist # der User authentisiert und autorisiert

Persistente Datenbankverbindungen

707

10.6 Persistente Datenbankverbindungen Bisher haben wir zwar eine Datenbankverbindung verwendet, uns aber noch keine Gedanken darüber gemacht, was dies bedeutet. Wenn der Webserver bei jedem Clientrequest eine neue Datenbankverbindung öffnen würde, dann hätten wir einen gewaltigen Performanceverlust zu beklagen, denn diese Operation ist sehr »teuer« hinsichtlich der Performance. Gott sei Dank bietet mod_perl in Verbindung mit Apache::DBI eine elegantere Möglichkeit, wenn man in der Datei startup.pl, die wir bereits eingangs bei der Perl-/ApacheIntegration kennen gelernt hatten, die folgenden Zeilen aktiviert: use Apache::DBI; Apache::DBI->connect_on_init( "dbi:driver(AutoCommit=>1):database", "userid", "passwd" );

Für »driver« ist der entsprechende Datenbanktreiber anzugeben, z.B. »mysql«, für »database« der Name der Datenbank, z.B. »test«, für »userid« der Datenbankuser sowie für »passwd« das Kennwort des Datenbankusers. Außerdem ist zum Beispiel bei mysql »(AutoCommit=>1)« zu entfernen, es sei denn, man besitzt eine Mysql-Version mit Unterstützung von Transaktionen. Sinnvoll ist es auch, das Attribut »RaiseError« auf 0 zu setzen (z.B. (RaiseError=>0)), um zu verhindern, dass sich der Prozess bei Datenbankfehlern beendet. Mehr Informationen über das Package Apache::DBI erhält man mit dem Kommando »perldoc Apache::DBI«. Unter Windows ist zwar das Modul Apache::DBI im Spezialpaket für die Perl-/ Apache-Integration enthalten, jedoch nicht die Dokumentation. Mit Hilfe des Skripts ppm.bat, das mit der Standard-Distribution von Perl ausgeliefert wird, kann man jedoch das Modul auch für die normale Perl-Distribution nachladen und hat dann die Dokumentation zur Hand. Hat man das Skript startup.pl wie gezeigt geändert, dann kann im Apache-Modul die Zeile use DBI ();

entfallen. Das Interessante daran ist, dass man die Datenbankverbindung in CGI-Skripts und in Apache-Perl-Modulen über das Package DBI durchführt (obwohl man es nicht lädt). In Wirklichkeit jedoch werden die Methoden des Packages Apache::DBI benutzt. Dieses wiederum benutzt DBI.

708

10

Perl/Apache-Integration

Stattdessen wird das Modul Apache::DBI benutzt, das einige DBI-Methoden überschreibt und so persistente Datenbankverbindungen ermöglicht. Mit jedem Start eines Apache-Prozesses wird nun automatisch eine Datenbankverbindung geöffnet und so lange offen gehalten, bis der Webserver-Prozess beendet wird. Der Clou bei dieser Methode ist, dass alle Apache-Module und selbst CGI-Skripts, die unter der Verwaltung von mod_perl ablaufen, nun diese überschriebenen Methoden verwenden (und damit die schnellen persistenten Datenbankverbindungen), ohne dass man den Programmcode der Skripts verändern muss. Sogar das Schließen des Datenbank-Handles kann im Programmcode stehen bleiben, weil auch diese Methode des Packages DBI vom Apache::DBI Package überschrieben wird. Das entbindet uns aber keinesfalls davon, darauf zu achten, dass nach Beendigung der Funktion handler() die Datenbankverbindung in jedem Fall geschlossen wird! Wenn zum Zeitpunkt des Webserver-Starts keine Datenbankverbindung möglich ist, sei es, dass der Datenbankserver heruntergefahren oder aus irgendeinem Grund nicht erreichbar ist, lässt sich der Webserver nicht starten. Wenn man alle Performance-Verbesserungen zusammenzählt, die wir mit mod_perl und Apache-Modulen erzielen können, dann erhält man einen Steigerungsfaktor der Antwortgeschwindigkeit, der im Bereich des 100-bis 1000-fachen gegenüber der herkömmlicher CGI-Technik liegt!

10.7 AuthCookie – ein Beispiel Nun wollen wir uns ein praktisches Beispiel ansehen: Authentifizierung und Autorisierung über Cookies mit einem Apache-Modul, das vollständig in Perl geschrieben ist und eine Datenbank für die Userdaten benutzt. Außerdem werden einige Tricks angewendet, um zu verhindern, dass der URI für das Loginformular im Browser nicht sichtbar wird. Bevor ich auf den Programmcode selbst eingehe, möchte ich Ihnen die Datei .htaccess zeigen, mit deren Hilfe das Modul konfiguriert werden kann: 01 02 03 04 05 06 07 08

AuthName Websecurity AuthType Apache::AuthCookie PerlAuthenHandler Apache::AuthCookie::handler PerlSetVar acDebug 5 PerlSetVar acSecretKey ganzgeheimesKennwort PerlSetVar acDbUri dbi:mysql:test PerlSetVar acDbUser undef PerlSetVar acDbPwd undef

AuthCookie – ein Beispiel 09 10 11 12

709

PerlSetVar acCookieName uid PerlSetVar acDummyUri /private/ballaballa.pl PerlSetVar acTemplateDir D:/templates require valid-user

Ein paar Einträge kennen wir schon. Alle Zeilen, die mit PerlSetVar beginnen, dienen der dynamischen Konfiguration unseres Moduls. Was die einzelnen Variablen zu bedeuten haben, werden wir weiter unten noch sehen, wenn ich mit Ihnen den Programmcode des Moduls durchgehe. Als Nächstes sehen wir uns die Datei startup.pl an: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19

#!/apacheProject/Perl/bin/perl -w $ENV{GATEWAY_INTERFACE} =~ /^CGI-Perl/ or die "GATEWAY_INTERFACE not Perl!"; use use use use

Apache::Registry; Apache::Status; Apache::DBI; strict;

use Apache::AuthCookie; Apache::DBI->connect_on_init( "dbi:mysql(RaiseError=>0):test", "", "" ); 1;

In Zeile 11 wird unser Apache-Package Apache::AuthCookie geladen. Die persistente Datenbankverbindung öffnet jeder Webserver-Prozess beim Start in den Zeilen 13 bis 17. Wenn wir in unserem Modul den Webclient überprüfen, benötigen wir Templates für das Loginformular sowie für die Ausgabe, wenn der User gesperrt ist. Die Templates sind in D:\templates unter den Dateien login.html sowie disabled.html abgelegt (siehe auch die Direktive in Zeil 11 von .htaccess). Hier ist das Template für das Loginformular:

Login

710

10

Perl/Apache-Integration

Login-Name:
Kennwort:




Im Template wird die Variable $$(action) verwendet, an deren Stelle durch das Modul ein spezieller URI eingesetzt wird (siehe Zeile 10 der Datei .htaccess). Wir werden noch sehen, wofür wir den brauchen. Im versteckten Formularfeld reqUri setzt das Modul den ursprünglich angeforderten URI ein. Nach erfolgreichem Login muss ein Redirect auf diesen URI durchgeführt werden. Und nun das Template für die Meldung, wenn ein User gesperrt ist:

Nutzersperre

Ihre Kennung wurde leider gesperrt

Und nun zum schwierigsten Teil, unserem Apache Modul: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

package Apache::AuthCookie; BEGIN { unshift( @INC, "D:/apacheProject/Apache/Apache" ); } use strict; use mod_perl qw( 1.25 StackedHandlers MethodHandlers Authen Authz ); use Apache::Constants qw( :response M_POST M_GET ); use Apache::File (); use IO::Handle; STDERR->autoflush( 1 ); use CGI qw( :cgi ); use CGI::Cookie; use Digest::MD5;

AuthCookie – ein Beispiel 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78

use Template (); our $VERSION = '1.00'; our $debug = 0; sub handler { my $r = shift( @_ ); my $prefix = "AuthCookie::handler:"; $debug = $r->dir_config( "acDebug" ); $debug = 0 unless ( defined( $debug ) ); return OK unless $r->is_initial_req(); $r->err_headers_out->add( "Pragma" => "no-cache" ); $r->err_headers_out->add( "Cache-Control" => "private" ); $r->update_mtime( time() ); my $dbUri = $r->dir_config( "acDbUri" ); unless ( $dbUri ) { dbg( "$prefix no DB URI defined" ); return SERVER_ERROR; } my $dbUser = $r->dir_config( "acDbUser" ); unless ( $dbUser ) { dbg( "$prefix no DB User defined" ); return SERVER_ERROR; } $dbUser =~ s/^undef$//; my $dbPwd = $r->dir_config( "acDbPwd" ); unless ( $dbPwd ) { $dbPwd = ""; } $dbPwd =~ s/^undef$//; my $dbh = DBI->connect( $dbUri, $dbUser, $dbPwd ); unless ( $dbh ) { dbg( "$prefix no database access" );

711

712 79 return SERVER_ERROR; 80 } 81 82 my $status = checkUser( $r, $dbh ); 83 84 $dbh->disconnect(); 85 86 return $status; 87 } 88 89 sub dbg { 90 my $level = $debug; 91 92 if ( 93 @_ and 94 defined( $_[ 0 ] ) and 95 ( $_[ 0 ] =~ /^\d+$/ ) 96 ) { 97 $level = shift( @_ ); 98 } 99 100 if ( $level > $debug ) { return; } 101 102 my @date = localtime(); 103 $date[ 5 ] += 1900; 104 $date[ 4 ]++; 105 106 my $dateStr = sprintf( 107 "[%d-%02d-%02d %02d:%02d:%02d] ", 108 reverse( @date[ 0 .. 5 ] ) 109 ); 110 111 STDERR->print( $dateStr ); 112 113 foreach my $arg ( @_ ) { 114 STDERR->print( 115 defined( $arg ) ? $arg : "undef" 116 ); 117 } 118 119 STDERR->print( "\n" ); 120 } 121 122 sub checkUser { 123 my ( $r, $dbh ) = @_; 124 125 my $prefix = "AuthCookie::checkUser:"; 126 127 my $secretKey = $r->dir_config( "acSecretKey" ); 128 unless ( $secretKey ) { 129 dbg( "$prefix no secret key defined" );

10

Perl/Apache-Integration

AuthCookie – ein Beispiel 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180

return SERVER_ERROR; } my $cookieName = $r->dir_config( "acCookieName" ); unless ( $cookieName ) { dbg( "$prefix no cookie name defined" ); return SERVER_ERROR; } my $cookies = $r->header_in( "Cookie" ); if ( $cookies and ( $cookies =~ /$cookieName\s*=\s*([^; ]+)/ ) ) { my $cookieVal = $1; my ( $key, $uid ) = $cookieVal =~ /^([^\.]+)\.(.+)$/; unless ( $key and $uid ) { return SERVER_ERROR; } my $ctx = new Digest::MD5; $ctx->add( $secretKey . $uid ); my $digest = $ctx->hexdigest(); if ( $key ne $digest ) { return SERVER_ERROR; } dbg( "$prefix user '$uid' authenticated" ); my $quid = $dbh->quote( $uid ); my $sql = execute() ) { $dbh->disconnect(); dbg( "$prefix cannot execute SQL ",

713

714 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

10 "'$sql', error = ", $dbh->errstr() ); return SERVER_ERROR; } my ( $status ) = $sth->fetchrow_array(); $sth->finish(); unless ( $status ) { dbg( "$prefix user '$uid' not found" ); return SERVER_ERROR; } if ( $status ne "e" ) { return sendAnswer( $r, "disabled.html" ); } dbg( "$prefix user '$uid' authorized", " through cookie" ); $r->connection->user( $uid ); return OK; } my $dummyUri = $r->dir_config( "acDummyUri" ); unless ( $dummyUri ) { dbg( "$prefix dummyUri not defined" ); return SERVER_ERROR; } my $uri = $r->uri(); if ( ( $r->method_number() eq M_POST ) and ( $uri eq $dummyUri ) ) { dbg( "$prefix got POST" ); my %args = $r->content(); my $login = $args{ "login" } || ""; my $pwd = $args{ "pwd" } || ""; my $reqUri = $args{ "reqUri" } || ""; dbg( "$prefix reqUri = '$reqUri'" );

Perl/Apache-Integration

AuthCookie – ein Beispiel 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282

my $qlogin = $dbh->quote( $login ); my $sql = execute() ) { dbg( "$prefix cannot execute SQL ", "'$sql', error = ", $dbh->errstr() ); return SERVER_ERROR; } my ( $dbPwd, $status ) = $sth->fetchrow_array(); $sth->finish(); unless ( $dbPwd and $status ) { dbg( "$prefix user '$login' not found" ); return SERVER_ERROR; } if ( $pwd ne $dbPwd ) { dbg( "$prefix user '$login' has ", "wrong password" ); $r->uri( $reqUri ); $r->method_number( M_GET ); $r->method( "GET" ); $r->header_out( "Location", $reqUri ); $r->status( REDIRECT ); return REDIRECT; } if ( $status ne "e" ) { dbg( "$prefix user '$login'", " not authorized" ); return sendAnswer(

715

716 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 } 323 324 sub 325 326 327 328 329 330 331 332 333

10 $r, "disabled.html" ); } my $ctx = new Digest::MD5; $ctx->add( $secretKey . $login ); my $digest = $ctx->hexdigest(); my $cookieVal = "$digest.$login"; $r->err_header_out( "Set-Cookie", CGI::cookie( "-name" => $cookieName, "-value" => $cookieVal, "-path" => "/" ) ); $r->connection->user( $login ); $r->uri( $reqUri ); $r->method_number( M_GET ); $r->method( "GET" ); $r->header_out( "Location", $reqUri ); $r->status( REDIRECT ); return REDIRECT; } dbg( "$prefix sending login form", "uri = '$uri'" ); return sendAnswer( $r, "login.html", { "action" => $dummyUri, "reqUri" => $uri, } );

sendAnswer { my ( $r, $templName, $subs ) = @_; my $prefix = "AuthCookie::sendAnswer:"; my $templDir = $r->dir_config( "acTemplateDir" ); unless ( $templDir and ( -d $templDir ) ) { dbg( "$prefix no template directory",

Perl/Apache-Integration

AuthCookie – ein Beispiel 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 } 352 353 1;

717

" defined" ); return SERVER_ERROR; } my $path = "$templDir/$templName"; my $templ = new Template( $path ); unless ( $templ ) { dbg( "$prefix cannot open $path" ); return SERVER_ERROR; } $r->send_http_header( "text/html" ); $r->print( $templ->substitute( $subs ) ); return DONE;

Beginnen wir mit dem einfachen Teil: Im BEGIN-Block unseres Moduls habe ich das Verzeichnis für die Suche nach Modulen hinzugefügt, in dem die Datei Template.pm abgelegt ist, die in Zeile 28 geladen wird. Die Funktion handler(), deren Code ab Zeile 33 beginnt, wird von mod_perl immer dann aufgerufen, wenn ein Client einen URI anfordert, der durch die Datei .htaccess geschützt ist. (Zur Erinnerung: Der Schutz ist rekursiv ab dem Verzeichnis, in welchem die Datei steht.) Wie üblich bei Apache-Modulen, wird der Funktion das RequestObjekt als Argument übergeben. In Zeile 38 setzen wir den Debug-Level auf den Wert, der in der Datei .htaccess mit der Direktive PerlSetVar acDebug angegeben ist. Die Zeile 41 kennen wir schon, an Teilrequests sind wir nicht interessiert. In den Zeilen 43 bis 51 wird alles Menschenmögliche versucht, den Browsern und Proxyservern das Caching abzugewöhnen, denn bei dynamisch erzeugten Seiten darf kein Cache wirksam sein. Anschließend holen wir uns aus .htaccess die Konfigurationsparameter für die Datenbankverbindung, die in Zeile 74 aufgebaut wird. Dabei müssen wir die Werte für den Datenbankuser und sein Kennwort, die in der Konfigurationsdatei auf undef gesetzt sind, durch leere Strings ersetzen. Das liegt daran, dass die Direktive PerlSetVar zwei Argumente benötigt. Zum Schluss rufen wir die Funktion checkUser() auf, schließen die Datenbankverbindung und geben den erhaltenen Returnstatus zurück.

718

10

Perl/Apache-Integration

Bemerkenswert ist, dass die Datenbankverbindung in Wirklichkeit gar nicht geschlossen, sondern das Handle für eine erneute Benutzung von Apache::DBI freigegeben wird. Aufgrund des sehr kurzen Programmcodes können wir uns vermutlich denken, dass die eigentliche Arbeit in der Funktion checkUser() erledigt wird. Diese nehmen wir uns jetzt vor: Zuerst lesen wir den geheimen Key, den wir für die Verschlüsselung des Cookies benötigen, sowie den Namen des Cookies aus der Konfigurationsdatei .htaccess. Anschließend versuchen wir, den eventuell vom Client gesendeten Cookie für die Authentifizierung aus dem HTTP-Header zu lesen. Hinweis: Im Client-Header Cookie stehen alle Cookies des Clients, deshalb müssen wir mit Pattern Matching den für uns relevanten extrahieren. Das Cookie (das wir weiter unten im Code selbst an den Client schicken werden) ist nach folgendem Schema aufgebaut: .

ist der Loginname des Users, enthält den verschlüsselten Wert des Strings $secretKey$uid. Wir müssen nun den Loginnamen, den wir aus dem Cookie extrahiert haben, und unseren eigenen geheimen Key verschlüsseln. Dann vergleichen wir das Ergebnis mit dem , den wir ebenfalls aus dem Cookie extrahiert haben. Sind beide verschlüsselten Werte gleich, dann ist der User authentifiziert. Falls nicht, geben wir einen Server Error zurück. Das war die Authentifizierung, bei der es darum geht, einen User zu identifizieren. Jetzt müssen wir aber den User noch autorisieren, d.h., wir prüfen, ob der User überhaupt auf den geschützten Bereich zugreifen darf. Hierzu lesen wir den Datensatz für den User aus der Datenbank und überprüfen, ob der User gesperrt oder vielleicht gar nicht mehr vorhanden ist. Falls eins der beiden zutrifft, erhält der Anwender im Browser eine Meldung, dass seine Kennung gesperrt wurde. Wird der User aber autorisiert, dann setzen wir im Webserver den User, damit taucht der Request im Logfile access_log vom Webserver mit Namen auf: 127.0.0.1 - hemu [25/May/2002:20:27:45 +0200] "GET /private/index.html HTTP/1.1" 200 3

Dieser Auszug aus der Datei access_log zeigt, dass der User mit dem Namen hemu auf den geschützten URI zugegriffen hat.

AuthCookie – ein Beispiel

719

Soweit zum Fall, dass vom Client das Login-Cookie gesendet wurde. Ist das aber nicht der Fall, dann gibt es zwei Möglichkeiten: 왘 Der Request wurde zum ersten Mal abgesetzt oder der URI einfach in der URIZeile des Browsers eingegeben und abgeschickt. Hier tritt Plan B ab Zeile 311 in Kraft. Wir müssen dem Client das Loginformular senden. Dazu verwenden wir die Funktion sendAnswer(), der wir das RequestObjekt, den Namen des Templates sowie eine Hash-Referenz, in der die Ersetzungsvariablen stehen, als Argumente übergeben. Den Returnstatus geben wir nach oben an die Funktion handler() weiter. Dieser ist entweder SERVER_ERROR im Fehlerfall, falls ein Templatefehler aufgetreten ist, oder DONE im Erfolgsfall, d.h., der Webserver darf keine Ausgabe mehr machen, weil unser Modul bereits alle HTTPHeader und den Content geschickt hat. Wir belegen die Templatevariable action mit einem beliebigen URI, den wir aus der Konfigurationsdatei gelesen haben. Wichtig ist hierbei, dass der URI auf ein nicht existentes CGI-Skript zeigt, da wir sonst vom Webserver eine Fehlermeldung erhalten. 왘 Der User hat das Loginformular abgeschickt. In diesem Fall müssen zwei Voraussetzungen erfüllt sein. Die HTTP Methode muss POST sein, und der URI muss unser nicht vorhandener CGI-URI sein, den wir als ACTION im Formular eingetragen hatten. In Zeile 224 holen wir uns die Formulardaten ab. Hinweis: Bei der HTTP-Methode POST können wir nicht $r->args() verwenden, diese Funktion liefert nur bei der GETMethode etwas ab. Anschließend versuchen wir den User aus der Datenbank zu lesen und müssen im Unterschied zur Cookie-Authentifizierung auch das Kennwort überprüfen. Wird der User nicht in der Datenbank gefunden, geben wir einen Server Error zurück. Ist das Kennwort falsch (Zeile 264 bis 275), dann passiert etwas scheinbar Seltsames: Wir machen einen Redirect auf den ursprünglichen URI. Zusätzlich ändern wir die HTTP-Methode von POST auf GET. Die Folge davon ist, dass der Browser nun noch einmal den ursprünglichen Request sendet, wieder ohne Cookie, der Webserver daraufhin nochmals unser Modul aktiviert. Wieder schicken wir das Loginformular. Zu guter Letzt kommen wir zu dem Fall, dass der Anwender das Loginformular richtig ausgefüllt hat und von uns autorisiert wurde. In diesem Fall müssen wir den ursprünglich angeforderten Request als OK abhaken und ein Cookie in die HTTP-Header einfügen (Zeile 287 bis 299). Wir verschlüsseln das Cookie wie oben bereits besprochen.

720

10

Perl/Apache-Integration

Wichtig ist, dass wir das Cookie mit der Funktion err_header_out() setzen, nicht mit header_out(), weil wir einen Redirect auf den ursprünglich angeforderten URI durchführen müssen. Nun noch kurz zur Funktion sendAnswer(): Das Einzige, was wir jetzt noch erklären müssen, sind die Zeilen 347 bis 350. Zunächst schicken wir den Content-Type-Header, anschließend den Inhalt des gefüllten Templates, und am Schluss müssen wir durch den Returncode DONE dafür sorgen, dass kein weiteres Modul des Webservers mehr eine Ausgabe an den Client schickt. Das war’s, wenn Sie alles verstanden haben, dann können Sie sich jetzt als Web-Guru bezeichnen.

A Style Guide 1. Beginne Namen von Variablen und Funktionen mit einem Kleinbuchstaben. 2. Verwende nur englische Namen für Identifier (deutsch: »Bezeichner«) von Variablen und Funktionen. 3. Beginne Namen von Klassen mit einem Großbuchstaben. 4. Beginne Dateinamen von Perl-Modulen mit einem Großbuchstaben. 5. Verwende in Dateinamen niemals Leerzeichen. 6. Statt eines Unterstrichs zur Trennung von Wortteilen in Identifiern werden Großbuchstaben zur Kennzeichnung eines neuen Wortteiles benutzt. Gut: my $varWithWordParts;

Schlecht: my $var_with_word_parts;

7. Statt harter TABs mit einer Länge von 8 Zeichen wird eine Einrückung mit 4 Blanks verwendet. 8. Vor dem Semikolon steht kein Leerzeichen. 9. Funktionsaufrufe immer mit runden Klammern angeben. Gut: print( sort( @a ) );

Schlecht: print sort @a;

10. Kein Leerzeichen zwischen den Funktionsnamen und die öffnende runde Klammer. 11. Blöcke in geschweiften Klammern: Die öffnende geschweifte Klammer steht entweder in derselben Zeile wie das zugehörige Schlüsselwort, die schließende geschweifte Klammer steht in derselben Einrückungsebene wie das Schlüsselwort.

722

A

Style Guide

12. Gut: sub myFunc { ... } sub myOtherFunc { ... }

Schlecht: sub myFunc { ... }

13. Leerzeichen um nahezu alle Operatoren setzen. Gut: if ( $a == 1 ) { print( "a" ); }

Schlecht: if($a==1){print("a");}

14. Leerzeilen einfügen, um logisch zusammengehörende Programmteile zu kennzeichnen. 15. Namen von Konstanten sollten komplett in Großbuchstaben gehalten sein.

B Vordefinierte Variablen Perl bietet eine ganze Reihe von vordefinierten Variablen an, die vom Programmierer direkt benutzt werden können, ohne vorher deklariert zu werden. Die meisten vordefinierten Variablen sind jederzeit verfügbar. Einige sind je nach Programmlogik nur kurzzeitig definiert. Fast alle vordefinierten Variablen haben eine Kurzform sowie eine Langform für den Variablennamen (zum Beispiel ist $@ die Kurzform für $EVAL_ERROR). Im Allgemeinen verwendet man die Kurzform, jedoch kann auch die Langform nach folgendem Import angewandt werden: use English;

Manche Variablen haben noch einen dritten Namen, der in seiner Länge und damit in seiner Aussagekraft zwischen Kurz- und Langname liegt. Meist wird jedoch der Kurzname verwendet. Im Folgenden werden die häufigsten vordefinierten Variablen erklärt. Eine komplette Liste aller vordefinierten Variablen findet man in der Online-Dokumentation unter dem Thema »perlvar«.

B.1 @_ Array, in dem die Übergabe-Argumente von Funktionsaufrufen stehen. Diese Variable ist nur innerhalb von Funktionsrümpfen vorhanden. Die Perl-Funktion shift() verwendet @_ automatisch in Funktionen, wenn kein Argument angegeben wird. (Vorsicht: Außerhalb von Funktionen ist dies nicht der Fall, hier wird in Hauptprogrammen die Variable @ARGV verwendet, wenn kein Array angegeben ist.): sub err { foreach my $arg ( @_ ) { print( STDERR defined( $arg ) ? $arg : "undef" ); }

724

B

Vordefinierte Variablen

print( STDERR "\n" ); } sub testArg { # Implizite Verwendung von @_ my $arg = shift(); return $arg ? 1 : 0; }

B.2 @ARGV In diesem Array stehen die Kommandozeilen-Argumente von Hauptprogrammen. Die Perl-Funktion shift() verwendet diese Variable automatisch, wenn shift() in Hauptprogrammen ohne Argumente aufgerufen wird. Anders als zum Beispiel in C enthält @ARGV nur die Aufruf-Argumente ohne den Programmnamen des Perl-Skripts: # Aufruf eines Perl-Skripts # Achtung: Enthält ein Aufruf-Argument Leerzeichen, # dann muss das gesamte # Argument in Quotes (einfache oder doppelte) gesetzt # werden. myPerlscript.pl 1 "zweites Argument" -v # @ARGV enthält insgesamt drei Elemente: # ( 1, "zweites Argument", "-v" ) # Im Perl-Skript greift man auf die Argumente # über @ARGV zu. # Implizite Verwendung von @ARGV: my $arg1 = shift(); # Explizite Verwendung von @ARGV: my $arg2 = shift( @ARGV ); while ( @ARGV ) { my $arg = shift( @ARGV ); ... }

B.3 %ENV Diese Hash-Variable enthält alle Umgebungsvariablen der Shell (KommandozeilenInterpreter), in welcher das Perl-Skript läuft. In Windows kann man in einer DOS-Box die Umgebungsvariablen mit dem Kommando set ausgeben, in UNIX je nach verwendeter Shell mit setenv oder set bzw. env.

$0

725

Wird ein neues Element im Perl-Skript hinzugefügt, dann vererbt sich diese Umgebungsvariable an Kindprozesse, die durch die Perl-Funktion fork() gestartet werden. # Ausgabe aller Umgebungsvariablen foreach my $name ( sort( keys( %ENV ) ) ) { print( "$name = $ENV{ $name }\n" ); } # Setzen einer neuen Umgebungsvariable $ENV{ "debug" } = 1;

B.4 $0 In dieser Variable wird der Pfad des Perl-Skripts abgelegt, so wie es aufgerufen wurde: #!/usr/bin/perl -w use strict; my $myPath = $0; # $0 enthält genau den Pfad des # Perl-Skripts, wie es aus der Kommandozeile # heraus aufgerufen wurde, # z.B. /home/user1/perl/myScript.pl # Der folgende Code extrahiert daraus den letzten # Pfad-Anteil (myScript.pl) if ( $myPath =~ m~/([^/]+)$~ ) { $myPath = $1; } print( "Dateiname des Perl Skripts = '$myPath'\n" ); exit( 0 );

Noch einmal zur Verdeutlichung: Der Inhalt von $0 hängt davon ab, wie man das Skript aufruft: % myScript.pl $0 enthält myScript.pl (UNIX). In DOS kann die Sache etwas anders aussehen, meist enthält $0 in diesem Fall den absoluten Pfadnamen des Skripts, z.B. D:\temp\myScript.pl. Ruft man das Skript aber nicht direkt auf, sondern über den Perl Interpreter, dann erhält man auch in DOS dasselbe Ergebnis: D:\temp>perl myScript.pl

Nun enthält $0 auch unter DOS myScript.pl.

726

B

Vordefinierte Variablen

D:\>perl .\myScript.pl $0 enthält .\myScript.pl. D:\temp>perl ./myScript.pl $0 enthält ./myScript.pl. Wenn das Skript nicht direkt aufgerufen wird, sondern über den Perl Interpreter, dann darf man als Verzeichnistrenner auch in DOS den Slash »/« anstelle des Backslashs »\« verwenden. Dieses Feature sollte man immer nutzen, weil man damit unabhängig vom Betriebssystem immer denselben Code benutzen kann. D:\temp>perl D:/temp/myScript.pl $0 enthält D:/temp/myScript.pl

B.5 @INC Diese Variable wird vom Perl-Interpreter benutzt, um nach Perl-Modulen zu suchen, die mit use oder require geladen werden. Jedes Array-Element entspricht einem Verzeichnis, in dem nach Modulen gesucht wird. Wenn vor dem Aufruf eines Skripts die Umgebungsvariable PERLLIB gesetzt wird, dann hängt der Perl-Interpreter die darin abgelegten Verzeichnisse vor die Default-Verzeichnisse. Gibt man beim Aufruf eines Perl-Skripts mit dem Schalter -I weitere Include-Verzeichnisse an, dann werden diese an den Anfang des Arrays gesetzt. Beispiele (der folgende Programmcode ist in der Datei printInc.pl abgelegt): #!D:/Perl/bin/perl.exe -w use strict; # Ausgabe aller Verzeichnisse, in denen nach Perl# Modulen gesucht wird print( "Include-Verzeichnisse:\n\t'", join( "'\n\t'", @INC ), "'\n" ); exit( 0 );

Aufruf des Skripts (hier unter Windows in einer DOS-Box): D:\temp>printInc.pl

@INC

727

Ausgabe des Skripts: Include-Verzeichnisse: 'D:/Perl/lib' 'D:/Perl/site/lib' '.'

Setzen der Umgebungsvariable PERLLIB Beachte: Es wird der Slash als Verzeichnistrenner verwendet, obwohl das Betriebssystem Windows ist! D:\temp>set PERLLIB=D:/temp D:\temp>printInc.pl Include-Verzeichnisse: 'D:/temp' 'D:/Perl/lib' 'D:/Perl/site/lib' '.'

Direkte Angabe von Verzeichnissen über den Schalter -I: D:\temp>perl -IC:/temp -ID:\tmp printInc.pl

Ausgabe des Skripts: Include-Verzeichnisse: 'C:/temp' 'D:\tmp' 'D:/temp' 'D:/Perl/lib' 'D:/Perl/site/lib' '.'

Beachte: Da der Schalter -I vom Perl-Interpreter selbst verarbeitet wird, kann das Skript nicht direkt aufgerufen werden, sondern nur indirekt über den Perl-Interpreter. Der folgende Aufruf hat also nicht das gewünschte Ergebnis: D:\temp>printInc.pl -IC:/temp -ID:\tmp

Die Schalter »-I« sind in diesem Fall Argumente für das Skript printInc.pl, nicht für den Perl-Interpreter, und werden somit auch nicht von diesem verarbeitet. Man kann im Perl-Skript selbst auch neue Verzeichnisse hinzufügen: #!D:/Perl/bin/perl.exe -w BEGIN { unshift( @INC, "D:/temp/myModules" ); }

728

B

Vordefinierte Variablen

use strict; ... use MyModule (); ... exit( 0 );

Im Beispiel wird angenommen, dass in dem Verzeichnis D:\temp\myModules das Modul MyModule.pm abgelegt ist. Da wir zum Laden des Moduls die use-Direktive anstelle von require verwendet haben, muss der Programmcode für die Erweiterung des Suchpfades in den BEGIN-Block des Skripts gestellt werden, damit er zur Übersetzungszeit ausgeführt wird, nicht erst zur Laufzeit des Skripts, weil die useDirektive ebenfalls zur Übersetzungszeit abgearbeitet wird. Ändern wir unseren Programmcode, indem wir die Zeile für die Erweiterung des Suchpfades aus dem BEGIN-Block entfernen: #!D:/Perl/bin/perl.exe -w use strict; unshift( @INC, "D:/temp/myModules" ); ... use MyModule (); ... exit( 0 );

Dann erhalten wir eine Fehlermeldung des Perl-Interpreters, weil er zunächst versucht, die use Direktive auszuführen, bevor der normale Programmcode abgearbeitet wird. Zu diesem Zeitpunkt ist der Suchpfad für Perl-Module jedoch noch nicht erweitert. Erst, wenn wir auch die use-Direktive durch require ersetzen, läuft das Skript ohne Fehler, weil dann das Laden des Moduls erst zur Laufzeit des Skripts erfolgt. In diesem Fall wurde die unshift()-Funktion bereits ausgeführt: #!D:/Perl/bin/perl.exe -w use strict; unshift( @INC, "D:/temp/myModules" );

%INC

729

... require MyModule; ... exit( 0 );

B.6 %INC In diesem Hash legt der Perl-Interpreter alle durch die use- oder require-Direktive geladenen Module ab. Diese Variable ist nicht zu verwechseln mit @INC! In den Keys der Hash-Elemente stehen die Dateinamen der Module, die Values der Hash-Elemente enthalten den gesamten Pfadnamen: #!D:/Perl/bin/perl.exe -w use strict; # Ausgabe aller bereits importierten Module foreach my $key ( sort( keys( %INC ) ) ) { print( "$key = $INC{ $key }\n" ); } exit( 0 );

Das gezeigte Skript gibt ein geladenes Modul aus: strict.pm = D:/Perl/lib/strict.pm

B.7 $$, $PID, $PROCESS_ID In dieser Variable speichert der Perl-Interpreter die Prozess-ID des Interpreter-Prozesses ab, der den Programmcode abarbeitet. Die Prozessnummer ist in jedem Fall eine systemweit eindeutige Nummer (in UNIX erhält man eine Liste der Prozesse mit dem ps Kommando, in Windows mit dem Task-Manager). Beispiel: #!D:/Perl/bin/perl.exe -w use strict; print( "Meine Prozess ID = $$\n" ); exit( 0 );

730

B

Vordefinierte Variablen

Ausgabe des Skripts: Meine Prozess ID = 776

B.8 $@, $EVAL_ERROR In dieser Variablen wird die Fehlermeldung abgelegt, wenn der Perl-Interpreter auf Laufzeitfehler stößt. Sie wird meist im Zusammenhang mit der Perl-Funktion eval() benutzt. Beispiel: D:\temp>perl -w use strict; my $i = 1; my $j = 0; eval { print( $i / $j ); }; print( "Fehler = '$@'\n" ) if ( $@ ); exit( 0 ); ^Z Fehler = 'Illegal division by zero at - line 5. ' D:\temp>

Hätte man statt der Variablen $i und $j Konstanten verwendet, dann würde die Fehlermeldung bereits früher erfolgen, weil der Interpreter den Fehler bereits zur Übersetzungszeit des Programmcodes erkannt hätte: D:\temp>perl -w use strict; eval { print( 7 / 0 ); }; Illegal division by zero at - line 3.

B.9 $_, $ARG In dieser Variable werden Eingabedaten abgelegt, wenn von STDIN gelesen wird (das ist in der Regel die Tastatur). Auch beim Suchen und Ersetzen mit Hilfe des Binding-Operators (Pattern Matching) wird diese Variable benutzt, allerdings empfehle ich Ihnen, dieses Feature selten zu verwenden. Da die Variable $_ von vielen integrierten Funktionen verwendet wird, kann es zu Nebeneffekten kommen, wenn man sie im Perl-Code selbst verändert. Deshalb sollte die Variable selten, und wenn, dann nur lesend benutzt werden.

$1, $2 ...

731

Beispiel: # Lesen von der Standard-Eingabe (meist Tastatur) while ( ) { print(); } # Ausführlicher Code ohne Benutzung von $_: while ( defined( my $line = ) ) { print( $line ); } # Noch etwas ausführlicher: while ( defined( my $line = ) ) { print( $line ); }

Die Variable »$_« wird von der Perl-Funktion find() des Perl-Moduls File::Find mit dem aktuellen Datei- oder Verzeichnisnamen belegt und kann in der Funktion, die von File::Find::find() aufgerufen wird, verwendet werden. Es wird allerdings nicht der absolute Pfadname, sondern nur der letzte Bestandteil des Pfades gespeichert. Um den kompletten Pfadnamen zu erhalten, muss man die Variable $File::Find::name benutzen.

B.10 $1, $2 ... Diese Variablen sind nur verfügbar in Pattern Matching-Operationen, wenn ein oder mehrere Pattern in runden Klammern stehen. Auf die Variablen kann nur lesend zugegriffen werden. Beispiel, das in einer Datei nach HTML-Tags sucht: #!D:/Perl/bin/perl.exe -w use strict; use FileHandle; my $fh = new FileHandle( $ARGV[ 0 ], "r" ); my $data = join( "", ); undef( $fh ); while # # #

( $data =~ //gs ) { In $data werden alle Strings gesucht, die in spitzen Klammern stehen und HTML-Tags darstellen.

732

B

Vordefinierte Variablen

# Jedes Mal, wenn ein solcher String # gefunden wird, stellt Perl das # gefundene Tag (ohne die spitzen Klammern) in # der Variable $1 zur Verfügung. # Enthält die Datei z.B. den String , # dann steht in $1 der String # "html". my $tag = $1; # Das gefundene Tag kann zusätzlich zum Namen # auch noch Attribute enthalten, # die in der nächsten Programmzeile # extrahiert werden. # Es wird im gefundenen Tag nach gültigen # Zeichen für den Tagnamen gesucht, # dem durch Leerzeichen getrennt weitere # Attribute folgen können. # Abschließende Tags haben am Anfang # das Zeichen "/". # Da im Suchpattern 3 runde Klammern # angegeben sind, füllt der Interpreter # die Variablen $1, $2 und $3, die wir # im Beispiel implizit durch die # Zuweisung an eine Liste mit 3 Variablen verwenden. my ( $closing, $name, $val ) = $tag =~ m~^(/?)([^\s]+)\s*(.*)~; $val = "" unless( defined( $val ) ); print( "HTML-Tag = $name, Attribute = $val\n" ); }

Ich habe die Option s angegeben, damit auch HTML-Tags gefunden werden, die sich über mehrere Zeilen erstrecken. Vor allem HTML Editoren umbrechen gerne Zeilen aus unerfindlichen Gründen mitten in HTML-Tags. Ohne die Option s würde das Sonderzeichen Punkt bei Zeilenendezeichen keinen Treffer erzielen. Alternativ kann man das Pattern auch so schreiben: while ( $data =~ /]+)>/g ) {

Hier wurde statt des Punktes eine exklusive Zeichenklasse verwendet, deshalb kann man sowohl das Fragezeichen für Minimal Matching als auch die Option s weglassen.

@+, @LAST_MATCH_END

733

Die Variablen »$1«, »$2« etc. sind nur im umgebenden Block des Pattern Matchings verfügbar: if ( $var =~ /(hallo)/ ) { # Hier existiert $1 } else { # Hier existiert $1 nicht } # Hier existiert $1 nicht mehr

B.11 @+, @LAST_MATCH_END Die Variable @+ ist wie die Variable @- bei Pattern Matching-Operationen verfügbar. Sie enthält die Offsets des auf einen Treffer folgenden Zeichens innerhalb des Suchstrings. Wenn z.B. im String »hallo Welt« nach »elt« gesucht wird, dann hat die Variable @+ ein Element und in $+[ 0 ] steht 10, denn dies ist der Offset des ersten Zeichens nach der Zeichenkette »elt« im Suchstring. Weitere Beispiele siehe bei Pattern Matching.

B.12 @-, @LAST_MATCH_START Diese Variable ist bei Pattern Matching-Operationen verfügbar. Sie enthält die Offsets der im Suchstring gefundenen Zeichenketten. Wenn z.B. im String »hallo Welt« nach »elt« gesucht wird, dann hat die Variable @- ein Element und in $-[ 0 ] steht 7, denn dies ist der Offset der Zeichenkette »elt« im Suchstring. Weitere Beispiele siehe bei Pattern Matching.

B.13 $&, $MATCH Diese Variable wird beim Pattern Matching verwendet und ist nur in den Programmblöcken gültig, in denen ein Treffer erzielt wurde. Sie enthält den gefundenen Treffer. Die Benutzung dieser Variable kann zu deutlichen Performanceeinbußen führen. Während in den Dollar-Variablen $1, $2 usw. nur die in runden Klammern stehenden Teile des Patterns bei einem Treffer gefüllt werden, enthält $& den gesamten Treffer eines Patterns, auch wenn keine Klammern angegeben sind.

734

B

Vordefinierte Variablen

Beispiel: my $s = "Das ist toll!"; # Suche nach einem Wort, das von einem Ausrufezeichen # gefolgt ist. if ( $s =~ /(\w+)!/ ) { print( "\$1 = '$1', \$& = '$&'\n" ); }

Der Programmcode gibt aus: $1 = 'toll', $& = 'toll!'

B.14 $`, $PREMATCH Diese Variable wird beim Pattern Matching verwendet und ist nur in den Programmblöcken gültig, in denen ein Treffer erzielt wurde. Sie enthält den String vor dem gefundenen Treffer. Die Benutzung dieser Variable kann zu deutlichen Performanceeinbußen führen. Beispiel: my $s = "Wow! Ich finde Perl einfach toll!"; $s =~ /!/;

in »$`« steht der String »Wow«.

B.15 $', $POSTMATCH Diese Variable wird beim Pattern Matching verwendet und ist nur in den Programmblöcken gültig, in denen ein Treffer erzielt wurde. Sie enthält den String nach dem gefundenen Treffer. Die Benutzung dieser Variable kann zu deutlichen Performanceeinbußen führen. Beispiel: my $s = "Wow! Ich finde Perl einfach toll!"; $s =~ /!/;

in »$'« steht der String » Ich finde Perl einfach toll!«.

$|, $OUTPUT_AUTOFLUSH

735

B.16 $|, $OUTPUT_AUTOFLUSH Diese Variable wird verwendet, um den Ausgabepuffer nach jedem print() zu leeren. Normalerweise werden die Ausgabedaten nicht sofort an den Ausgabekanal gesendet, sondern vom Betriebssystem in einen Puffer geschrieben, der erst geleert wird, wenn er voll ist oder wenn das Programm beendet wird. Beispiel: # Mit select() FileHandle für Ausgabe besorgen my $fh = select( STDOUT ); # Ausgabepuffer außer Betrieb setzen $| = 1; # Altes FileHandle wiederherstellen select( $fh ); # Gleiche Wirkung wie obiger Code use IO::Handle; IO::STDOUT->autoflush( 1 );

B.17 $,, $OFS, $OUTPUT_FIELD_SEPARATOR Der Kurzname für die Variable lautet $,, das zweite Komma dient der Trennung der einzelnen Namen. Mit dieser Variable können Sie das Trennzeichen für die Argumente von print()-Aufrufen einstellen. Normalerweise gibt print() alle Argumente ohne Trennzeichen direkt aufeinander folgend aus. Beachte: $, kann auch aus einer Zeichenkette bestehen. Beispiele: use strict; $, = ", "; print( "hallo", "Welt\n" ); print( localtime() ); $, = ""; print( "\n", localtime(), "\n" );

736

B

Vordefinierte Variablen

Hier die Ausgaben des Programmcodes: D:\temp>perl -w use strict; $, = ", "; print( "hallo", "Welt\n" ); print( localtime() ); $, = ""; print( "\n", localtime(), "\n" ); ^Z hallo, Welt 23, 42, 6, 17, 5, 102, 1, 167, 1 2342617510211671 D:\temp>

Hinweis: Wenn Sie in $, das Zeichen \n speichern und die Ausgabe unter Windows in eine Datei schreiben, dann wird daraus das DOS-Zeilenende-Zeichen \r\n, während es unter UNIX unverändert bleibt.

B.18 $\, $ORS, $OUTPUT_RECORD_SEPARATOR Diese Variable beeinflusst das Verhalten bei print()-Aufrufen. Sie können damit den String einstellen, der nach der Ausgabe aller Argumente von print() zusätzlich ausgegeben werden soll. Standardmäßig wird nichts ausgegeben. Beispiel: D:\temp>perl -w use strict; $\ = "\n"; print( "hallo" ); print( "Welt" ); ^Z hallo Welt D:\temp>

Wenn Sie in $\ das Zeichen \n speichern und die Ausgabe unter Windows in eine Datei schreiben, dann wird daraus das DOS-Zeilenende-Zeichen \r\n, während es unter UNIX unverändert bleibt.

$?, $CHILD_ERROR

737

B.19 $?, $CHILD_ERROR In dieser Variable speichert der Interpreter den Statuscode ab, der sich nach Betriebssystemaufrufen ergibt. Allerdings muss der Inhalt von $? noch durch 256 geteilt werden, um den tatsächlichen Status zu erhalten. In einem END-Block des Hauptprogramms enthält die Variable den Exit-Status des Perl-Skripts (der unter Umständen dort auch verändert werden kann). Beispiel: # Aufzurufendes Kommando in Variable schreiben my $cmd = 'dir \temp'; # Kommando mit dem Backtick-Operator aufrufen my $result = `$cmd`; # Status des aufgerufenen Programms ermitteln my $code = $?; # Tatsächlichen Statuscode durch Division # mit 256 extrahieren $code >>= 8; print( "Status des Kommandos = $code\n" );

Hinweis: Die Variable $? wird bei jedem Betriebssystemaufruf neu gesetzt. Man muss sich also den Rückgabestatus direkt nach dem Aufruf eines Kommandos besorgen.

B.20 $!, $ERRNO, $OS_ERROR In dieser Variable speichert der Interpreter den betriebssystemspezifischen Fehlercode von externen Aufrufen ab. Diesen kann man sowohl als Zahl als auch als String auslesen. Beispiel: 01 02 03 04 05 06 07 08 09 10 11

D:\temp>perl -w use strict; my $cmd = "ls /bla"; system( $cmd ); my $status = $!; print( "Status = $status\n" );

738

B

Vordefinierte Variablen

12 $status = int( $status ); 13 14 print( "numerischer Status = $status\n" ); 15 ^Z 16 Der Befehl "ls" ist entweder falsch geschrieben oder konnte nicht gefunden werden. 17 Status = No such file or directory 18 numerischer Status = 2

Die Ausgabe in der Zeile 16 kommt direkt vom Betriebssystem und nicht von unserem Perl-Code.

B.21 %SIG Mit diesem vordefinierten Hash kann man die Signalbehandlung eines Perl-Skripts beeinflussen. Signale können für die prozessübergreifende Kommunikation oder auch ganz einfach dazu benutzt werden, um einen Prozess von außen, d.h. von einem anderen Prozess aus, zu beenden. Werden in einem Programm keine Vorkehrungen für die Signalbehandlung getroffen, dann beendet das Betriebssystem das Programm sofort, wenn an den Prozess ein beliebiges Signal gesendet wird. Welche Signale in einem bestimmten Betriebssystem unterstützt werden, lässt sich unter UNIX relativ einfach ermitteln: Man liest die Datei /usr/include/signal.h. Oft steht die Datei auch unter /usr/include/sys. In Windows ist die Sache nicht ganz so einfach. Allerdings kann man eine Liste von Signalen mit folgendem Programmcode ausgeben: #!D:/Perl/bin/perl.exe -w use strict; foreach my $key ( sort( keys( %SIG ) ) ) { print( "Signal $key\n" ); } exit( 0 );

Das Skript wird in etwa folgende Ausgabe machen: Signal Signal Signal Signal Signal

ABRT ALRM BREAK CHLD CLD

%SIG Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal Signal

739 CONT FPE ILL INT KILL NUM01 NUM05 NUM06 NUM07 NUM10 NUM12 NUM16 NUM17 NUM18 NUM19 NUM24 PIPE QUIT SEGV STOP TERM

Ich habe bei der Ausgabe darauf verzichtet, auch die Values der Hash-Elemente anzuzeigen, da anfänglich alle Hash-Values von %SIG den Pseudowert undef besitzen. Ich möchte hier nicht weiter auf die einzelnen Signale eingehen, da dies den Rahmen des Buches sprengen würde. Die meisten UNIX-Anwender werden aber das Signal KILL bestens kennen, das auch unter der Nummer 9 bekannt ist. Mit diesem Signal kann man nämlich jeden beliebigen Prozess »killen«, d.h. beenden, ohne dass im Programm dagegen etwas unternommen werden kann. Alle anderen Signale jedoch können vom Programmcode abgefangen oder sogar gänzlich ignoriert werden. Ignoriert werden kann ein Signal ganz einfach dadurch, dass man am Anfang des Programmcodes (im Hauptprogramm am besten im BEGIN-Block) das entsprechende Hash-Element auf den Wert IGNORE setzt (der String muss in Großbuchstaben angegeben sein). Beispiel für das Ignorieren des Signals INT (dieses Signal wird ausgelöst, indem man die Tastenkombination [Strg]+[C] drückt): #!D:/Perl/bin/perl.exe -w use strict; BEGIN {

740

B

Vordefinierte Variablen

$SIG{ "INT" } = "IGNORE"; } sleep( 60 ); exit( 0 );

Führt man das Skript aus, dann kann es nicht durch die Tastenkombination [Strg]+[C] abgebrochen werden. Erst nach Ablauf von 60 Sekunden beendet es sich von alleine. Lässt man hingegen den BEGIN-Block weg, kann das Programm jederzeit beendet werden. Es ist auch möglich, als Value des Elements von %SIG eine Referenz auf eine Funktion zu verwenden. In diesem Fall können selbstgeschriebene Signal-Handler implementiert werden. Das sind Funktionen, die automatisch aufgerufen werden, wenn das Programm ein Signal erhält: #!D:/Perl/bin/perl.exe -w use strict; BEGIN { $SIG{ "INT" } = \&handler; } sleep( 60 ); exit( 0 ); sub handler { $SIG{ "INT" } = \&handler; print( "selbstgeschriebener Signal-Handler\n" ); }

Wenn wir den Programmcode ausführen und anschließend [Strg]+[C] drücken, wird jedes Mal der String »selbst geschriebener Signal-Handler« ausgegeben, weil mit jedem Signal die Funktion handler() aufgerufen wird. Jetzt werden Sie sich sicher fragen: »Warum wird in der Funktion handler() das HashElement INT noch einmal gesetzt?«. Wenn wir dies nicht tun, dann reagiert das Programm nur ein einziges Mal auf das Signal über die Funktion. Beim zweiten Signal beendet es sich. Das liegt daran, dass Signale über ein Flag verwaltet werden, das nach dem Aufruf des Signal-Handlers wieder gelöscht wird. Wir müssen das Flag im Signal-Handler also erneut setzen, damit sie beim Eintreffen des nächsten Signals wiederum aufgerufen wird.

%SIG

741

%SIG kann auch dazu verwendet werden, um lästige Warnungen des Perl-Interpreters

zu unterbinden, die normalerweise automatisch auf STDERR ausgegeben werden. In UNIX hat dies bei so genannten »Cronjobs« (das sind Hintergrundprozesse, die zeitgesteuert vom System gestartet werden) die unangenehme Eigenschaft, dass der User, unter dessen Account der Cronjob gestartet wurde, eine E-Mail mit dem Text der Ausgabe erhält. Im Hash %SIG können einige spezielle Elemente gesetzt werden, die der Perl-Interpreter intern verwendet. So dient der Key __WARN__ z.B. dazu, das Verhalten bei Warnungen zu beeinflussen. Sehen wir uns die Auswirkung an einem Beispiel an: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

#!D:/Perl/bin/perl.exe -w use strict; require File::Find; File::Find::find( \&doIt, "." ); exit( 0 ); sub doIt { my $n = $File::Find::name; print( "$n\n" ); } 1;

Wenn wir das Skript laufen lassen, dann gibt der Interpreter neben den Dateien und Directories des aktuellen Verzeichnisses auch noch eine Warnung aus: Name "File::Find::name" used only once: possible typo at - line 13.

Um diese Ausgabe abzuschalten, kann man jetzt mehrere Varianten wählen: 왘 Man verwendet in Zeile 04 die use-Direktive statt der require-Direktive: 04 use File::Find ();

왘 Man schaltet kurzfristig den Warnmechanismus aus: 11 no warnings; 12 my $n = $File::Find::name; 13 use warnings;

왘 Man schließt das FileHandle STDERR: BEGIN { close( STDERR ); }

742

B

Vordefinierte Variablen

왘 Man setzt explizit den Hash-Key $SIG{ "__WARN__" }: BEGIN { $SIG{ "__WARN__" } = sub { return; }; }

Der Nachteil bei den letzten beiden Varianten ist, dass der Warnmechanismus für die gesamte Laufzeit des Programms ausgeschaltet wird. Bei der letzten Variante hat man jedoch den Vorteil, dass damit das Verhalten von Warnungsmeldungen beliebig beeinflusst werden kann, indem man für den Wert des Hash-Elements __WARN__ eine anonyme Funktion (oder auch eine Referenz auf eine selbstimplementierte Funktion) einsetzt. Hier im Beispiel habe ich der Einfachheit halber eine anonyme Funktion gewählt, die nichts anderes tut als sich sofort wieder zu beenden. Da die Warnung vom Perl-Interpreter zur Übersetzungszeit und nicht zur Laufzeit ausgegeben wird, muss der Programmcode in einen BEGIN-Block gestellt werden, damit er ausgeführt wird, bevor der Interpreter das Skript analysiert. Es reicht nicht aus, dass man den Value des Hash-Elements __WARN__ einfach auf IGNORE setzt.

C Vordefinierte Funktionen Dieser Abschnitt enthält eine Übersicht der wichtigsten Perl-Funktionen, die im Lieferumfang der Perl-Distribution enthalten sind. Ich habe bewusst darauf verzichtet, alle Funktionen aufzuführen, vielmehr möchte ich hier diejenigen Funktionen beschreiben, die Sie vermutlich öfter als nur einmal im Jahr benötigen. Denjenigen unter Ihnen, die nach einer hier nicht aufgeführten Funktion suchen, sei das Thema »perlfunc« der Online-Dokumentation ans Herz gelegt.

C.1 abs() Syntax: abs( x )

Liefert den absoluten Wert der Zahl x. Das Ergebnis ist immer positiv. Die Funktion wird verwendet, um unabhängig vom Vorzeichen einer Zahl deren Zahlenwert zu erhalten. Beispiel: abs( 5 ) # liefert 5 abs( -5 ) # liefert 5 # Funktioniert auch mit Fest- und Gleitkommazahlen abs( -3.14 ) # liefert 3.14 abs( -3.5E4 ) # liefert 35000 # Auch Hex- oder Oktal- bzw. Dualzahlen sind möglich abs( -0x12 ) # liefert 18 abs( -012 ) # liefert 10 abs( -0b1100 ) # liefert 12

744

C

Vordefinierte Funktionen

C.2 atan2() Syntax: atan2( y, x )

Liefert den Arcustangens des Quotienten »y/x« in der Einheit »Radiant«. Das Ergebnis liegt im Bereich von -PI bis +PI. Beispiel: atan2( 3, 3 ) # liefert PI/4, dies entspricht 45 Grad

Hinweis: Die Funktion atan2() kann man gut für die Zuweisung der Kreiszahl PI an eine Variable oder Konstante benutzen: my $PI = 4 * atan2( 1, 1 ); # PI enthält 3.14159265358979

C.3 binmode() Syntax: binmode( fileHandle[, discipline ] )

Diese Funktion sollte nur dann verwendet werden, wenn man Binärdateien lesen oder schreiben möchte, nicht aber bei Lese-Operationen mit Textdateien. binmode() muss direkt nach dem Öffnen einer Datei vor der ersten I/O-Operation aufgerufen werden. Das erste Argument ist ein FileHandle, das man nach dem Öffnen der Datei mit der Perl-Funktion open() oder dem FileHandle-Package erhalten hat. Im zweiten Argument kann man entweder :raw (Binärmodus, Default) oder :crlf (Textmodus) angeben. Da man allerdings die Funktion hauptsächlich benötigt,

um Dateien binär zu lesen oder zu schreiben, sollte man dieses optionale Argument weglassen. In manchen Betriebssystemen wie zum Beispiel Windows wird das Zeilenende durch zwei Steuerzeichen gekennzeichnet, während bei anderen nur ein Steuerzeichen dafür verwendet wird. Dieser Unterschied wirkt sich bei allen Lese- und Schreiboperationen aus. Will man nun in einem Perl-Skript unabhängig vom Betriebssystem Dateien bearbeiten, die zum Beispiel nur \n (Linefeed) enthalten, dann muss man vor der ersten I/O-Operation die Funktion binmode() aufrufen, da print() unter Windows sonst nach jeder Zeile zwei Steuerzeichen (\r\n) schreibt.

binmode()

745

Beispiel für die Benutzung von binmode(): # Programmcode ohne binmode() ... use FileHandle; ... my $fh = new FileHandle( "bla.txt", "w" ); unless ( $fh ) { # Fehler ... } $fh->print( "Hello, World\n" ); ... $fh->close();

Unter UNIX arbeitet der Code wie erwartet, es wird genau das in die Datei geschrieben, was im String angegeben ist. Unter Windows jedoch wird vor dem Steuerzeichen \n zusätzlich ein \r eingefügt, ohne dass man darauf Einfluss hat. Will man unabhängig vom Betriebssystem \n als Zeilenendezeichen verwenden, dann muss der Code wie folgt abgeändert werden: # Programmcode mit binmode() ... use FileHandle; ... my $fh = new FileHandle( "bla.txt", "w" ); unless ( $fh ) { # Fehler ... } # Umschalten auf Binärmodus. # Dies sollte vor der ersten I/O-Operation # erfolgen. binmode( $fh ); $fh->print( "Hello, World\n" ); ... $fh->close();

746

C

Vordefinierte Funktionen

C.4 bless() Syntax: bless( referenceVar, class )

Diese Funktion wird in der objektorientierten Programmierung im Konstruktor (Funktion new() eines Packages) verwendet, um die Referenzvariable auf das anonyme Hash, in dem die Daten des Objekts gespeichert werden, zusätzlich zu einer Objektreferenz zu machen. referenceVar ist die Referenzvariable auf das anonyme Hash (meist $self genannt) und class ist der Packagename der Klasse, an welche die Referenzvariable gebunden werden soll. Die Funktion kann auch mit nur einem Parameter aufgerufen werden. In diesem Fall wird die Referenzvariable an die aktuelle Klasse (bzw. das aktuelle Package) gebunden. Es sollte jedoch immer die Version mit zwei Parametern verwendet werden. Zum einen ist der Programmcode dann besser zu verstehen, zum anderen muss man bei abgeleiteten Klassen immer die Fassung mit zwei Argumenten benutzen. Beispiel für bless(): # Modul MyObject.pm package MyObject; ... sub new { my $proto = shift; my $class = ref( $proto ) || $proto; ... my $self = { "name" "id" };

=> "MyObject", => 1,

... # Bis hierher ist $self nur eine normale # Referenzvariable bless( $self, $class ); # Ab hier ist $self zusätzlich eine Objektreferenz # der Klasse MyObject return $self; }

chdir()

747

C.5 chdir() Syntax (Angaben in eckigen Klammern sind optional): chdir([ path ])

Wechselt das aktuelle Verzeichnis auf der Festplatte. Wenn path nicht angegeben ist, wird zunächst versucht, in das HOME-Verzeichnis zu wechseln (dieses steht unter UNIX in der Umgebungsvariablen HOME). Ist diese Variable nicht gesetzt oder existiert das Verzeichnis nicht, wird versucht, in das Verzeichnis zu wechseln, das durch die Umgebungsvariable LOGDIR angegeben ist. Die Funktion liefert TRUE bei Erfolg, FALSE bei einem Fehler.

C.6 chmod() Syntax: chmod( permission, list ) permission muss eine Oktalzahl sein, in welcher die Zugriffsberechtigungen bitcodiert stehen. list ist eine Liste von Datei- oder Verzeichnisnamen, welche die angegebenen Berechtigungen erhalten sollen. chmod() gibt die Anzahl der erfolgreich geänderten Berechtigungen zurück.

Beispiel: unless ( chmod( 0755, "/tmp/bla.txt" ) ) { print( STDERR "kann Berechtigungen für ", "/tmp/bla.txt nicht setzen\n" ); exit( 1 ); } # Die nächste Variante funktioniert nicht, da das # erste Argument keine Oktalzahl, # sondern ein String ist chmod( '0644', "/tmp" );

Man kann für die bitcodierten Berechtigungen auch Konstanten aus dem Modul Fcntl verwenden: use Fcntl( ':mode' ); chmod( S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, "/tmp/bla" );

748

C

Vordefinierte Funktionen

Hinweis: Der Programmcode entspricht dem UNIX-Befehl chmod 0755 /tmp/bla

C.7 chomp() Syntax: chomp( scalar ) chomp( list )

Die Funktion entfernt alle Zeilenendezeichen am Ende des angegebenen Strings (erste Variante). Wird kein skalarer String, sondern eine Liste von Strings (zweite Variante) angegeben, dann werden alle Zeilenendezeichen jedes einzelnen Array-Elements entfernt. Die Funktion liefert die Anzahl der entfernten Zeichen zurück. Manche Programmierer verwenden irrtümlicherweise die Funktion chop() anstelle von chomp(). chop() sollte jedoch nur dann aufgerufen werden, wenn keine Dateioperationen durchgeführt werden, sondern wenn man gezielt das letzte Zeichen eines Strings entfernen will. Hinweis: Als Zeilenendezeichen wird der Inhalt der vordefinierten Variablen $/ verwendet. Dieser ist je nach Betriebssystem unterschiedlich. Beispiele: my $line = ; # # # # # # chomp( $line );

Liest eine Zeile von STDIN (meist ist das die Tastatur). $line enthält zusätzlich zu den eingegebenen Zeichen auch das Zeilenende-Steuerzeichen (\n, \r oder \r\n)

# Entfernt alle Zeilenende-Zeichen # unabhängig vom Betriebssystem # automatisch

# Ganze Datei einlesen my $fh = new FileHandle( "...", "r" ); ... my @lines = ; chomp( @lines );

chop()

749

$fh->close(); # Aus allen Elementen von @lines wurde das # Zeilenendezeichen entfernt

Obwohl das Argument von chomp() nicht als Referenz, sondern als Kopie übergeben wird, ändert die Funktion den Inhalt der übergebenen Variable!

C.8 chop() Syntax: chop( scalar ) chop( list )

Diese Funktion entfernt das letzte Zeichen eines Strings und gibt das entfernte Zeichen zurück. Wird eine Liste (zweite Variante) angegeben, dann entfernt chop() das letzte Zeichen aller Elemente und gibt das entfernte Zeichen des letzten Elements zurück. Wie die Funktion chomp() ändert chop() das übergebene Argument, obwohl dieses nicht als Referenz, sondern als Kopie übergeben wird. chop() wird oft benutzt, um das Zeilenendezeichen nach dem Einlesen einer Zeile aus

einer Datei zu entfernen. Für diese Operationen sollte jedoch die Funktion chomp() verwendet werden, da sie je nach Art des Betriebssystems automatisch das richtige Zeichen entfernt (UNIX verwendet \n als Zeilenendezeichen, DOS die beiden Zeichen \r\n; hier würde chop() nur ein Zeichen entfernen). Beispiel: # Umkehren einer Zahl my $str = "12345"; my $str1 = ""; while ( length( $str ) > 0 ) { # Letztes Zeichen aus $str entfernen # und an $str1 anhängen $str1 .= chop( $str ); } # Es geht natürlich auch anders: my $str1 = join( "", reverse( split( "", $str ) ) ); # Dies ist ein typisches Beispiel für einen "Einzeiler", # die man in Perl häufig findet.

750

C

Vordefinierte Funktionen

C.9 chown() Syntax: chown( uid, gid, list )

Mit dieser Funktion wird die Zuordnung der angegebenen Liste von Dateien bzw. Verzeichnissen zu Usern bzw. Gruppen geändert. Sie wird meist in UNIX verwendet. uid muss die numerische ID des Benutzers sein, gid die numerische Gruppen-ID. list enthält eine Liste der zu ändernden Dateien bzw. Verzeichnisse. chown() gibt die Anzahl der erfolgreich geänderten Pfade zurück. Sie kann nur unter

einer privilegierten Systemkennung aufgerufen werden (in UNIX muss der User, unter welchem das Perl-Skript abläuft, root sein).

C.10 chr() Syntax: chr( characterCode )

Liefert das Zeichen für den angegebenen Zeichencode, der in characterCode steht. Welches Zeichen dem Code entspricht, hängt von der verwendeten Codetabelle ab. Wenn nichts angegeben ist, dann wird der Zeichensatz ISO-LATIN-1 verwendet. Mit use utf8; kann man z.B. explizit Unicode angeben. Beispiele: chr( 65 ) # Entspricht "A". chr( 0x41 ) # Entspricht "A". { use utf8; chr( 0x0041 ) # Entspricht "A". # Außerhalb der geschweiften # Klammern gilt der # Standard-Zeichensatz. }

Hinweis: Um aus einem Zeichen den Zeichencode zu erhalten, verwendet man die Funktion ord().

cos()

751

C.11 cos() Syntax: cos( x )

Liefert den Cosinus von x, der in der Einheit Radiant angegeben sein muss. Beispiel: my $pi = 4 * atan2( 1, 1 ); my $y = cos( $pi ); # $y enthält -1, das entspricht dem Cosinus von 180 Grad

Hinweis: Es gibt keine eingebaute Umkehrfunktion »acos()«. Benutzen Sie hierfür bitte »Math::Trig::acos()« oder den Ausdruck: sub acos { return atan2( sqrt( 1 - $_[ 0 ] * $_[ 0 ] ), $_[ 0 ] ) }

Das Package Math::Trig gehört zur Standard-Distribution von Perl und enthält weitere trigonometrische Funktionen sowie Konstanten wie pi.

C.12 crypt() Syntax: crypt( cleartext, salt )

Diese Funktion verschlüsselt cleartext gemäß dem angegebenen salt, der aus 2 Zeichen der Zeichenklasse »[./0-9A-Za-z]« bestehen muss. Sie arbeitet genauso wie die C-Funktion crypt(), einmal verschlüsselte Strings lassen sich damit also nicht mehr entschlüsseln. Die Funktion crypt() ist nicht besonders gut geeignet für kryptografische Programme (auch deshalb nicht, weil man einen verschlüsselten String nicht mehr entschlüsseln kann), siehe hierfür im CPAN-Verzeichnis die Module unter Crypt bzw. PGP.

752

C

Vordefinierte Funktionen

C.13 defined() Syntax: defined( scalar ) defined( &func )

Diese Funktion testet, ob die angegebene skalare Variable scalar einen definierten Wert oder den Pseudowert undef hat, und wird meist zum Prüfen von Fehlerfällen (Returnwerte von Funktionen) verwendet. Ist eine Funktion als Argument angegeben (zweite Variante), dann prüft defined(), ob die angegebene Funktion definiert worden ist oder nicht. defined() sollte nicht auf Hashes oder Arrays angewendet werden, sondern nur auf skalare Variablen. List-Variablen können niemals undef als Wert haben, sie werden vielmehr daraufhin geprüft, ob sie leer sind oder nicht.

Beispiele: ... my $buf = undef; my $len = read( $fh, $buf, 10 ); unless ( defined( $len ) ) { # Fehler beim Lesen } # Vorsicht: Die folgende Abfrage wäre falsch unless ( $len ) { # Fehler beim Lesen } # Hash prüfen (defined() wird hier nicht verwendet) unless ( %myHash ) { # Hash ist leer } # Referenzvariable my $href = \%myHash; unless ( %{ $href } ) { # Hash ist leer } # Array prüfen (defined() wird hier nicht verwendet) unless ( @myArray ) { # Array ist leer } # Referenzvariable

defined()

753

my $aref = \@myArray; unless ( @{ $aref } ) { # Array ist leer }

Schreibt man eine Funktion, die sowohl einen skalaren als auch einen List-Wert zurückliefern kann, darf man nicht einfach undef zurückgeben, wenn ein Fehler aufgetreten ist. Bei solchen Funktionen muss man wantarray() verwenden, um zwischen skalarem und List-Kontext unterscheiden zu können. Beispiel, wie man es nicht machen sollte: # Funktion, die sowohl einen skalaren als auch einen # List-Wert zurückgeben kann sub myFunc { # Nehmen wir an, es sei ein Fehler vorgekommen: return undef; } # Hauptprogramm my @array = myFunc(); unless ( @array ) { print( "Fehler\n" ); } else { print( "OK\n" ); }

Wenn man den Programmcode laufen lässt, wird immer OK ausgegeben. Dieses Fehlverhalten liegt daran, dass Arrays immer definiert sind. Versucht man, einer Arrayvariable den Pseudowert undef zuzuweisen, dann erzeugt der Interpreter ein Array, das ein einziges Element enthält. Der Wert dieses Elements ist undef. Beispiel, wie es richtig wäre: # Funktion, die sowohl einen skalaren als auch einen # List-Wert zurückgeben kann sub myFunc { # Nehmen wir an, es sei ein Fehler vorgekommen: return wantarray() ? () : undef; } # Hauptprogramm my @array = myFunc(); unless ( @array ) { print( "Fehler\n" ); } else { print( "OK\n" ); }

754

C

Vordefinierte Funktionen

C.14 delete() Syntax: delete( hashKey )

Mit dieser Funktion kann ein Hash-Element gelöscht werden, das durch den Key hashKey angegeben ist Sowohl Key als auch Value werden entfernt. Beispiel: my %hash = ( "key1" => "val1", "key2" => "val2", ); delete( $hash{ "key1" } ); # %hash enthält nun ( "key2" => "val2" )

Beachte: Zum Entfernen von Hash-Elementen sollte immer delete(), niemals undef() benutzt werden. Genau genommen kann als Argument von delete() ein beliebiger Ausdruck angegeben werden, er muss nur als Hash- oder als Array-Element bzw. »Slice« (Teilausschnitt) eines Hashs oder Arrays evaluiert werden können. Vor allem bei Arrays kann man damit jedoch Schiffbruch erleiden, weil ein Array-Element in der Mitte zwar auf den Pseudowert undef gesetzt, aber nicht aus dem Array entfernt wird. Die nachfolgenden Elemente rücken dadurch also nicht nach vorne. Wenn man ein Array-Element wirklich aus der Liste entfernen möchte, dann sollte man besser die Perl-Funktion splice() benutzen. Wird allerdings ein Array-Element mit der Funktion exists() geprüft, das vorher mit delete() entfernt wurde, dann gibt exists() den Wert FALSE zurück. Das Element ist also entfernt, der Index für das Element aber ist weiterhin belegt. Beispiel, das meist unerwartete Ergebnisse liefert: my @arr = ( 1, 2, 3, ); delete( $arr[ 1 ] ); for ( my $i = 0; $i perl -w use strict; die( "Ende" ); exit( 0 ); ^Z Ende at - line 3. D:\temp>perl -w use strict; die( "Ende\n" ); exit( 0 ); ^Z Ende D:\temp>

756

C

Vordefinierte Funktionen

Innerhalb der eval()-Funktion verhält sich die Funktion anders, es wird dann nicht das Hauptprogramm beendet, sondern nur der eval-Block, in dem die() aufgerufen wurde. Außerdem enthält die Variable $@ in diesem Fall die Fehlermeldung. Beispiel: D:\temp>perl -w use strict; eval { die( "Ende durch die" ); print( "Ausgabe in eval\n" ); }; print( "Inhalt von \$@ = '$@'\n" ); exit( 0 ); END { print( "normales Ende\n" ); } ^Z Inhalt von $@ = 'Ende durch die at - line 3. ' normales Ende D:\temp>

Wie man an der Ausgabe sieht, wird der print()-Aufruf im eval-Block nicht ausgeführt, da dieser durch den die()-Aufruf vorher verlassen wird. Die Anweisungen nach dem eval-Block werden jedoch ausgeführt, da zwar der Block beendet wurde, nicht aber das Programm. Der END-Block wird auch dann durchlaufen, wenn der die()-Aufruf außerhalb des eval-Blocks steht. Nach der schließenden geschweiften Klammer des eval-Blocks muss ein Semikolon stehen. Die Funktion die() wird in Perl sehr häufig verwendet. Sie bildet einen ähnlichen Mechanismus ab wie »Exceptions« in Java. Ein großes Problem bei Modulen, nämlich die Verarbeitung von Fehlermeldungen, wird auf diese Weise vereinheitlicht, da diese beim Aufruf von die() in eval-Blöcken immer in der vordefinierten Variable $@ abgelegt werden.

each()

757

C.16 each() Syntax: while ( list = each( %hash ) ) while ( scalar = each( %hash ) )

Die Funktion each() kann sowohl im List-Kontext (erste Variante) als auch im skalaren Kontext (zweite Variante) aufgerufen werden. Im List-Kontext gibt die Funktion den Key und den Value eines Hash-Elements in Form einer Liste aus zwei Elementen zurück. In skalaren Kontext wird nur der Key eines Hash-Elements zurückgeliefert. each() arbeitet ähnlich wie keys() bzw. values(): Die Elemente werden unsortiert übergeben. Befindet sich der interne Positionszeiger am Ende, d.h. wurden alle Elemente übergeben, dann liefert die Funktion den Wert FALSE zurück. Bei nochmaligem Aufruf beginnt ein neuer Durchlauf des Hashs. Man sollte vermeiden, innerhalb einer each()-Schleife neue Elemente hinzuzufügen oder bestehende Elemente zu löschen. Beispiele: D:\temp>perl -w use strict; my %hash = ( "a" => 1, "b" => 2, ); while ( my $key = each( %hash ) ) { print( "$key = $hash{ $key }\n" ); } ^Z a = 1 b = 2 D:\temp>perl -w use strict; my %hash = ( "a" => 1, "b" => 2, ); while ( my ( $key, $val ) = each( %hash ) ) { print( "$key = $val\n" ); } ^Z a = 1 b = 2

C.17 eof() Syntax: eof( fileHandle ) eof eof( list )

758

C

Vordefinierte Funktionen

Hier wird nur die erste Variante mit einem geöffneten FileHandle als Argument beschrieben. Die zweite Variante verwendet das zuletzt benutzte FileHandle und sollte nicht benutzt werden, da der Programmcode dadurch schwerer zu verstehen ist. Die letzte Variante mit einer Liste als Argument hat eine andere, sehr spezielle Bedeutung. Interessierte können mit perldoc -f eof

mehr darüber erfahren. Die Funktion eof() in der ersten Variante liefert TRUE zurück, wenn der Dateizeiger für die Datei des angegebenen fileHandle am Ende der Datei steht, ansonsten FALSE. Beachte: Wenn sich fileHandle auf ein Terminal bezieht, ist ein Einsatz der Funktion eof() nicht sinnvoll, weil diese zunächst versucht, ein Zeichen zu lesen. Bei Erfolg wird

das gelesene Zeichen wieder in den Systempuffer zurückgeschrieben. Dies ist aber bei Terminals (z.B. der Tastatur) unmöglich. Meist kommt man ohne eof() aus, weil man beim Lesen von FileHandles durch den Rückgabewert der Lesefunktionen bereits erkennen kann, ob das Ende erreicht ist oder nicht.

C.18 eval() Syntax: eval( expression ); eval { expression }; eval() wird benutzt, um dynamisch zur Laufzeit Perl-Code auszuführen, wobei

Laufzeitfehler nicht zum Abbruch des Skripts führen (zum Beispiel bei einer Division durch 0). Falls der Programmcode in expression zu einem Laufzeitfehler führt, wird die Fehlermeldung in der vordefinierten Variable $@ gespeichert, ansonsten ist die Variable FALSE. In der ersten Variante wird expression bei jedem Aufruf neu kompiliert und ausgeführt. Sie eignet sich für dynamischen Programmcode, der z.B. aus einer Datei oder aus einer Datenbank eingelesen wird Bei der zweiten Variante wird der in geschweiften Klammern stehende Programmblock nur ein einziges Mal kompiliert. Er wird hauptsächlich benutzt, um »gefährliche« Programmabschnitte ablaufen zu lassen, in denen so genannte »Exceptions« ausgelöst werden können.

eval()

759

In Perl ist dies mit dem Aufruf der Funktion die() verbunden, was normalerweise zum sofortigen Programmabbruch führt. Viele der frei verfügbaren Perl-Module benutzen diesen Mechanismus im Fehlerfall (so z.B. auch das Package DBI). Will man in seinem Programm jedoch selbst die Fehlerbehandlung durchführen, dann müssen solche »Exceptions« abgefangen werden. eval() ist genau die richtige Methode dafür. Wenn nämlich eine Exception innerhalb eines eval-Blocks ausgelöst wird, dann bricht der Interpreter nicht das ganze Programm ab, sondern nur den eval-Block. Im Programm kann man anschließend die Variable $@ überprüfen, um festzustellen, ob ein Fehler aufgetreten ist oder nicht. Bei der zweiten Variante (eval-Block) muss nach der schließenden geschweiften Klammer ein Semikolon stehen. Da ein eval-Block nur einmal kompiliert wird, ist der Code gegenüber der ersten Variante vor allem dann schneller, wenn er in einer Schleife mehrfach ausgeführt wird, zum anderen werden Syntaxfehler bereits zur Übersetzungszeit des Programms erkannt, nicht erst zur Laufzeit. Der Programmcode in expression wird im aktuellen Namespace ausgeführt. Das bedeutet, dass man damit sowohl lesend als auch schreibend auf Variablen zugreifen kann, die außerhalb des eval-Codes definiert wurden. Jetzt ist es an der Zeit für ein Beispiel: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23

#!D:/Perl/bin/perl.exe -w # Programm, das a/b berechnet und das Ergebnis # ausgibt. Divisionen durch 0 werden abgefangen. use strict; print( "Zähler: "); my $a = ; # Zeilenendezeichen entfernen. # Perl ist zwar in dieser Hinsicht kulant, # d.h., man kann $a anschließend auch direkt # als Zahl verwenden (wenn eine gültige # Zahl eingegeben wurde), aber wir wollen # ordentlich programmieren: chomp( $a ); print( "Nenner: " ); my $b = ; chomp( $b ); my $c = undef; eval( '$c = $a / $b' );

760 24 25 26 27 28 29 30 31

C

Vordefinierte Funktionen

if ( $@ ) { print( "Laufzeitfehler '$@'\n" ); } else { print( "c = '$c'\n" ); } exit( 0 );

Wenn wir das Programm z.B. in einer DOS-Box laufen lassen (nehmen wir an, es ist in der Datei div.pl gespeichert), dann erhalten wir im Fehlerfall folgende Ausgabe: D:\temp>div.pl Zõhler: 1 Nenner: 0 Laufzeitfehler 'Illegal division by zero at D:\buecher\perl\AnhangC\code\evalTest.pl line 23, line 2. ' D:\temp>

Wie immer gilt bei sonderbaren Zeichen in der DOS-Box: Der Zeichencode in DOS ist ein anderer als in Windows, daher wird ein ä als õ ausgegeben. Unser Programm wurde also nicht abrupt vom Interpreter abgebrochen. Vielmehr behandeln wir den Fehler selbst, und unser Programmcode wird bis zum Ende abgearbeitet. Beachte: Der dynamische Programmcode in Zeile 23 muss in einfache Quotes gesetzt werden, nicht in doppelte, da der Interpreter sonst bereits vor der Ausführung des Codes in eval() eine Ersetzung der Variablen durch ihre aktuellen Werte durchführen würde. Zur Veranschaulichung hier die falsche Variante: 23 eval( "$c = $a / $b" );

Man kann natürlich auch die zweite Variante in einem eval-Block verwenden: 23 eval { $c = $a / $b };

Ich habe hier deshalb die erste Variante verwendet, damit ich Sie auf die Problematik des Quotings aufmerksam machen kann. Normalerweise benutzt man einen evalBlock, da dieser zum einen schneller ist und da Syntaxfehler zum anderen bereits zur Übersetzungszeit des Programms erkannt werden.

eval()

761

Der Programmcode, der durch eval() ausgeführt wird, beeinflusst die Variablen des Hauptprogramms, d.h., die Variable $c hat nach dem Aufruf von eval() einen anderen Wert als vorher. Die Variablen müssen jedoch vor dem Aufruf von eval() definiert worden sein. Folgender Programmcode funktioniert also nicht: ... 19 chomp( $b ); 20 21 eval { my $c = $a / $b }; ...

Hier wurde die Variable $c im eval-Block definiert. Sie hat außerhalb des Blocks also keine Gültigkeit. Dasselbe Ergebnis hätten wir auch bei der Verwendung der ersten Variante, wo der Code in Quotes gesetzt ist. Nun zu einem weiteren Problem: Angenommen, der Anwender gibt gar keine Zahlen ein. In diesem Fall könnte die Ausgabe etwa so aussehen: D:\temp>div.pl Zõhler: a Nenner: b Argument "b" isn't numeric in division (/) at (eval 1) line 1, line 2. Argument "a" isn't numeric in division (/) at (eval 1) line 1, line 2. Laufzeitfehler 'Illegal division by zero at (eval 1) line 1, line 2. ' D:\temp>

Die ersten beiden Fehlermeldungen kommen nicht von unserem Programm, sondern vom Perl-Interpreter, und zwar scheinbar auch dann, wenn der Programmcode mit eval() ausgeführt wird. Auch diesem Problem kann abgeholfen werden. Wir müssen nur dafür sorgen, dass während der Abarbeitung des Codes in eval() die Warnungen des Interpreters abgeschaltet werden: ... 20 no warnings; 21 eval( '$c = $a / $b' ); 22 use warnings;

Nach der Direktive no warnings; ist der Warnmechanismus von Perl außer Betrieb, deshalb müssen wir ihn anschließend wieder einschalten.

762

C

Vordefinierte Funktionen

Alternativ kann man auch das vordefinierte Hash %SIG verwenden: 20 $SIG{ "__WARN__" } = sub { return; }; 21 eval( '$c = $a / $b' ); 22 delete( $SIG{ "__WARN__" };

C.19 exists() Syntax: exists( hashKey ) exists( arrayElement ) exists( &functionName )

Aus langjähriger Erfahrung weiß ich, dass man beim Tippen des Wortes exists gerne exits eingibt, also aufgepasst! exists() prüft in der ersten Variante, ob der angegebene Hash-Key existiert oder nicht.

Beispiel: my %hash = ( "key1" => 1, ); exists( $hash{ "key1" } ) # liefert TRUE exists( $hash{ "notExistingKey" } ) # liefert FALSE

Die Funktion exists() prüft nicht den Value, sondern nur den Key des Hash-Elements. Will man feststellen, ob sowohl Key als auch Value definiert sind, muss man defined() benutzen: if ( defined( $hash{ "key1" } ) ) # Prüft, ob der Key "key1" existiert und # der Value definiert ist

In der zweiten Variante wird geprüft, ob das angegebene Array-Element existiert oder nicht. Vorsicht ist geboten, wenn man die Funktion delete() verwendet: my @array = ( 1, 2, 3, ); # Löschen des zweiten Elements delete( $array[ 1 ] ); print( "Anzahl Elemente = ", scalar( @array ), "\n" ); if ( exists( $array[ 1 ] ) ) { print( "Element 1 existiert\n" ); }

exists()

763

else { print( "Element 1 existiert nicht\n" ); }

Der Programmcode macht folgende Ausgaben: Anzahl Elemente = 3 Element 1 existiert nicht

Wie man sieht, ist das zweite Element vorhanden und doch nicht existent. Ich rate aus diesem Grund davon ab, die Funktion delete() bei Arrays zu verwenden. Nehmen Sie hierfür lieber splice(). Die Funktion exists() kann in der letzten Variante auch verwendet werden, um festzustellen, ob die angegebene Funktion deklariert worden und damit aufrufbar ist oder nicht. Aufgrund der Tatsache, dass der Perl-Interpreter ähnlich der inetd-Funktionalität (unter UNIX) Funktionen auch erst beim erstmaligen Aufruf laden kann (man nennt dies »Autoload«), ist diese Form der Abfrage manchmal nicht hilfreich. Vorsicht: Der folgende Code ist falsch: if ( exists( &myFunc() ) ) { ... }

Hier wird die Funktion myFunc() aufgerufen statt abgefragt, weil wir nach dem Funktionsnamen runde Klammern gesetzt haben. Richtig ist: if ( exists( &myFunc ) ) { ... }

Vorsicht bei Referenzen: Perl hat die etwas seltsame Eigenschaft, dass Referenzen gewissermaßen automatisch ins Leben gerufen werden. Sehen wir uns hierzu ein kleines Beispiel an: my $ref = undef; if ( exists( $ref->{ "a" }{ "b" } ) ) { print( "ref->{ 'a' }{ 'b' } existiert\n" ); } else { print( "ref->{ 'a' }{ 'b' } existiert nicht\n" ); } if ( exists( $ref->{ "a" } ) ) { print( "ref->{ 'a' } existiert\n" ); } else { print( "ref->{ 'a' } existiert nicht\n" ); }

764

C

Vordefinierte Funktionen

Hinweis: In den print()-Aufrufen habe ich einfache Quotes für die Hash-Keys verwendet, da die doppelten Quotes ja bereits verwendet sind. Eigentlich müssten wir erwarten, dass der Programmcode einen Fehler erzeugt, da ja die Variable $ref zu Beginn auf undef gesetzt wird. Wir erhalten jedoch folgende Ausgabe: ref->{ 'a' }{ 'b' } existiert nicht ref->{ 'a' } existiert

Beim Test von Referenzen, gleichgültig, ob es sich dabei um Hash-, Array- oder Funktionsreferenzen handelt, legt Perl bei einer Abfrage alle oberhalb des zu prüfenden Elements liegenden Referenzen automatisch an. Hierzu sei gesagt, dass dieses Verhalten in einer zukünftigen Perl-Version bereinigt werden soll. Gehen Sie also lieber nicht davon aus, dass geschachtelte Referenzen immer automatisch angelegt werden.

C.20 exit() Syntax (Angaben in eckigen Klammern sind optional): exit([ statusCode ])

Diese Funktion wird verwendet, um das Hauptprogramm eines Perl-Skripts zu beenden. Optional kann der Shell, aus welcher das Programm aufgerufen wurde, ein numerischer Statuscode übergeben werden. Grundsätzlich gilt: Es sollte der Wert 0 bei Erfolg, der Wert 1 bei Misserfolg (Fehler) angegeben sein. Bei Funktionen ist es genau umgekehrt, diese sollte man mit einem TRUE-Wert im Erfolgsfall beenden, ansonsten mit einem FALSE-Wert. Falls im Skript ein oder mehrere END-Blöcke definiert sind, werden diese vor dem Programmende ausgeführt. Hier hat man die Möglichkeit, den Statuscode sowohl zu lesen als auch zu verändern. Die Funktion exit() sollte nur im Hauptprogramm verwendet werden, nicht aber in Funktionen. Wie viele Programmierer haben sich schon halb totgesucht, wenn ein Hauptprogramm aus unersichtlichen Gründen beendet wurde, nur um festzustellen, dass in der zwanzigsten Funktion des siebten Moduls ein exit()-Aufruf stand.

flock()

765

Beispiel für »exit()«: #!D:/Perl/bin/perl.exe -w use strict; my $c = ; chomp( $c ); if ( $c =~ /[^\d]/ ) { exit( -1 ); } exit( $c ); END { if ( $? < 0 ) { print( "ungültige Zahl\n" ); $? = 1; } else { print( "OK\n" ); $? = 0; } }

Das Skript liest von der Standardeingabe und gibt eine Fehlermeldung aus, wenn keine Zahl eingegeben wurde oder die Zahl kleiner als 0 ist. In diesem Fall wird der ExitStatus so verändert, dass er 1 enthält. Wurde eine gültige Zahl eingegeben, dann gibt das Programm den String OK aus und setzt den Status auf 0. Dabei wird das Feature benutzt, dass der END Block des Hauptprogramms immer bei Programmende durchlaufen wird und der Exit-Status sowohl gelesen als auch verändert werden kann.

C.21 flock() Syntax: flock( fileHandle, operation )

Die Funktion flock() wird für Dateisperren verwendet. Diese benutzt man, um die Konsistenz der Daten zu gewährleisten, wenn mehrere Prozesse gleichzeitig sowohl lesend als auch schreibend auf dieselbe Datei zugreifen. Auf Betriebssystemen, bei denen die entsprechende C-Funktion flock() bzw. lockf() oder der Sperrmechanismus über fcntl() nicht implementiert ist, erzeugt die Funktion einen »fatal error«.

766

C

Vordefinierte Funktionen

flock() liefert bei Erfolg den Wert TRUE, andernfalls FALSE zurück.

fileHandle muss ein FileHandle auf eine bereits geöffnete Datei sein. operation muss eine der drei im Fcntl-Modul definierten Konstanten LOCK_SH (shared lock), LOCK_EX (exclusive lock) bzw. LOCK_UN (unlock) sein. Diese Konstanten können in den Namespace des Perl-Skripts mit der Anweisung use Fcntl( ':flock' ); übernommen werden. LOCK_SH erstellt einen Shared Lock, d.h., andere Prozesse können die Datei zwar lesen,

nicht aber schreibend darauf zugreifen. LOCK_EX erzeugt einen Exclusive Lock, d.h., kein anderer Prozess kann auf die Datei

zugreifen (auch nicht lesend). LOCK_UN hebt die Dateisperre auf.

Die im Package Fcntl zusätzlich definierte Konstante LOCK_NB ist ein Flag, mit dem man den Aufruf von flock() nichtblockierend machen kann. Standardmäßig wartet die Funktion so lange, bis die Operation erfolgreich ausgeführt werden kann. Ist dies nicht der Fall, weil zum Beispiel ein anderer Prozess dieselbe Datei zwar gesperrt, aber nicht mehr freigegeben hat, dann würde der flock()-Aufruf ohne Angabe des Flags LOCK_NB das Skript blockieren. Gibt man das Flag an, dann gibt die Funktion in einem solchen Fall sofort den Wert FALSE zurück, ohne den Aufrufer zu blockieren. Um LOCK_NB zu verwenden, muss LOCK_EX bzw. LOCK_SH mit LOCK_NB bitweise mit ODER verknüpft werden. flock() hat keine Wirkung, wenn sich nicht alle Prozesse dieser Funktion bedienen,

um Dateien zu sperren. So kann man eine Datei nach wie vor mit einem Editor ändern oder sogar löschen, selbst wenn diese gerade per Perl-Skript mit flock() gesperrt ist. Dateien, die über NFS erreichbar sind, sollten nicht mit flock() gesperrt werden, da der NFS-Mechanismus zu unsicher ist. Man sollte nicht diejenige Datei sperren, die man bearbeiten möchte, sondern dafür eine eigene Sperrdatei verwenden. Nach den vielen fremden Begriffen nun ein Beispiel zur Veranschaulichung: Wir implementieren ein Perl-Skript, das eine Datei bearbeitet und hierzu eine Dateisperre benutzt. Damit das Beispiel nicht zu unübersichtlich wird, fällt die Bearbeitung der Daten sehr einfach aus. Das Skript schreibt einfach die PID (Prozess-ID, unter der das Skript gestartet wurde und damit im System eindeutig identifiziert werden kann) in die

flock()

767

Datei. Anschließend »schläft« das Programm eine Zeit lang, bevor die Dateisperre wieder aufgehoben wird. Damit kann man reale Verhältnisse in einer Mehr-Prozessumgebung vortäuschen. Sehen wir uns nun zunächst das Hauptprogramm an (die verwendeten Funktionen folgen später): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

#!D:/Perl/bin/perl.exe -w use strict; use FileHandle; # Alle Konstanten importieren, die für flock # benötigt werden use Fcntl qw( :flock ); # Der Pfadname unserer Datendatei my $workfile = "bla.txt"; # Dateisperre erzeugen, damit wir die # einzigen sind, die auf die Daten # zugreifen können unless ( lockFile( $workfile ) ) { exit( 1 ); } # Datendatei zum Schreiben öffnen und # unsere PID schreiben my $fh = new FileHandle( $workfile, "w" ); unless ( $fh ) { err( "kann Datei $workfile nicht anlegen" ); exit( 1 ); } print( "PID $$: Datei gesperrt\n" ); $fh->print( "PID $$\n" ); # Wir legen uns nun für 10 Sekunden schlafen, # damit täuschen wir eine Multi-Prozessumgebung # vor. sleep( 10 ); print( "PID $$: Ende\n" ); # Wir sind fertig mit der Bearbeitung. # Zum Löschen der Dateisperre benutzen # wir eleganterweise einen END-Block.

768

C

Vordefinierte Funktionen

43 END { 44 unlockFile(); 45 } 46 47 exit( 0 );

Ich glaube, der Programmcode ist genügend kommentiert und relativ übersichtlich, so dass keine weiteren Erklärungen notwendig sind. Für jeden logischen Programmabschnitt habe ich print()-Statements eingebaut, damit man sieht, was das Programm tut. Sehen wir uns noch kurz die Ausgaben an, wenn das komplette Programm ausgeführt wird (ich habe es in der Datei flockTest.pl abgelegt): D:\temp>flockTest.pl PID 1176: Datei gesperrt PID 1176: Ende D:\temp>

Nun wollen wir uns den eigentlich interessanten Teil des Programms ansehen, der in den Funktionen lockFile() und unlockFile() steht: 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

# Hilfsfunktion für Fehlerausgaben sub err { foreach my $arg ( @_ ) { STDERR->print( defined( $arg ) ? $arg : "undef" ); } STDERR->print( "\n" ); } # # # # # # # # # # # # # # # # {

Sowohl die Funktion lockFile() als auch unlockFile() greifen gemeinsam auf die Variable $lckFh zu, in der das FileHandle für die Sperrdatei abgelegt ist. Man könnte nun entweder eine globale Variable verwenden, die mit der Direktive our im Hauptprogramm deklariert wird, oder man nutzt das Perl-Feature, dass Variablen innerhalb von Programmblöcken, die von geschweiften Klammern umrahmt sind, so lange gültig sind wie der sie umgebende Block. Wenn nun zusätzlich zu den Variablen auch Funktionen im Programmblock definiert werden, dann behalten die so definierten Variablen ihren Wert und sind für die Funktionen global.

flock() 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128

769 # Variable, in welcher das FileHandle # für die Sperrdatei gespeichert ist. # Sie ist innerhalb des Programmblocks # global und verliert ihren Wert nicht. our $lckFh = undef; # Funktion zum Sperren einer Datei. # Der Clou dabei ist, dass nicht diejenige # Datei gesperrt wird, in der die Daten # verarbeitet werden, sondern eine # eigene Datei für die Sperre verwendet wird. # Damit ist es möglich, dass die Datendatei # z.B. kurzzeitig gelöscht werden kann, ohne # dass die Sperre aufgehoben wird. sub lockFile { my ( $path ) = @_; # Wenn bereits eine Dateisperre existiert, # tun wir nichts. if ( $lckFh ) { return 1; } # Der Dateiname für die Sperrdatei geht # aus dem Namen der Datendatei hervor, # an den die Endung .lck angehängt wird. my $lckPath = "$path.lck"; # Öffnen der Sperrdatei. Wenn sie noch nicht # existiert, wird sie neu angelegt. $lckFh = new FileHandle( $lckPath, "a+" ); unless ( $lckFh ) { err( "kann Datei $lckPath ", "nicht oeffnen" ); return undef; } # Wir versuchen maximal 10-mal, die Datei # zu sperren (ohne Blockierung). Nach jedem # erfolglosen Versuch legen wir uns # 3 Sekunden schlafen. my $status = undef; for ( my $i = 0; $i < 10; $i++ ) { if ( flock( $lckFh, LOCK_EX | LOCK_NB ) ) { $status = 1; last; }

770 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 } 167 168 1;

C

Vordefinierte Funktionen

sleep( 3 ); } # Wenn keine Dateisperre erzeugt werden # konnte, müssen wir die Sperrdatei # wieder schließen und die Variable # $lckFh auf undef setzen. unless ( $status ) { err( "kann Datei $lckPath ", "nicht sperren" ); $lckFh->close(); $lckFh = undef; return undef; } # Wir haben erfolgreich eine # Dateisperre erzeugt. return 1; } # Funktion für das Freigeben der Sperrdatei sub unlockFile { # Vorsichtsmaßnahme: Wenn gar keine # Sperre existiert, tun wir nichts. unless ( $lckFh ) { return; } # Sperre aufheben flock( $lckFh, LOCK_UN ); # Sperrdatei schließen $lckFh->close(); # Variable für das FileHandle # auf undef setzen $lckFh = undef; }

Um zu testen, ob bei einem gleichzeitigen Zugriff mehrerer Prozesse die Datenkonsistenz gewährleistet ist, öffnet man in Windows am besten mehrere DOS-Boxen parallel und startet in jeder einzelnen Box nacheinander dasselbe Skript. Unter UNIX kann man das Skript mehrfach im Hintergrund aufrufen, z.B. /home/user1% for i in 1 2 3 4 5 >do > flockTest.pl & >done /home/user1%

getc()

771

Je mehr Prozesse gestartet werden, desto schneller sind einige dabei, die keine Dateisperre erzeugen können, weil die maximale Anzahl von Wiederholungsversuchen erreicht ist. Anhand der unterschiedlichen Prozess-IDs können die Ausgaben unterschieden werden. Egal, wie viele Prozesse Sie starten, mit flock() ist in jedem Fall sichergestellt, dass immer nur ein einziger Prozess Zugriff auf die Daten hat.

C.22 getc() Syntax (Angaben in eckigen Klammern sind optional): getc([ fileHandle ])

Die Funktion getc() liefert das nächste Zeichen aus dem Eingabestrom (Input Stream), der durch fileHandle angegeben ist. Wird kein Argument übergeben, dann verwendet die Funktion STDIN als FileHandle. getc() liefert den Wert undef zurück, falls kein Zeichen mehr verfügbar ist oder ein

Fehler aufgetreten ist. Um festzustellen, ob man am Ende der Datei angelangt oder ob ein Fehler eingetreten ist, kann man entweder die vordefinierte Variable $! oder die Perl-Funktion eof() benutzen.

C.23 gmtime() Syntax (Angaben in eckigen Klammern sind optional): gmtime([ time ]) scalar( gmtime([ time ]) )

time ist optional und in Sekunden seit dem 1.1.1970 00:00:00 Uhr anzugeben. Falls nicht angegeben, wird die aktuelle Zeit mit der Perl-Funktion time() automatisch eingesetzt. gmtime() interpretiert die aktuelle oder die angegebene Zeit als GMT (Greenwich Mean Time) und liefert wie die Perl-Funktion localtime() im List-Kontext ein Array zurück. Das Array hat folgendes Format: ( $secs, $mins, $hours, $mday, $mon, $year, $wday, $yday, $isSum )

772 $secs: $mins: $hours: $mday: $mon: $year: $wday: $yday: $isSum:

C

Vordefinierte Funktionen

Sekunden (0-59) Minuten (0-59) Stunden (0-23) Tag des Monats (1-31) Monat (0-11, d.h. Januar = 0, Dezember = 11) Jahr (4-stelliges Jahr minus 1900, d.h. 2001 ist 101) Wochentag (0 = Sonntag, 6 = Samstag) Tag des Jahres (0-365) immer 0

Wird gmtime() im skalaren Kontext aufgerufen, dann liefert die Funktion das Datum und die Zeit als String zurück. Beispiele: print( gmtime(), "\n" ); # Liefert z.B. 1611515510261650, # da print() List-Kontext hat. # Die einzelnen Elemente des zurückgelieferten # Arrays werden ohne Zwischenraum getrennt # hintereinander von print() ausgegeben, der # etwas unverständliche Ausdruck bedeutet also # ( 16, 1, 15, 15, 5, 102, 6, 165, 0 ). my $dateTime = gmtime(); print( "$dateTime\n" ); # Liefert z.B. "Mon Dec 31 05:25:33 2001"

C.24 grep() Syntax: grep( expression, list )

expression ist ein Suchpattern, das auch reguläre Ausdrücke enthalten kann. list enthält eine Liste, auf welche das Suchpattern angewendet wird. grep() liefert im skalaren Kontext die Anzahl der Treffer von expression in list, im List-

Kontext eine Liste derjenigen Elemente aus list, bei denen ein Match erzeugt wurde. Beispiele: ... # Annahme: @lines enthält mehrere Zeilen, # z.B. aus der Datei "/etc/passwd". # List-Kontext:

grep()

773

my @noComments = grep( !/^#/, @lines ); # @noComments enthält alle Elemente von @lines, # die nicht mit # beginnen # Skalarer Kontext: my $lineCount = grep( /\n/s, @strings ); # $lineCount enthält die Anzahl von Elementen # in @strings, die ein "\n" enthalten

Wird grep() in einer Schleife mit List-Kontext verwendet und werden die einzelnen Elemente verändert, dann überschreibt man damit die Elemente der Originalliste! Beispiel für grep() im List-Kontext: D:\temp>perl -w use strict; my @arr = ( 1, 5, 12, 19, 20 ); foreach my $ele ( grep( /^1/, @arr ) ) { $ele = "hi"; } print( join( ", ", @arr ), "\n" ); ^Z hi, 5, hi, hi, 20 D:\temp>

Es werden alle Elemente der Liste, die mit dem Zeichen 1 beginnen, in hi geändert. Wie wir an der Ausgabe sehen, überschreibt man in der foreach-Schleife aber das Originalarray. So etwas sollte unbedingt vermieden werden. Besser ist: D:\temp>perl -w use strict; my @arr = ( 1, 5, 12, 19, 20 ); my @a1 = grep( /^1/, @arr ); foreach my $ele ( @a1 ) { $ele = "hi"; } print( join( ", ", @a1 ), "\n" ); ^Z hi, hi, hi D:\temp>

774

C

Vordefinierte Funktionen

Damit benötigt man zwar eine zusätzliche Variable, der Code ist aber erheblich verständlicher und damit leichter zu warten.

C.25 hex() Syntax: hex( expression )

Die Funktion hex() interpretiert expression als hexadezimalen Ausdruck und liefert den Zahlenwert zurück. Es sind nur Integerwerte als Argument erlaubt. Beispiel: print( hex( "aa" ), "\n" ); print( hex( "0xaa" ), "\n" ); # Beide Aufrufe geben aus: 170 # Vorsicht: hex( 20 ) # liefert dezimal 32. # Da aber die Zahl keine Sonderzeichen des # Hexadezimalsystems enthält, ist das nicht # sofort ersichtlich, denn der Mensch denkt # schließlich dezimal. # Noch einmal mehr Vorsicht: hex( 020 ) # liefert dezimal 22, nicht etwa 16.

Es dürfen keine Dual- oder Oktalzahlen angegeben werden, auch nicht in Quotes. Hier verhält sich hex() anders als die Funktion oct().

C.26 int() Syntax: int( expression )

Die Funktion int() liefert den ganzzahligen Anteil von expression.

int()

775

Sie sollte nicht zum Runden verwendet werden, weil sie bei manchen Gleitkommazahlen ein falsches Ergebnis liefert. Hierfür sollte stattdessen sprintf(), printf(), POSIX::ceil() oder POSIX::floor() verwendet werden. Häufig verwendet man int(), um aus einer Gleit- oder Festkommazahl durch Abschneiden der Nachkommastellen eine ganze Zahl zu machen, z.B. für das Erzeugen von ganzzahligen Zufallszahlen. Beispiel für Lottozahlen (Skript lotto.pl ): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

#!D:/Perl/bin/perl.exe -w use strict; my @arr = (); for ( my $i = 1; $i add() 694 Headerzeilen bei HTTP 420 hex() 44, 774, 783 Hexadezimalsystem 44 Hexcode 45 Hexcode im Querystring 414 Hexdump von Dateien 350 Hexzahl 44 Hintergrundprozess 511 Homepage von Perl 19, 21 host Attribut im URI 413 HTML Formulare 416 HTML Formulare und HTTP Methode 418 HTML Formulare verarbeiten 484

838

HTTP 416 http Internet Protokoll 413 HTTP_HOST Umgebungsvariable bei CGI 437 HTTP_USER_AGENT Umgebungsvariable bei CGI 437 HTTP-Aktionsfluss 416 httpd.conf 681, 682, 684, 686 HTTP-Header 418 HTTP-Header des Client lesen 690, 691 HTTP-Headerzeilen 420 HTTP-Methode GET 418, 689, 719 HTTP-Methode HEAD 419, 689 HTTP-Methode POST 419, 689, 719 HTTP-Protokoll Definition 416 HTTP-Redirect 423 HttpSession 574 HttpSession.pm 574, 586 HTTPS-Protokoll 413, 703 HTTP-Version Attribut im Request 420 HTTP-Version Attribut in der Response 423 Hyper Text Transfer Protocol 416 Hypertext-Links 375 Hypertext-Links ausgeben 362 I i Option bei Pattern Matching 205, 217, 233, 336 -I Schalter von Perl 726 I/O Subsystem 121 I/O-Operator 121 Identifier 35, 55, 56, 59, 61, 81 if Statement 133, 136 If-Modified-Since Request Header 421 IGNORE 742 image/gif 420 Implementierung einer Sessionverwaltung 574 implizit definierte Referenzen 763 Importieren von Modulen 166 Importieren von Modulen (Probleme mit dem Namespace) 173 IN 528 in 482 in Zeichenklassen 225

Index

Includevariablen in Templates 451 INDEX 523 Index 50, 517 Index (Datenbanken) 520 Index von Arrays 70 Indexdatei 377 Indexnummer 50 Indizes 49, 520 Inheritance 301 Initialisierung 47 Initialisierung mehrerer Variablen 68 Initialisierung von Array Variablen 70 Initialisierung von Arrays 72 Initialisierung von Hash Variablen 76 Initialisierung von Schleifenvariablen 137 Initiator von Cookies 428 inkrementieren 104 Inline Dokumentation 34, 35 Input Stream 771, 790 INSERT-Statement 510, 526 Installation von mod_perl 677 Installation Zusatzmodule 19 Installationsverzeichnis 18 Instanz 280, 283, 284 Instanzattribute 289 Instanzcode 287 Instanzierung 280 Instanzmethoden 289 Instanzmethoden aufrufen 291 INT (Datenbanken) 520 INT Signal 740 int() 75, 774, 790 INTEGER (Datenbanken) 520 integer Pseudomodul 822 Integerwerte 44 Integerzahl 43, 44 Integration von Perl und Apache 673 Internal Server Error Status bei HTTP 424 internal_redirect() 695 internal_redirect_handler() 695 Internet-Protokoll 413, 416 Interpretation von Sonderzeichen 45 Interpreter 16, 22, 23, 24, 28, 31 IO 164, 187, 188, 797

Index

is_initial_req() 689 IS_NOT_NULL 528 IS_NULL 528 ISO-LATIN-1 750, 783 J Java 15 join() 73, 776, 805, 813 Joins 530, 532 K Kapselung 279 Kennzeichen für skalare Variable 46 Kennzeichner 56 Kennzeichnung von Strings 45 Kernel 121 Key 54, 76 Key/Value Paare 54 Key/Value Paare im Querystring 414 Keys 55 Keys als Barewords 55 keys() 78, 757, 777 keys() als lvalue Funktion 777 Kind-Klasse 301, 302 Klammernpärchen bei Pattern Matching 215 Klartext Übertragung von Logindaten 428 Klasse 280, 282 Klassenattribute 282 Klassenmethoden 282, 283 Kleinbuchstaben 36 kleinstmöglicher Geltungsbereich 68 Kombinationen mit dem Zuweisungsoperator 103 Komma 44, 50 Kommandozeile 21, 31 Kommandozeilen-Interpreter 22, 24 Kommentare in Perl 27, 34 Komplexe Datentypen 261 komplexe Statements 132 Konstante als Array Referenz 57 Konstanten 56 Konstanten für das Setzen der Dateizeigerposition 187 Konstruktor 176, 283, 746

839

Konstruktor der Klasse Template 452 Konstruktor ohne Argumente 310 Kontext 51 Kontext abfragen mit wantarray() 162 Kontext einer Funktion 162 Kopieren von Dateien mit File::Copy 337 Kourne-Shell 17, 19 Kreiszahl PI 744 Kreuzprodukt 530, 532, 534 kryptografische Anwendungen 809 Kurzeinführung in SQL 510 Kurzformen häufig benutzter Zeichenklassen 228 L Label 136 Labels 142 Labels bei geschachtelten Schleifen 142 Laden von Apache Perl Modulen 682 Laden von Perl Modulen 165, 168 Länge von Variablennamen 59 last 141 Last-Modified Response Header 425 Laufzeit 33, 37, 167, 168 Laufzeit Fehlermeldung 33 Laufzeitfehler 24 lc() 778 lcfirst() 778 le Operator 115 leere Arrays prüfen 71 leere Liste (Definition) 49 Leere Liste in der use Direktive 170 leerer Konstruktor 310 leerer String 48 Leerzeichen 36 Leerzeichen in Zeichenklassen 225 Leerzeile in HTTP Headern 422 Leerzeilen 25 length() 778 Lesbarkeit 25 Lesen aller Cookie Namen 497 Lesen aus Dateien 184 Lesen von Cookies mit CGI.pm 497 Lesen von Textdateien 184 Lesen von Verzeichnissen 192 Leserechte 401 Lesezeichen 412

840

lexikalische Sortierung 74 lexikalische Sortierung von Zahlen 271 lexikalischer Vergleichsoperator 116 lib 30 LIKE 528, 529 Linktabelle 519 Linux 16, 32 Liste von Hash Keys 78 Liste von Hash Values 79 Listen 43, 48, 49, 50, 51, 53 Listen und Arrays 57 Listen und return 144 List-Konstanten 56 List-Kontext 51, 52, 53, 68, 69, 121, 160, 162 List-Operator 68, 69, 70 List-Operator bei Hashes 76 literale Zeichen 45, 46 localtime() 52, 53, 771, 779, 814 localtime() (List Kontext) 52 localtime() (skalarer Kontext) 52 Location Response Header 425 LOCK_EX 766, 821 LOCK_NB 766, 821 LOCK_SH 766, 821 LOCK_UN 766, 821 Löschen eines Cookie 499 Löschen von Dateiinhalten 189 Löschen von Hash-Elementen 77 Löschen von Verzeichnissen 191, 325 log() 780 Logarithmus zur Basis 10 780 Logarithmus zur Basis e 780 Logfile des Webservers (Fehlermeldungen bei CGI) 440 Logische Operatoren 105 look-ahead assertion 254 look-behind assertion 255 Loops 449 Loopvariablen in Templates 449, 450 Loopvariablen in Templates ersetzen 463 lstat() 780 lt-Operator 114 LVALUE 792 lvalue-Funktion 212, 777, 785

Index

M Macintosh 46 mailto-Protokoll 413 main 23, 41, 65, 165 manuelle Installation Zusatzmodule 19 Maschinencode 16, 21, 22, 23 Match 197, 202 Matching 197 Matching-Operator m 129, 200 Matching-Optionen 205 Math 45, 751, 801, 809 max-age-Attribut bei Set-Cookie2 435 maximale Länge von Variablennamen 59 Mehrdimensionale Arrays 91, 261 Mehrdimensionale Hashes 266 mehrere Bereiche in Zeichenklassen 227 mehrfach importierte Funktionen 173 mehrfach importierte Variablen 173 Mehrfach Vererbung 304 Mehrsprachige Datensätze 669 mehrzeilige Kommentare 27 META-Daten bei HTTP 419 META-Informationen 418 Metazeichen 225, 239 Metazeichen bei PatternMatching 203, 221 Metazeichen entwerten 203, 221 Metazeichen in Zeichenklassen 227 Method Not Allowed Status bei HTTP 424 method() 689 Method-Attribut des form Tags 443, 444 Method-Attribut im Request 418 Methoden 279, 282 MIME-TYPEs 420, 423, 426 Mindestversion des Perl Interpreters 169 Mindestversion von Perl Modulen 169 Minimal Matching 229, 232, 236, 239, 241, 365, 456 mkdir() 191, 780 mkpath() 191, 325 mod_perl 16, 439, 677 mod_perl Installation 677 mod_perl UNIX Installation 680 mod_perl Windows Installation 677 mod_ssl 677 Modul 16, 23, 27, 42

Index

Modul File 325, 328 Module 164 Module finden 30 Module importieren 166, 170 Module laden 165, 168 Module logisch zusammenfassen 164 Modulname 28 modulo Division 101 monoton aufsteigende Bereiche 123 m-Operator 129, 200, 201 m-Option bei Pattern Matching 206 Moved Permanently Status bei HTTP 424 mozilla 411 ms Option bei Pattern Matching 211 MSI Installer 17 Multiple Index 520, 523 Multi-Threading 298 Muster 198 my 61, 64, 68 Mysql 507 mysql Programm 511 Mysql-Client 511 N n:1-Relation 518 n:m-Relation 518 Nachbearbeitungsteil nicht ausführen 144 name-Attribut bei Set-Cookie 431 name-Attribut bei Set-Cookie2 434 Namenskonvention bei Getter Methoden 291 Namenskonvention bei Setter Methoden 292 Namenskonvention für Getter Methoden bei bool'schen Attributen 293 Namenskonvention für Methoden 291 Namenskonvention für Setter Methoden bei booleschen Attributen 293 Namenskonventionen 35 Namenskonventionen für Variablen 59 Namespace 65, 165, 169 Namespace bei der require Direktive 167 Namespace erweitern mit der use Direktive 170

841

Namespace verändern mit der use Direktive 170 natürlicher Logarithmus 780 n-dimensionale Arrays 94 ne Operator 113 Nebeneffekte und $_ 202 Negation 107 Negation des Binding Operators 203 Negierte Zeichenklassen 227 Netscape Cookies 430 Netzwerkverbindungen 121 neue Array Elemente hinzufügen 72 new 283 new() 746 news Internet Protokoll 413 next 141 nicht initialisierte Daten 67 nicht initialisierte Variablen 47 NICHT Verknüpfung 105 no Direktive 42 no warnings 761 no_chdir Attribut bei find() 332, 336 Not Found-Status bei HTTP 424 Not Implemented-Status bei HTTP 424 Not Modified-Status bei HTTP 424 NOT NULL 521 not-Operator 107 Notwendigkeit von Cookies 427 NULL 47, 520, 525, 553 NUMBER (Datenbanken) 520 Numerische Keys (Datenbanken) 524 numerische Sortierung 74, 111 numerischer Vergleichsoperator 111 O O_APPEND 178 O_CREAT 177, 179 O_RDONLY 178 O_RDWR 179 O_WRONLY 178 Objektinstanz 280 Objektmethoden (zusätzlicher unsichtbarer Parameter) 285 Objektmethoden aufrufen mit -> 119 Objektorientierte Programmierung 16, 279

842

Objektreferenz 284, 287 Objektreferenz (unsichtbares Argument bei Methoden) 291 oct() 44, 774, 782 ODER-Verknüpfung 105 ODER-Verknüpfung (bitweise) 118 ODER-Verknüpfung bei Patterns 216, 223 ODER-Verknüpfung bei Zeichenklassen 225 Öffnen eines Verzeichnisses 191 Öffnen von Dateien 176 Öffnen von Dateien (Beispiele) 178 Offset in Dateien 177, 186 Offsets der Treffer bei Pattern Matching 219 Offsets in Zeichenketten 213 OK Status bei HTTP 423 Oktalsystem 44 Oktalzahl 44 Online Dokumentation 21 Online Hilfe 20, 21 OOP 279 open() 176 Operanden 42, 96 Operator 42, 56, 104, 125, 127, 788 Operator qw 57 Operator und Funktion 98 Operatoren 96 Optionen bei Pattern Matching 205 OR (SQL) 528 or Operator 106 Oracle 507 Oracle-Client 514, 522 ord() 352, 783 our 62, 66, 283 Overloading 307, 636 Overriding 309 P pack() 783, 819 Package 42, 282, 400 package 28, 65, 164 package Direktive 42, 165, 282 Package Scope 283 Packagename 28, 36 Parent 301 Parent Klasse 301, 302

Index

Parse Phase 33 parsen 37 PATH 18, 19, 31 path-Attribut bei Set-Cookie 431 path-Attribut bei Set-Cookie2 434 path-Attribut im URI 414 Pattern 198 Pattern Matching 197 Pattern Matching Trennzeichen 201 Pattern Substitution 241 Pattern verknüpfen 223 Pearl 15 Performance 45 Perl 15 Perl Apache-Integration 673, 680 Perl Distribution 17 Perl Online Hilfe 20, 21 Perl Zusatzmodule 19 perl.exe 17 PerlAddVar 692 PerlAuthenHandler 685, 686 Perl-Bibliotheken 18 Perl-Distribution 16, 17, 18, 19, 20, 21, 30 perldoc 20, 21, 35 Perl-Grundlagen 41 Perl-Homepage 19, 21 Perl-Installation 16, 32 Perl-Interpreter 16, 17, 18, 31 PERLLIB 31, 38, 164, 726, 727, 792 Perl-Module 19, 27, 28, 30 perlpod 35 PerlRequire 682 PerlSetVar 685, 686, 692, 709, 717 Perl-Skript 21, 26 Perl-Skripts prüfen 34 persistente Datenbankverbindung 707, 708, 709 persistente Datenbankverbindungen mit mod_perl 677, 682 persistente HTTP Daten mit Cookies 429 persistente Speicherung von Clientdaten mit CGI.pm 483 Pfadname bei find() 329 Pfeil Regel für mehrdimensionale Arrays 93 Pflichtattribute von Objekten 311 PGP Modul 751

Index

pi 744, 751, 801 Pipe 121, 131, 175, 194 Pipe (HTTP Methode POST) 444 plain old documentation 35 Plattform-Unabhängigkeit 16 Platzhalter 545, 546 pod 35 Pointer Variablen 80 pop() 72, 785 Port Definition 413 Portabilität 49 port-Attribut bei Set-Cookie2 435 port-Attribut im URI 413 pos() 219, 242, 456, 785 pos() (Trefferposition bei Pattern Matching) 212 pos() als lvalue Funktion 785 Position des Dateizeigers 186 Positionieren des Dateizeigers (Beispiele) 187 Positionsvariablen bei Pattern Matching 219 POSIX 75, 775 POST HTTP-Methode 419, 689, 719 PostGRES 507 postprocess Attribut bei find() 332 ppm.bat 20, 707 Präzision 44 prepare()-Methode 543, 544 preprocess Attribut bei find() 331, 336 Primary Key 517, 524 print() 52, 786, 788 print() (Apache Methode) 694 print() (built-in Perl Funktion) 183 print() (FileHandle Methode) 183 print() Kontext 52 print() und