Copyright © 1995 by O'Reilly/International Thomson Verlag

Bitte denken Sie daran: Sie dürfen zwar die Online-Version ausdrucken, aber diesen Druck nicht fotokopieren oder verkaufen.

Wünschen Sie mehr Informationen zu der gedruckten Version des Buches "Linux - Wegweiser zur Installation & Konfiguration", dann klicken Sie hier.


Kapitel 6

Perl

Perl ist möglicherweise das Beste, was sich seit Jahren für die Programmierung unter UNIX ergeben hat. Allein dafür lohnt es sich, Linux anzuschaffen. Perl ist eine Sprache zur Bearbeitung von Texten und Dateien, die ursprünglich dazu gedacht war, große Textmengen einzulesen, zu verarbeiten, und aus den Daten schließlich ansprechend formatierte Berichte zu erstellen. Im Laufe der Zeit hat sich Perl allerdings zu einer vielseitig verwendbaren Skript-Sprache entwickelt, die sowohl Prozesse verwalten als auch mittels TCP/IP über ein Netzwerk kommunizieren kann. Perl wurde als freie Software von Larry Wall entwickelt, dem UNIX-Guru, von dem auch der News-Reader rn und verschiedene beliebte Tools wie patch stammen.

Die wahre Stärke von Perl liegt darin, daß es die am häufigsten genutzten Eigenschaften solcher Sprachen wie C, sed , awk sowie einiger Shells zu einer einzigen, interpretierenden Skript-Sprache zusammenfaßt. Bisher mußte man die verschiedenen Sprachen in geschickter Weise miteinander kombinieren, um komplizierte Dinge zu erledigen. Oft bedeutete das, daß man die Ausgaben von sed -Skripts in einer Pipe an awk -Skripts weiterreichte, die wiederum ihre Ergebnisse an Shell-Skripts übergaben, die schließlich eine Pipe in ein C-Programm füllten. Perl macht Schluß mit der in UNIX-Kreisen weitverbreiteten Philosophie, daß man mit kleinen Tools kleine Teile eines großen Problems lösen sollte. Statt dessen erledigt Perl alles in einem Durchgang -- und es bietet viele verschiedene Methoden, eine Aufgabe zu lösen. Sogar dieses Kapitel wurde von einem KI-Programm geschrieben, das wir in Perl entwickelt haben. (War nur ein Scherz, Larry.)

Perl bietet den einfachen Zugriff auf viele Merkmale, die man in anderen Sprachen manchmal nur schwer nutzen konnte. Ein Beispiel: Viele Skripts, die bei der Verwaltung von UNIX-Systemen benutzt werden, lesen große Mengen von Text, schneiden bestimmte Felder aus jeder Zeile aus (anhand eines Musters, das in der Regel ein regulärer Ausdruck ist), und erzeugen aus diesen Daten einen Bericht. Nehmen wir an, daß Sie die Ausgaben des UNIX-Befehls last verarbeiten wollen, der für alle Benutzer des Systems eine Übersicht über die Login-Zeiten erstellt, etwa so:

mdw       ttypf    loomer.vpizza.co Sun Jan 16 15:30 - 15:54  (00:23)
larry     ttyp1    muadib.oit.unc.e Sun Jan 16 15:11 - 15:12  (00:00)
mkjohnson ttyp4    mallard.vpizza.c Sun Jan 16 14:34 - 14:37  (00:03)
jem       ttyq2    mallard.vpizza.c Sun Jan 16 13:55 - 13:59  (00:03)
linus     ftp      kruuna.helsinki. Sun Jan 16 13:51 - 13:51  (00:00)
linus     ftp      kruuna.helsinki. Sun Jan 16 13:47 - 13:47  (00:00)

Wenn wir daraus die gesamte Login-Zeit für jeden Benutzer errechnen wollten (die in Klammern im letzten Feld steht), könnten wir dazu ein sed -Skript schreiben, das die Zeitangabe aus jeder Zeile extrahiert; könnten dann mit einem awk -Skript die Daten nach Benutzern sortieren und die Zeiten für jeden Benutzer aufaddieren; und könnten schließlich mit einem weiteren awk -Skript aus den akkumulierten Zeiten einen Bericht erstellen. Eine andere Möglichkeit wäre es, ein ziemlich komplexes C-Programm zu schreiben, das die gesamte Verarbeitung erledigt. Jeder C-Programmierer weiß, daß es ein komplexes Programm werden würde, weil C über keine allzu bequemen Funktionen zur Bearbeitung von Texten verfügt.

