freeX: Programmierung mit Perl, Teil 2

Die ausschließlichen Vertriebsrechte an diesem Artikel liegen beim Computer- & Literaturverlag (C&L). Der Artikel darf nicht kopiert oder gar erneut in einer Zeitschrift oder einem Buch veröffentlicht werden ohne vorherige Erlaubnis von C&L. Der Verlag gestattet freundlicherweise die Veröffentlichung auf diesen Seiten. Wer öfter auf diesen Hinweis trifft, sollte sich überlegen, die Zeitschrift freeX zu abonnieren.

Perl hier, Perl da, Perl ist immer da!

 
Perl gehört zu Unix wie die Milch zu den Cornflakes. Obwohl die Sprache nicht bei allen Derivaten von BSD und kommerziellen Unix-Systemen zum direkten Lieferumfang gehört, ist sie dennoch die am weitesten verbreitete Skript-Sprache auf Unix-artigen Systemen.

 

1. Was tun? - Dokumentation
2. Dateioperationen
3. Arrays und Hashes
4. Stringverarbeitung
5. Beispiel: Finger
6. Schreiben in Dateien

In der letzten Ausgabe der freeX wurde die Sprache vorgestellt. In dieser Ausgabe sollen die Kenntnisse angewendet und erweitert werden. Dazu werden verschiedene Beispielprogramme entwickelt und ausgebaut.

Was tun? - Dokumentation

Um mit Perl arbeiten zu können, reicht es natürlich nicht aus, die Syntax der Sprache zu verstehen. Das ist lediglich Handwerkszeug. Viel wichtiger ist es, passende Funktionen und Programme zu kennen - oder zu wissen, wie wo man sie findet.

Perl wird mit umfassender Dokumentation ausgeliefert. Dort findet man neben vielen Anregungen zur Problemlösung oftmals auch Code-Fragmente, die man so in die eigenen Programme übernehmen darf. Die Dokumentation liegt teilweise in Form von sogenannten Manpages vor und wird mit dem Programm »man« angezeigt (z.B. »man perldoc« oder »man perl«).

Ein Teil liegt in einem speziellen Perl-Format vor und wird mit »perldoc« angezeigt. Dabei findet man auch die Manpages. »perldoc« bietet jedoch auch die Möglichkeit, die Beschreibung einzelner Funktionen direkt anzuzeigen, ohne erst das gesamte Dokument durchsuchen zu müssen. Mit »perldoc -f open« wird die Beschreibung der Routine »open« ausgegeben, die weiter unten benutzt wird. Zusätzlich wird mit »perldoc -q« nach Schlüsselwörtern in der beiliegenden FAQ.

Es lohnt sich, die umfassende Dokumentation zu Perl und den Modulen zu lesen, wenn man sich einarbeiten möchte. Um ein komplizierteres Beispiel zu verstehen, werden weitere wichtige Eigenschaften von Perl benötigt, z.B.

Dateioperationen

Was wären Programme, wenn sie nicht mit Dateien arbeiten könnten? Viele Aufgaben erfordern das Einlesen und Schreiben von Dateien, z.B. um Inhaltsverzeichnisse anzulegen, Abrechnungen zu erstellen u.s.w. Perl unterstützt die wichtigsten Dateioperationen mit einfacher Bedienung. Wer weniger Komfort, dafuer mehr Kontrolle benötigt, findet sie in »IO::Handle« und »IO::File«.

Wie in C auch, werden Dateien mit einem »open«-Befehl geöffnet und mit einem »close«-Befehl geschlossen. Beim Öffnen einer Datei wird sie einem sogenannten Datei-Handle, einer speziellen Variable, zugewiesen. Über dieses Handle wird anschließend aus der Datei gelesen oder in sie geschrieben.

Die Art, wie eine Datei geöffnet wird, bestimmt, ob sie zum Lesen oder zum Schreiben zur Verfügung steht. Der Begriff "Datei" ist hier etwas weiter gefaßt, denn es dürfen auch Programme verwendet werden, von denen gelesen wird oder in die geschrieben wird. Dabei wird die Ausgabe der Programme von Perl verwendet oder Perl liefert die Eingabe für die Programme.

Der einfachste Weg, eine Datei zu öffnen, geschieht mit der Anweisung »open (HANDLE, dateiname)«. Der Dateiname wird in einer Variablen übergeben oder direkt angegeben. Die Routine öffnet die Datei und weist sie dem Handle, das meistens zur besseren Lesbarkeit in Großbuchstaben geschrieben wird, zu. Zusätzlich wird »wahr« oder »falsch« zurückgegeben, so daß man im Perl-Programm abfragen kann, ob die Operation erfolgreich war.

Die folgenden Befehle sind gleichbedeutend:

   $file = "/etc/passwd";
   open (FOO, $file);

