178 85 5MB
German Pages 852 Year 2002
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:
Name | Beschreibung | Status | max. Anzahl Mitglieder | aktuelle Anzahl Mitglieder |
---|---|---|---|---|
$$(name) | $$(desc) | $$(status) | $$(mmc) | $$(mc) |
Name | |
---|---|
Beschreibung | $$(gdesc) |
Status | 67 freigeschaltet 68 70 gesperrt | 71
max. Anzahl Mitglieder | 7476 |
81 |