Eine solche Aufgabenstellung kann ohne Probleme von einem einfachen Perl-Skript bewältigt werden. Perl bietet einfachen Zugriff auf die Ein- und Ausgabe sowie die Suche mit regulären Ausdrücken, auf das Sortieren nach verschiedensten Kriterien sowie auf umfangreiche Zahlenspielereien. Perl-Programme sind in der Regel kurz und effizient und ohne technischen Hokuspokus, der Ihr Programm davon abhält, tatsächlich etwas zu tun .

Perl unter Linux ist nicht anders als Perl unter irgendeinem anderen UNIX-System. Es gibt bereits mehrere gute Bücher zum Thema, darunter Programming Perl von Larry Wall und Randal L. Schwartz sowie Einführung in Perl von Randal L. Schwartz; beide sind im Verlag O'Reilly & Associates erschienen. Wir finden Perl trotzdem so faszinierend, daß wir wenigstens eine Einführung geben wollen. Schließlich ist Perl freie Software wie Linux -- die beiden passen zueinander.

Ein Programmbeispiel

Was uns an Perl ganz besonders gefällt, ist die Tatsache, daß Sie sofort mit der Problemlösung beginnen können -- Sie müssen nicht erst ausführlich Datenstrukturen anlegen, Dateien oder Pipes öffnen, Speicher für Daten freimachen usw. Alle diese Aufgaben werden Ihnen freundlicherweise abgenommen.

Wir werden anhand des Beispiels, das wir etwas weiter oben erwähnt haben, einige der grundlegenden Eigenschaften von Perl vorstellen. Dazu drucken wir zuerst das komplette Skript (mit Kommentaren) ab und erläutern dann, was darin passiert. Dieses Skript liest die Ausgabe des Befehls last (siehe auch das Beispiel weiter oben) und gibt für jeden Benutzer des Systems die Anzahl der Logins und die gesamte Login-Zeit aus. (Zur Vereinfachung haben wir die Zeilen durchnumeriert.)

1       #!/usr/local/bin/perl
2
3       while (<STDIN>) {   # solange Eingaben vorhanden sind, ... 
4         # Zeilen lesen und Name, Login-Zeit merken
5         if (/^(\S*)\s*.*\((.*):(.*)\)$/) { 
6           # Stunden, Minuten und Logins aufaddieren
7           $hours{$1} += $2;
8           $minutes{$1} += $3;
9           $logins{$1}++;
10        }
11      }
12
13      # Für jeden Benutzer ...      
14      foreach $user (sort(keys %hours)) {
15         # aus den Minuten die Stunden errechnen
16         $hours{$user} += int($minutes{$user} / 60);
17         $minutes{$user} %= 60;
18         # Ergebnis für diesen Benutzer ausgeben
19         print "User $user, total login time ";
20         # auch Perl kennt printf
21         printf "%02d:%02d, ", $hours{$user}, $minutes{$user};
22         print "total logins $logins{$user}.\n";
23      }

Zeile 1 teilt dem Programm-Lader mit, daß dies ein Perl-Skript und kein Shell-Skript ist. In Zeile 3 beginnt das Programm. Hier steht der Anfang einer einfachen while-Schleife, wie sie C- und Shell-Programmierern vertraut sein dürfte. Der Code zwischen den Klammern in den Zeilen 4 und 11 soll so lange ausgeführt werden, wie eine bestimmte Bedingung erfüllt ist. Die Bedingung »<STDIN>« sieht allerdings merkwürdig aus. Tatsächlich ist diese Bedingung erfüllt, solange Eingaben vorhanden sind -- in diesem Fall von der Standardeingabe, wie Sie vielleicht schon erraten haben.

Perl liest seine Eingaben zeilenweise (wenn Sie nichts anderes bestimmt haben). Voreingestellt ist auch, daß es von der Standardeingabe liest -- auch hier gilt: solange Sie nichts anderes festlegen. Diese while-Schleife wird also solange zeilenweise die Standardeingabe lesen, bis dort keine Daten mehr vorliegen.

Das furchtbare Durcheinander in Zeile 5 ist nur eine if-Anweisung. Wie bei den meisten Programmiersprachen wird auch hier der Code innerhalb der geschweiften Klammern (Zeilen 6 bis 9) ausgeführt, wenn der Ausdruck hinter dem if wahr ist. Aber was bedeutet dieser Ausdruck? »/^(\S*)\s*.*\((.*):(.*)\)$/«? Wer sich mit den UNIX-Tools grep und sed auskennt, wird dies sofort als regulären Ausdruck erkennen -- eine kryptische, aber nützliche Methode, ein Muster zu definieren, nach dem in der Eingabe gesucht werden soll. Reguläre Ausdrücke werden in der Regel von Schrägstrichen begrenzt (/.../).