oder zusammengefaßt:

   open (FOO, "/etc/passwd");

So zu programmieren, zeugt jedoch nicht nur von schlechtem Stil, sondern ist auch nicht sicher vor Fehlern. Wenn das Öffnen der Datei nicht möglich war, dann würde das Programm munter mit der Datei arbeiten, auf die es gar nicht zugreifen kann. Das mag beim Lesen von einer Datei vielleicht nicht allzu gefährlich sein, wenn jedoch eine Datei beschrieben werden soll, würde es bedeuten, daß die gesamte Ausgabe verloren ginge. Daher sollten bei solchen Operationen immer mögliche Fehler abgefangen werden, z.B. wie folgt:

   if (open (FOO, "/etc/passwd") {
       close (FOO);
   } else {
   }
Datei öffnen in Perl

Um in eine Datei zu schreiben, wird ein Größerzeichen vor den Dateinamen geschrieben. Wenn die Datei allerdings bereits existiert, wird sie überschrieben. Soll hingegen Text an eine bestehende Datei angehängt werden, so werden zwei Größerzeichen verwendet. Wenn es nicht gelingt, die Datei zum Schreiben zu öffnen, wird teilweise das gesamte Programm abgebrochen. Dafür wird die Routine »die« verwendet, mit der zusätzlich eine Fehlermeldung und die Zeilennummer ausgegeben wird. Das folgende Fragment versucht, eine Datei zum Schreiben zu öffnen:

   open (FOO, ">/etc/passwd.new") ||
        die "Can't open /etc/passwd.new, $!,";

Wenn die Datei »/etc/passwd.new« nicht geöffnet werden kann, z.B. weil das Programm als normaler Benutzer und nicht als »root« ausgeführt wird, dann wird es mit einer entsprechenden Fehlermeldung abgebrochen. Dieses ist in nebenstehender Abbildung zu sehen.

Ein Sonderfall liegt dann vor, wenn keine Datei aus dem Dateisystem geöffnet, sondern ein Programm gestartet wird. Die Anweisung »open(FOO, "/bin/ls|")« startet das Programm »/bin/ls« und liest von dessen Ausgabe. Dieses Beispiel implementiert man jedoch besser mit »opendir()« und »readdir()«. Soll das Perl-Skript die Eingabe für ein Programm liefern, dann wird das Pipe-Zeichen (»|«) vor den Befehl gesetzt.

[Arrays]

Abbildung 1: Arrays sind eindimensionale Listen

Wurde eine Datei zum Lesen geöffnet, so bewirkt jede Verwendung von »«, daß eine weitere Zeile gelesen wird. Ein Sonderfall tritt ein, wenn der Wert nicht einer normalen Variablen zugewiesen wird, sondern einem Array. In dem Fall wird die gesamte Datei eingelesen und jedes Element des Arrays entspricht einer Zeile der Datei.

Arrays und Hashes

Eine Stärke von Perl besteht in der komfortablen Verwaltung von Arrays und Hashes. Arrays sind eindimensionale Listen von Werten. Perl vergrößert sie bei Bedarf automatisch, so daß sich der Programmierer nicht mit solch "unwichtigen" Dingen wie Listenmanagement beschäftigen muß. Die Elemente sind beliebige Perl-Objekte, Zeichenketten, Zahlen, Arrays, Listen, Zeiger etc.

Das gesamte Array wird mit »@name« referenziert, auf die einzelnen Elemente wird mit »$name[position]« zugegriffen. Dabei ist zu beachten, daß bei 0 angefangen wird zu zählen. Das erste Element ist daher »$name[0]«. Der Index des letzten Elements erhält man mit »$#name«. Ergibt dieses 0, so hat das Array ein Element. Nur, wenn -1 zurückgegeben wird, ist das Array leer und entspricht »()«.

[Hashes]

Abbildung 2: Ein Hash ist eine zweidimensionale Liste

Das wichtigste Array ist »@ARGV«. In ihm sind die Argumente gespeichert, die an das Programm auf der Kommandozeile übergeben wurden. Das erste Element (also »$ARGV[0]«) enthält das erste Argument.

Hashes bzw. assoziative Arrays sind besondere Typen von Arrays. Die Anzahl ihrer Elemente ist immer eine gerade Zahl. Es sind indizierte Arrays. Jedes Element ist ein Paar (key,value). Über den Index wird auf den Wert zugegriffen, hier werden jedoch geschweifte Klammern verwendet. Mit den speziellen Funktionen »keys« und »values« wird eine eindimensionale Liste der Indizes bzw. Werte erzeugt:

   @keys = keys %ENV;
   @values = values %ENV;
   
   foreach $key (@keys) {
       printf "%s=%s\n", $key, $ENV{$key};
   }
Hash- und Arrayfunktionen

Viele Funktionen geben Arrays zurück, weswegen es wichtig ist, mit ihnen umgehen zu können. Funktionen, die mehrere Werte zurückliefern, haben Arrays als Rückgabewerte. Einige Funktionen liefern zudem Zeichenketten oder einzelne Werte zurück, wenn die nachfolgende Zuweisung diese erwarten, oder ein Array, falls das Ergebnis einem Array zugewiesen wird.

Stringverarbeitung

Ein Großteil der Arbeiten, die mit Perl erledigt werden, betrifft das Aufteilen von Zeichenketten nach bestimmten Regeln. Oftmals werden Listen verarbeitet, dessen Felder durch Trennzeichen voneinander getrennt sind. Die Paßwortdatei (»/etc/passwd«) trennt die einzelnen Felder z.B. mit einem Doppelpunkt, die Filesystem-Table (»/etc/fstab«) verwendet eine beliebige Anzahl von Blanks (Leerzeichen und Tabulatoren) als Trennzeichen.

Sollen aus diesen Listen Berichte erstellt werden, dann müssen die Felder zuerst aufgeteilt werden. Dieses geschieht am einfachsten mit dem Befehl »split«, der eine gegebene Variable nach ebenfalls angegebenen Trennzeichen durchsucht und die einzelnen Elemente in einem Array speichert, das zurückgegeben wird. Als Trennzeichen werden reguläre Ausdrücke akzeptiert. Das gibt die nötige Freiheit, z.B. wenn ein oder mehrere Blanks verwendet werden.

Das folgende Programm verdeutlicht dieses. Es liest die Paßwortdatei und gibt die Logins und Namen der Benutzer aus. Die System-Benutzer wie »root«, »daemon« etc. werden übersprungen. Die Anweisung »next« springt aus der aktuellen Schleife heraus und die Ausführung des Programms wird am Ende der Schleife fortgesetzt. Ist die Schleifenbedingung weiterhin gültig, wird die Schleife mit neuen Werten erneut ausgeführt.

   
   if (open (P, "/etc/passwd")) {
       @passwd = <P>;
       close (P);
       foreach $line (@passwd) {
   	   @array = split (/:/, $line);
   	   next if ($array[2] < 100);
   	   printf "%s\t%s\n", $array[0], $array[4];
       }
   }
Verarbeiten einer einfachen Textdatei

Nutzt man einen oft verwendeten Sonderfall aus, so läßt sich das Programm vereinfachen. Perl verwendet die Standardvariable »$_«, wenn nicht explizit eine Variable angegeben wurde und sonst auch keine Eingabe zur Verfügung steht. Wird z.B. von einer Datei gelesen, ohne daß die Zeile einer Variablen zugewiesen wird, dann hat Perl sie implizit »$_« zugewiesen. Findet anschließend Pattern Matching oder ein Split statt, ohne daß explizit eine Variable angegeben wurde, so wird ebenfalls »$_« verwendet. Das Beispielprogramm von oben sieht modifiziert wie folgt aus:

   
   if (open (P, "/etc/passwd")) {
       while (<P>) {
   	   @array = split (/:/);
   	   next if ($array[2] < 100);
   	   printf "%s\t%s\n", $array[0], $array[4];
       }
       close (P);
   }
Inline-Verarbeitung einer Datei

Dabei werden zwei Seiteneffekt ausgenutzt. Wird lediglich »

« geschrieben, so bedeutet es implizit »$_ =

«, der Variablen »$_« wird also die nächste Zeile aus der zu »P« gehörenden Datei zugewiesen. Der zweite Seiteneffekt betrifft die Zuweisung, wenn sie als Bedingung verwendet wird. Der logische Wert einer Zuweisung ist normalerweise "wahr", es sei denn, es wird aus einer Datei gelesen und das Ende dieser ist erreicht. Wenn der Dateizeiger am Ende der Datei steht, und im Programm wird mit »while (

)« versucht, zu lesen, dann ist der logische Wert der Zuweisung "falsch" und die Schleifenbedingung trifft nicht mehr zu. Die Schleife wird daher beendet.

Beispiel: Finger

Das Programm wird leicht modifiziert, so daß es nur noch Informationen zu einem einzigen Benutzer ausgibt, ähnlich wie das Programm »finger«. Der Name des Benutzers wird in der Befehlszeile übergeben. Das Programm soll einen Fehler ausgeben, wenn kein Benutzer angegeben wurde.

   
   die "Kein Benutzer angegeben," if ($#ARGV == -1);
   
   if (open (P, "/etc/passwd")) {
       while (<P>) {
   	   @array = split (/:/);
   	   next until ($array[0] eq $ARGV[0]);
   	   printf "Login: %s\n", $array[0];
   	   @name = split (/,/,$array[4]);
   	   printf "  Name .....: %s\n", $name[0];
   	   printf "  Office ...: %s\n", $name[1] if ($#name> 0);
   	   printf "  Work phone: %s\n", $name[2] if ($#name> 1);
   	   printf "  Home phone: %s\n", $name[3] if ($#name> 2);
   	   printf "  Home .....: %s\n", $array[5];
   	   printf "  Shell ....: %s\n", $array[6];
       }
       close (P);
   }
Benutzerinformationen mit Perl ermitteln

Neu sind hier zwei Konstrukte: »next until ()« und »print if ()«. Die Anweisung »next« überspringt den Schleifenkörper, daher muß sie an eine Bedingung gekoppelt werden. Das Fragment »until (bedingung)« bedeutet, daß die daran geknüpfte Anweisung solange ausgeführt wird, bis die Bedingung "wahr" ergibt. Erst dann werden die nachfolgenden Anweisungen in der Schleife ausgeführt.

Da das Namensfeld in der Paßwort-Datei erneut eine Liste von vier Feldern sein kann, wird das Feld ebenfalls mit »split« in ein Array aufgeteilt. Sollte das Namensfeld leer sein, so ist auch das Array »@name« leer. Daher ist »$name[0]« eine leere Zeichenkette, wenn eine Zeichenkette erwartet wird, oder 0, wenn eine Zahl erwartet wird (z.B. bei »printf "%d\n", $name[0]«).

Die drei folgenden »printf«-Anweisungen werden nur dann ausgeführt, wenn ausreichend Elemente in diesem Array vorhanden sind. In diesem Fall wurde die »if«-Bedingung in bisher unbekannter Reihenfolge geschrieben, denn die Verzweigung folgt auf den Befehl und nicht umgekehrt. Eine "if"-Verzweigung läßt sich bequem abkürzen, wenn die eigentliche Verzweigung hinter der Anweisung geschrieben wird.

Die folgenden Code-Fragmente sind daher äquivalent:

   printf " Office ...: %s\n", $name[1] if ($#name> 0);
   
   if ($#name > 0) {
       printf " Office ...: %s\n", $name[1];
   }

Wenn nur eine einzige Anweisung von der Bedingung abhängt, wird meist die kürzere Variante verwendet.

Schreiben in Dateien

Bisher ist bekannt, wie Dateien geöffnet werden und wie aus ihnen gelesen wird, jedoch nicht, wie in sie geschrieben wird. Dazu muß man sich die Syntax der »print«-Befehle vor Augen führen:

   print HANDLE "text"

sowie

   printf HANDLE "format", argument [argument, ...]

Wenn in Beispielen bisher »print«-Befehle benutzt wurden, so sah die Verwendung jedoch anders aus. Das »HANDLE« war nicht zu sehen. Das liegt daran, daß Perl auch hier ein implizites Handle verwendet, wenn nicht explizit eines angegeben wird. Daher bedeutet:

   print "text\n"

soviel wie

   print STDOUT "text\n"

Die Ausgabe erfolgt also auf der Standard-Ausgabe, also auf der Konsole oder im xterm. Das gleiche gilt für das Lesen aus Dateien. Wird im Programm »<>« verwendet, so wird implizit die Standard-Eingabe verwendet. Die Anweisung ist daher äquivalent zu »«. Perl unterstützt drei vordefinierte Dateideskriptoren: »STDIN«, »STDOUT« und »STDERR«.

Das folgende Beispiel kopiert den Eintrag eines Benutzers aus der Paßwort-Datei in eine Datei im lokalen Verzeichnis. Dazu wird die Datei »onepasswd« zum Schreiben geöffnet.

   
   die "Kein Benutzer angegeben," if ($#ARGV == -1);
   
   open (OUT, ">onepasswd") || die "Can't open onepasswd,";
   
   if (open (P, "/etc/passwd")) {
       while (<P>) {
   	   @array = split (/:/);
   	   next until ($array[0] eq $ARGV[0]);
   	   print OUT;
       }
       close (P);
       close (OUT);
   }
Benutzerinformationen kopieren

Die Bedeutung dieses Programms sollte klar sein. In die neue Datei wird mit der Anweisung »print OUT« geschrieben. Doch was wird dort geschrieben? Dieses mal ist zwar ein Datei-Handle angegeben, jedoch kein Text. Hier kommt Perl wieder mit impliziten Variablen ins Spiel, denn wenn keine Variable angegeben wurde, wird automatisch »$_« verwendet. Was in der Variablen enthalten ist, wurde oben bereits erläutert.

Zur Verdeutlichung wird das Programm so modifiziert, daß anstelle von »print OUT« nur noch »print« geschrieben wird. Wie ist dann das Verhalten des Programms?

Martin Schulze
Quelle: freeX 2/00