Dieser spezielle Ausdruck paßt auf Zeilen mit folgendem Format:

mdw       ttypf    loomer.vpizza.co Sun Jan 16 15:30 - 15:54  (00:23)

Dieser Ausdruck »merkt« sich außerdem den Benutzernamen (mdw) und die Login-Zeit aus diesem Eintrag (00:23). Machen Sie sich keine Gedanken über den Ausdruck selbst -- die Formulierung von regulären Ausdrücken ist ein schwieriges Thema. Im Augenblick müssen Sie nur wissen, daß diese if-Anweisung Zeilen im o.a. Format findet und daß sie daraus zwecks Weiterverarbeitung den Benutzernamen und die Login-Zeit extrahiert. Der Benutzername wird der Variablen $1 zugewiesen, die Stunden gehen in $2 und die Minuten in $3. (Variablen werden in Perl mit einem $ versehen, aber anders als in der Shell muß das $ auch bei einer Zuweisung an die Variable benutzt werden.) Diese Zuweisung erfolgt dadurch, daß eine passende Zeile gefunden wird (alles, was im regulären Ausdruck zwischen runden Klammern steht, wird zur späteren Verarbeitung in einer der Variablen $1 bis $9 gespeichert.)

In den Zeilen 7 bis 9 werden diese Informationen dann verarbeitet. Dies geschieht auf eine interessante Weise: unter Benutzung eines assoziativen Arrays . Während einfache Arrays mit einer angehängten Zahl indiziert werden, wird ein assoziatives Array durch einen beliebigen String indiziert. Damit stehen Ihnen viele und umfangreiche Möglichkeiten offen -- Sie können einen Satz von Daten mit anderen Daten verknüpfen, die gerade erst gewonnen wurden. In unserem kurzen Programm dienen die Benutzernamen, die wir aus der Ausgabe von last gewonnen haben, als Indexschlüssel. Wir arbeiten mit drei assoziativen Arrays, die über den Benutzernamen indiziert sind: hours, in dem die vollen Stunden registriert werden, während derer ein Benutzer eingeloggt war; minutes, das die Minuten enthält; und logins, in dem die Anzahl der Logins gespeichert wird.

Wenn wir z.B. die Variable $hours{'mdw'} abfragen, erhalten wir die Gesamtzahl der Stunden, in denen der Benutzer mdw eingeloggt war. Dasselbe Ergebnis erhalten wir, wenn in der Variablen $1 der Benutzername mdw gespeichert ist und wir $hours{$1} abfragen.

In den Zeilen 7 bis 9 werden die Werte in diesen Arrays erhöht, und zwar entsprechend den Daten in der aktuellen Eingabezeile. Eine Eingabezeile wie:

jem       ttyq2    mallard.vpizza.c Sun Jan 16 13:55 - 13:59  (00:03)

bewirkt, daß in Zeile 7 der Wert des Arrays hours mit dem Index $1 (der Benutzername, also jem) um die Anzahl an Stunden erhöht wird, die jem eingeloggt war (der Wert steht in der Variablen $2). Perl benutzt ebenso wie C den Operator +=, um einen Wert zu erhöhen. In Zeile 8 wird in ähnlicher Weise der Wert von minutes für diesen Benutzer erhöht. Zeile 9 erhöht mit dem Operator ++ den Wert im Array logins um eins.

Assoziative Arrays gehören zu den mächtigsten Merkmalen von Perl. Sie können damit komplexe Datenbestände aufbauen, indem Sie die Indizes aus dem gerade durchsuchten Text gewinnen. Es ist fast unmöglich, diese Aufgabe mit einfachen Arrays zu bewältigen -- wir müßten zuerst in den Eingabedaten die Anzahl der Benutzer feststellen, dann ein entsprechend großes Array anlegen und jedem Benutzer eine Position in diesem Array zuweisen (mittels einer Hash-Funktion oder einer anderen Methode der Indizierung). Ein assoziatives Array können Sie dagegen direkt unter Benutzung von Strings indizieren, und Sie brauchen sich nicht um die Größe des Arrays zu kümmern. (Natürlich stellt sich bei der Arbeit mit großen Arrays immer die Frage nach der Verarbeitungsgeschwindigkeit, aber in den meisten Fällen wird daraus kein Problem entstehen.)

Lassen Sie uns weitermachen. In Zeile 14 taucht die Perl-Anweisung foreach auf, die Sie vielleicht aus Shell-Skripts kennen. (Die foreach-Schleife ist eigentlich eine for-Schleife, ähnlich der in C.) In unserem Beispiel wird bei jedem Schleifendurchlauf der Variablen $user der nächste Wert aus der mit sort(keys %hours) erzeugten Liste zugewiesen. %hours bezeichnet einfach das ganze assoziative Array hours, das wir angelegt haben. Die Funktion keys liefert eine Liste aller Indexwerte zurück, die wir zur Indizierung des Arrays benutzt haben -- in diesem Beispiel also eine Liste aller Benutzer. Die Funktion sort schließlich sortiert die von keys erzeugte Liste. Das Ergebnis des Ganzen ist, daß wir in einer Schleife die sortierte Liste aller Benutzernamen durchlaufen und den jeweiligen Namen der Variablen $user zuweisen.

In den Zeilen 16 und 17 wird der Fall behandelt, daß die Anzahl der Minuten 60 übersteigt -- aus der Gesamtzahl der Minuten werden die vollen Stunden berechnet und der Stunden-Wert für diesen Benutzer wird entsprechend angepaßt. Die Funktion int liefert den ganzzahligen Anteil einer Zahl zurück. (Ja, Perl kann auch mit Kommazahlen rechnen -- deshalb müssen wir hier int benutzen.)

Zum Abschluß werden in den Zeilen 19 bis 22 für jeden Benutzer die Gesamt-Loginzeit und die Anzahl der Logins ausgegeben. Die einfache print-Anweisung gibt nur ihre Argumente aus -- genau wie die awk -Funktion gleichen Namens. Beachten Sie, daß innerhalb einer print-Anweisung Variablen benutzt werden können -- wie in den Zeilen 19 und 22. Wenn Sie allerdings auf ausgefeilte Textformatierung Wert legen, sollten Sie die Funktion printf benutzen (die genau wie ihr Gegenstück in C funktioniert). In diesem Beispiel haben wir die Felder zur Ausgabe der Werte für hours und minutes zweistellig gemacht; Leerstellen sollen mit führenden Nullen gefüllt werden. Die printf-Anweisung in Zeile 21 erledigt das.

Wenn Sie dieses Skript in der Datei logintime abspeichern, können Sie es folgendermaßen aufrufen:

papaya$ last | logintime 
User johnsonm, total login time 01:07, total logins 11.
User kibo, total login time 00:42, total logins 3.
User linus, total login time 98:50, total logins 208.
User mdw, total login time 153:03, total logins 290.
papaya$

Natürlich kann dieses Beispiel keinen Perl-Kursus ersetzen, aber Sie haben sicherlich eine Vorstellung davon bekommen, was Perl leisten kann. Wir empfehlen Ihnen, eines der vorhandenen hervorragenden Perl-Bücher zu lesen, wenn Sie mehr lernen möchten.

Weitere Merkmale

Mit diesem Beispiel haben wir die am häufigsten benutzten Eigenschaften von Perl in einem richtigen, lebendigen Programm vorgestellt. Es gibt noch viel mehr Möglichkeiten -- sowohl bekannte als auch weniger bekannte.

Wir haben bereits erwähnt, daß Perl in der Lage ist, Berichte zu erstellen, die über die Möglichkeiten der Standardfunktionen print und printf hinausgehen. Mit der Berichte-Funktion definiert der Programmierer ein Bericht-»Format«, in dem das Aussehen der einzelnen Seiten des Berichts beschrieben wird. Wir hätten z.B. folgende Format-Definition in unserem Beispiel verwenden können:

format STDOUT_TOP =
User           Total login time     Total logins
-------------- -------------------- -------------------
.
format STDOUT =
@<<<<<<<<<<<<< @<<<<<<<<            @#### 
$user,         $thetime,            $logins{$user}
.

Die Definition STDOUT_TOP beschreibt die Kopfzeile, die auf jeder Seite des Berichts erscheinen soll. STDOUT legt das Aussehen der einzelnen Druckzeilen fest. Jede Feldbeschreibung wird mit einem @-Zeichen eingeleitet; @<<<< erzeugt ein linksbündiges Feld, @#### ein numerisches Feld. In der Zeile unterhalb der Felddefinitionen stehen die Namen der Variablen, die dort gedruckt werden sollen. In unserem Beispiel nimmt die Variable $thetime die formatierte Zeitangabe auf.

Wenn wir einen Ausdruck in diesem Format erzeugen wollen, ersetzen wir im Skript die Zeilen 19 bis 22 durch:

$thetime = sprintf("%02d:%02d", $hours{$user}, $minutes{$user});
write;

In der ersten Zeile formatieren wir mit der Funktion sprintf die Zeitangabe und speichern sie in der Variablen $thetime; die zweite Zeile enthält die Anweisung write, die Perl eine Druckzeile mit dem vorgegebenen Format ausgeben läßt.

Mit diesem Bericht-Format sieht der Ausdruck etwa folgendermaßen aus:

User           Total login time     Total logins
-------------- -------------------- -------------------
johnsonm       01:07                   11
kibo           00:42                    3
linus          98:50                  208
mdw            153:03                 290

Mit anderen Bericht-Formaten lassen sich andere (und attraktivere) Ergebnisse erzielen.

Ein weiteres seltsames Merkmal von Perl ist die Fähigkeit, (mehr oder weniger) direkt auf verschiedene Systemaufrufe von UNIX einschließlich der Interprocess Communication (IPC) zuzugreifen. So enthält Perl z.B. die Funktionen msgctl, msgget, msgsnd und msgrcv aus der IPC von System V. Perl unterstützt außerdem die Socket-Implementierung von BSD, so daß aus einem Perl-Programm heraus direkte Kommunikation via TCP/IP möglich ist. C ist also nicht mehr die einzige Sprache, wenn es um Netzwerkdämonen und -Clients geht. Ein Perl-Programm mit IPC-Fähigkeiten kann in der Tat äußerst nützlich sein -- insbesondere wenn man bedenkt, daß viele Client/Server-Implementierungen genau die fortschrittlichen Textverarbeitungsfunkionen brauchen, die Perl zur Verfügung stellt. Es ist in der Regel einfacher, Protokollbefehle zu analysieren, die von einem Perl-Skript zwischen Client und Server übertragen werden, als für diesen Zweck ein komplexes C-Programm zu schreiben.

Betrachten sie z.B. den bekannten SMTP-Dämon, der das Senden und Empfangen von elektronischer Post erledigt. Das SMTP-Protokoll benutzt für die Kommunikation zwischen Client und Server interne Befehle wie »recv from« und »mail to«. Sowohl der Client als auch der Server als auch beide können in Perl geschrieben werden; sie können dabei Perls Fähigkeiten zur Bearbeitung von Texten und Dateien ebenso in vollem Umfang nutzen wie die lebenswichtigen Socket-Funktionen.

Man hört, daß Larry Wall vorhatte, den News-Reader rn in Perl komplett neu zu schreiben, um zu demonstrieren, was sich im Extremfall mit Perl und IPC alles anstellen läßt.

Die Vor- und Nachteile

Eine der Eigenschaften von Perl (manche würden sagen, eines seiner »Probleme«) ist die Fähigkeit, den Code extrem kurz und knapp -- bis zur Unlesbarkeit -- zu formulieren. In unserem Beispiel haben wir einige der üblichen Abkürzungen benutzt. So werden Eingaben für Perl z.B. in die Variable $_ eingelesen. Da sich die meisten Operationen aber per Voreinstellung sowieso auf die Variable $_ beziehen, muß diese in der Regel nicht ausdrücklich angeführt werden.

Perl bietet Ihnen verschiedene Wege, ein Ziel zu erreichen; je nach Blickwinkel kann das ein Vor- oder ein Nachteil sein. Larry Wall zeigt in Programming Perl ein Beispiel für ein kurzes Programm, das einfach die Standardeingabe wieder ausgibt. Die folgenden Programmzeilen sind alle gleichwertig:

while ($_ = <STDIN>) { print; }
while (<STDIN>) { print; }
for (;<STDIN>;) { print; }
print while $_ = <STDIN>;
print while <STDIN>;

Es steht dem Programmierer frei, sich für die Syntax zu entscheiden, die der jeweiligen Situation angemessen ist.

Perl ist sehr beliebt geworden -- und das nicht nur, weil es so nützlich ist. Ein Hauch von Exzentrizität umgibt Perl und macht es sozusagen zu einem Spielzeug für den Hacker. Perl-Programmierer überbieten sich ständig gegenseitig mit immer trickreicherem Code. Perl fordert zu interessanten Codiertricks, sauberen Hacks und sowohl äußerst guter als auch extrem schlechter Programmierung heraus. UNIX-Programmierer betrachten Perl als eine Herausforderung -- weil Perl noch relativ neu ist, sind noch nicht alle seine Möglichkeiten ausgereizt worden. Selbst wenn Perl Ihnen zu altbacken vorkommt, muß man ihm doch gewisse Kunstfertigkeiten zugestehen. Innerhalb der UNIX-Gemeinde darf man stolz sein, wenn man sich als »Perl-Hacker« bezeichnen kann.


Inhaltsverzeichnis Vorherige Abschnitt Nächste Abschnitt