TOC PREV Übungen NEXT INDEX

A Lösungen zu den Übungen

In diesem Anhang finden Sie die Lösungen zu den Übungen am Ende jedes Kapitels.

Lösungen zu den Übungen in Kapitel 2

  1. So könnte eine mögliche Lösung aussehen:
#!/usr/bin/perl -w
$pi = 3.141592654;
$umfang = 2 * $pi * 12.5;
print "Der Umfang eines Kreises mit dem Radius 12.5 ist $umfang.\n";
Wie Sie sehen, beginnt das Programm mit der typischen #!-Zeile; Ihr Pfad zu Perl kann sich davon unterscheiden. Außerdem haben wir die Warnungen eingeschaltet.
Die erste richtige Codezeile füllt die Variable $pi mit dem Wert von p. Es gibt eine Reihe von Gründen, aus denen ein guter Programmierer wie hier gezeigt eine Konstante1 benutzen würde: Es kostet Zeit, 3.141592654 in Ihr Programm einzufügen, wenn Sie den Wert mehr als einmal brauchen. Zudem könnte es einen mathematischen Fehler bedeuten, wenn Sie an einer Stelle Ihres Programms 3.141592654 schreiben und 3.14159 an einer anderen. Auf die hier beschriebene Art gibt es nur eine Stelle in Ihrem Programm, an der Sie überprüfen müssen, ob Sie nicht versehentlich 3.141952654 eingegeben haben und Ihre Raumsonde dadurch womöglich zum falschen Planeten schicken. Es ist einfacher, $pi einzugeben als p, insbesondere, wenn es bei Ihnen keine Unicode-Unterstützung gibt.
Außerdem ist Ihr Programm leichter zu pflegen, falls sich der Wert von p jemals ändern sollte.2
Als nächstes berechnen wir den Umfang und legen den Wert in $umfang ab. Am Schluß wird dieser Wert in einer netten Nachricht ausgegeben. Die Nachricht endet auf ein Newline-Zeichen, da jede Ausgabezeile eines guten Programms dies tun sollte. Falls Sie kein Newline-Zeichen benutzen, sieht Ihre Ausgabe, abhängig vom Shell-Prompt, unter Umständen wie folgt aus:
Der Umfang eines Kreises mit dem Radius 12.5 ist 78.53981635.bash-2.01$
Der graue Kasten am Ende der Zeile steht hierbei für die blinkende Einfügemarke. Der seltsame Text hinter Ihrer Nachricht ist die Eingabeaufforderung der Shell.3 Da der Umfang des Kreises eigentlich nicht 78.53981635.bash-2.01$, ist, könnte dies womöglich als Programmierfehler angesehen werden. Benutzen Sie also lieber \n am Ende jeder Ausgabezeile.
  1. So könnte eine mögliche Lösung aussehen:
#!/usr/bin/perl -w
$pi = 3.141592654;
print "Wie lautet der Radius? ";
chomp($radius = <STDIN>);
$umfang = 2 * $pi * $radius;
print "Der Umfang eines Kreises mit dem Radius $radius ist $umfang.\n";
Dies ist fast das gleiche Programm wie in der vorigen Übung, nur daß wir hier die Variable $radius benutzen, wo vorher der unveränderliche Wert 12.5 stand. Hätten wir das erste Programm mit etwas mehr Weitsicht geschrieben, hätten wir auch dort schon die Variable $radius verwendet. Mit Hilfe des chomp-Operators entfernen wir das Newline-Zeichen am Ende der Zeile. Selbst wenn wir das nicht getan hätten, würde unser mathematischer Ausdruck noch funktionieren, da der String "12.5\n" für die Berechnung automatisch in die Zahl 12.5 umgewandelt wird. Geben wir am Ende des Programms jedoch die Nachricht aus, bekommen wir jetzt eine Ausgabe wie die folgende:
Der Umfang eines Kreises mit dem Radius 12.5
ist 78.53981635.
Das liegt daran, daß das Newline-Zeichen sich immer noch in $radius befindet, auch wenn wir die Variable zwischenzeitlich als Zahl benutzt haben. Da sich in der print-Anweisung zwischen $radius und dem Wort "ist" ein Leerzeichen befindet, wird dieses nun als erstes Zeichen der zweiten Zeile ausgegeben. Die Moral von der Geschicht' lautet: Benutzen Sie chomp für Ihre Eingaben, es sei denn, Sie haben einen guten Grund, dies nicht zu tun.
  1. So könnte eine mögliche Lösung aussehen:
#!/usr/bin/perl -w
$pi = 3.141592654;
print "Wie lautet der Radius? ";
chomp($radius = <STDIN>);
$umfang = 2 * $pi * $radius;
if ($radius < 0) {
$umfang = 0;
}
print "Der Umfang eines Kreises mit dem Radius $radius ist $umfang.\n";
Hier haben wir einen Test auf einen ungültigen Radius eingebaut. Auf diese Weise wird bei einem ungültigen Radius jedenfalls kein negativer Wert mehr ausgegeben. Sie hätten auch den Radius auf null setzen können, um dann den Umfang zu berechnen, aber es gibt schließlich mehr als eine mögliche Lösung. Das ist übrigens das Motto von Perl: »Es gibt mehr als eine Lösung« (»There Is More Than One Way To Do It«, TIMTOWTDI). Aus diesem Grund beginnen die Lösungen zu den Übungen mit dem Satz: »So könnte eine mögliche Lösung aussehen.«
  1. So könnte eine mögliche Lösung aussehen:
print "Bitte geben Sie die erste Zahl ein: ";
chomp($eins = <STDIN>);
print "Bitte geben Sie die zweite Zahl ein: ";
chomp($zwei = <STDIN>);
$ergebnis = $eins * $zwei;
print "Das Ergebnis ist $ergebnis.\n";
Beachten Sie, daß wir in dieser Antwort die #!-Zeile nicht mit angegeben haben. Von jetzt an gehen wir davon aus, daß Sie wissen, daß die Zeile da ist, damit Sie sie nicht jedesmal mitlesen müssen.
Vermutlich ist die Wahl der Namen für die Variablen nicht besonders gelungen. In einem großen Programm könnte der Wartungsprogrammierer denken, die Variable $zwei solle den Wert 2 enthalten. In diesem kurzen Programm ist das wahrscheinlich noch nicht so wichtig, aber wir hätten die Variablen etwas verständlicher benennen können, beispielsweise $erste_antwort.
In diesem Programm würde es keinen Unterschied machen, ob wir chomp benutzen oder nicht, da die Variablen $eins und $zwei nicht mehr als Strings benutzt werden, nachdem sie deklariert wurden. Ändert der Wartungsprogrammierer aber nächste Woche die ausgegebene Nachricht in etwas wie: Das Ergebnis der Multiplikation von $eins und $zwei ist $ergebnis.\n - werden uns diese elendigen Newline-Zeichen immer weiter verfolgen. Auch in diesem Fall gilt also: Benutzen Sie chomp,4 es sei denn, Sie haben einen guten Grund, dies nicht zu tun. Die folgende Übung zeigt einen solchen Fall.
  1. So könnte eine mögliche Lösung aussehen:
print "Bitte geben Sie einen String ein: ";
$string = <STDIN>;
print "Bitte geben Sie eine Anzahl ein: ";
chomp($anzahl = <STDIN>);
$ergebnis = $string x $anzahl;
print "Das Ergebnis ist:\n$ergebnis";
Das Programm ist fast das gleiche wie das aus der letzten Übung. Hier »multiplizieren« wir einen String. Wir haben also die Struktur der vorigen Übungen beibehalten. Hier benutzen wir aber für den String kein chomp, da die Aufgabenstellung verlangte, die Strings auf eigenen Zeilen auszugeben. Hätte der Benutzer fred und ein Newline-Zeichen als String und 3 für die Zahl eingegeben, stünde nun hinter jedem Vorkommen automatisch ein Newline-Zeichen - und genau das wollten wir ja!
In der print-Anweisung am Ende des Programms stellen wir dem $ergebnis ein zusätzliches Newline-Zeichen voran, damit das erste fred auch auf einer eigenen Zeile ausgegeben wird. Hätten wir das nicht getan, stünde des erste fred am Ende der ersten Zeile und die anderen zwei in einer Reihe untereinander:
Das Ergebnis ist: fred
fred
fred
Gleichzeitig haben wir am Ende der print-Anweisung kein Newline-Zeichen angegeben, da $ergebnis dies bereits enthalten sollte.
In den meisten Fällen ist es Perl egal, an welcher Stelle in Ihrem Programm Sie Leerzeichen benutzen. Sie können sie benutzen oder auch weglassen, ganz wie Sie wollen. Aber es ist wichtig, nicht versehentlich eine falsche Formulierung zu benutzen! Stünde der x-Operator direkt an dem davor stehenden Variablennamen $str, würde Perl hierin die Variable $strx sehen und das funktioniert nun einmal nicht.

Lösungen zu den Übungen in Kapitel 3

  1. So könnte eine mögliche Lösung aussehen:
print "Bitte geben Sie einige Zeilen ein, und druecken Sie dann Ctrl-D:\n";
# oder auch Ctrl-Z
@zeilen = <STDIN>;
@zeilen_umgedreht = reverse @zeilen;
print @zeilen_umgedreht;
...oder, noch einfacher:
print "Bitte geben Sie einige Zeilen ein, und druecken Sie dann Ctrl-D:\n";
print reverse <STDIN>;
Die meisten Perl-Programmierer ziehen vermutlich die zweite Lösung vor, solange keine Liste der Zeilen für eine spätere Verwendung angelegt werden muß.
  1. So könnte eine mögliche Lösung aussehen:
@namen = qw/ Fred Betty Barney Dino Wilma Pebbles Bambam /;
print "Geben Sie zeilenweise ein paar Zahlen zwischen 1 und 7 ein, und druecken Sie dann Ctrl-D:\n";
chomp(@zahlen = <STDIN>);
foreach (@zahlen) {
print "$zahlen[ $_ - 1 ]\n";
}
Wir müssen hier eins von der Index-Zahl abziehen, damit der Benutzer Zahlen von 1 bis 7 eingeben kann, obwohl die Arrayindizes von 0 bis 6 gehen. Eine andere Möglichkeit wäre die Verwendung eines Dummy-Elements in unserem @namen, und zwar wie folgt:
@namen = qw/ dummy_element Fred Betty Barney Dino Wilma Pebbles Bambam /;
Schreiben Sie sich ein paar Zusatzpunkte gut, wenn Sie zusätzlich noch überprüft haben, ob die Benutzereingaben tatsächlich im Bereich zwischen 1 und 7 liegen.
  1. Hier sehen Sie eine mögliche Lösung, bei der alles auf einer Zeile ausgegeben wird:
chomp(@zeilen = <STDIN>);
@sortiert = sort @zeilen;
print "@sortiert\n";
Um die Ausgaben auf ihren eigenen Zeilen darzustellen, können Sie auch folgendes schreiben:
print sort <STDIN>;

Lösungen zu den Übungen in Kapitel 4

  1. So könnte eine mögliche Lösung aussehen:
sub gesamt {
my $summe; # private Variable
foreach (@_) {
$summe += $_;
}
$summe;
}
Diese Subroutine benutzt die Variable $summe, um den Gesamtwert zu speichern. Zu Beginn ist der Wert von $summe noch undef, da wir die Variable neu angelegt haben. (Noch einmal: Es gibt keine automatische Verbindung zwischen @_, dem Parameterarray, und $_, der Standardvariablen für die foreach-Schleife.)
Beim ersten Schleifendurchlauf wird zur Variablen $summe die erste Zahl (in $_) hinzugezählt. Bis zu diesem Zeitpunkt ist der Wert von $summe selbstverständlich undef, da wir hier bisher noch nichts gespeichert haben. Da wir die Variable aber hier als Zahl benutzen (was Perl an dem numerischen Operator += erkennt), wird sie hier behandelt, als hätte sie bereits den Wert 0. Perl addiert zur ersten Zahl 0 hinzu und speichert das Ergebnis wieder in $summe.
Beim nächsten Schleifendurchlauf wird der nächste Parameter zum Wert von $summe hinzuaddiert, der nun nicht länger undef ist. Die Summe von Parameter und Wert wird wiederum in $summe gespeichert. Dies wird nun so lange wiederholt, bis es keine Parameter mehr gibt und der Wert von $summe an die aufrufende Anweisung zurückgegeben wird.
In dieser Subroutine gibt es eine mögliche Fehlerquelle, je nachdem wie Sie sich die Dinge vorstellen. Nehmen wir an, die Subroutine wäre mit einer leeren Parameterliste aufgerufen worden (worauf wir in der neugeschriebenen Subroutine &max im Kapitel selbst eingegangen sind). In diesem Fall wäre der Wert von $summe undef, und dieser Wert würde auch zurückgegeben. In der Subroutine wäre es aber vermutlich »korrekter«, statt dessen 0, zurückzugeben. (Wollten Sie das Ergebnis einer leeren Liste jedoch von der Summe aus (3, -5, 2) unterscheiden, indem Sie undef zurückgeben, wäre das auch nicht falsch.)
Wollen Sie jedoch keine undefinierten Werte zurückgeben, so gibt es ein einfaches Mittel dagegen: Initialisieren Sie $summe einfach mit dem Wert 0 anstatt das standardmäßige undef zu benutzen:
my $summe = 0;
Nun gibt die Subroutine immer einen Wert zurück, selbst wenn eine leere Parameterliste übergeben worden ist.
  1. So könnte eine mögliche Lösung aussehen:
# Denken Sie daran, die Subroutine &gesamt aus der vorigen Übung
# einzubauen!
print "Die Zahlen von 1 bis 1000 ergeben zusammen", &gesamt(1..1000),".\n";
Wir können die Subroutine nicht direkt aus einem String in doppelten Anführungszeichen heraus aufrufen.5 Statt dessen übergeben wir den Subroutinenaufruf einfach als weiteres Element der Liste, die print übergeben wird. Das Gesamtergebnis ist 500500, eine runde Summe. Hierbei sollten Sie kaum bemerken, wie die Zeit vergeht. Die Arbeit mit Parameterlisten, die tausend Werte enthalten, ist für Perl reine Routine.

Lösungen zu den Übungen in Kapitel 5

  1. So könnte eine mögliche Lösung aussehen:
my %nachname = qw{
Fred Feuerstein
Barney Geroellheimer
Wilma Feuerstein
};
print "Bitte geben Sie einen Vornamen ein: ";
chomp(my $name = <STDIN>);
print "Aha, Sie meinen $name $nachname{ $name }.\n";
In diesem Fall benutzen wir eine qw//-Liste (mit geschweiften Klammern als Trennzeichen), um den Hash zu initialisieren. Solange wir mit einfachen Daten arbeiten, ist das völlig in Ordnung, da sowohl die Schlüssel als auch die Werte einfache Namen sind. Sobald Ihre Daten jedoch Leerzeichen oder anderes enthalten - zum Beispiel wenn Herr Müller-Lüdenscheidt oder Die sieben Zwerge Steintal einen Besuch abstatten - würde diese einfache Methode nicht mehr so gut funktionieren.
Eventuell haben Sie sich entschieden, jedes Schlüssel/Wert-Paar für sich zuzuweisen, wie hier:
my %nachname;
$last_name{"Fred"} = "Feuerstein";
$last_name{"Barney"} = "Geroellheimer";
$last_name{"Wilma"} = "Feuerstein";
Beachten Sie, daß Sie Ihren Hash zuerst deklarieren müssen, bevor Sie ihm irgendwelche Werte zuweisen können. (Falls use strict benutzt wird, müssen Sie Ihren Hash mit my deklarieren.) Auf die folgende Art läßt sich my jedoch nicht benutzen:
my $nachname{"Fred"} = "Feuerstein"; # Hoppla!
Der my-Operator funktioniert nur mit vollständigen Variablen, niemals mit einem einzelnen Element eines Arrays oder Hashs. Und wo wir gerade von lexikalischen Variablen reden: Es ist Ihnen vielleicht aufgefallen, daß die lexikalische Variable $name innerhalb des Funktionsaufrufs von chomp durchgeführt wird. Es kommt recht häufig vor, daß eine my-Variable, wie hier gezeigt, erst bei Bedarf deklariert wird.
Dies ist ein weiterer Grund, warum die Verwendung von chomp so wichtig ist. Gibt jemand den aus fünf Zeichen bestehenden String "Fred\n" ein und vergessen wir, mit chomp das Newline-Zeichen zu entfernen, wird versucht, das Hashelement mit dem Schlüssel "Fred\n" zu finden, und das gibt es nicht. Natürlich ist auch die Verwendung von chomp nicht hundertprozentig wasserdicht. Gibt jemand beispielsweise den String "Fred \n" (mit einem Leerzeichen nach Fred) ein, hätten wir bis jetzt keine Möglichkeit herauszufinden, daß eigentlich Fred gemeint war.
Falls Sie mit exists überprüft haben, ob der angegebene Schlüssel existiert, können Sie dem Benutzer zumindest mitteilen, daß ein Name offenbar falsch geschrieben wurde. Schreiben Sie sich ein paar Zusatzpunkte gut, wenn Sie das gemacht haben.
  1. So könnte eine mögliche Lösung aussehen:
my(@woerter, %zaehler, $wort); # (optionales) Deklarieren der Variablen
chomp(@woerter = <STDIN>);

foreach $wort (@woerter) {
$zaehler{$wort} += 1; # oder $zaehler{$wort} = $zaehler{$wort}+1;
}

foreach $wort (keys %zaehler) { # oder sort keys %zaehler
print "$wort kam $count{$word} mal vor.\n";
}
Hier haben wir alle Variablen zu Beginn des Programms deklariert. Wenn Sie vor Perl bereits andere Sprachen, wie etwa Pascal, gelernt haben (bei denen Variablen immer »am Anfang« deklariert werden), kommt Ihnen diese Methode vermutlich eher bekannt vor, als die Variablen erst zu deklarieren, wenn sie wirklich gebraucht werden. Wir deklarieren hier unsere Variablen auf jeden Fall, da wir davon ausgehen, daß standardmäßig use strict benutzt wird. Prinzipiell erwartet Perl solche Deklarationen jedoch nicht.
Als nächstes benutzen wir den Zeileneingabe-Operator <STDIN> im Listenkontext, um alle Eingabezeilen in das Array @woerter einzulesen; danach wenden wir chomp auf alle eingelesenen Zeilen gleichzeitig an. Das Array @woerter enthält nun sämtliche eingegebenen Wörter als separate Elemente (sofern sie jeweils auf einer eigenen Zeile gestanden haben, wie es verlangt war, versteht sich).
Nun geht die erste foreach-Schleife die Wörter durch. In der Schleife findet sich hierbei die wichtigste Anweisung des gesamten Programms. Hier wird zum Wert von $zaehler{$wort} eins hinzugezählt und das Ergebnis wieder in $zaehler{$wort} gespeichert. Sie können hier beide Schreibweisen benutzen, wobei die kurze Version (mit dem +=-Operator) ein wenig effizienter ist, da Perl das $wort im Hash nur einmal nachschlagen muß.6
In der ersten foreach-Schleife zählen wir auf diese Weise zum Wert von $zeaehler{$wort} für jedes Wort eins hinzu. Wäre das erste Wort fred gewesen, würden wir nun also $zaehler{"fred"} um eins erhöhen. Da wir $zaehler{"fred"} hier jedoch zum erstenmal zu Gesicht bekommen, ist dessen Wert noch undef. Wir benutzen den Wert hier aber als Zahl (durch die Benutzung von += beziehungsweise +, falls Sie die lange Form gewählt haben), daher wandelt Perl diesen Wert automatisch von undef in eine 0 um. Schließlich wird er um eins erhöht, so daß $zaehler{"fred"} nun den Wert 1 besitzt.
Nehmen wir an, beim nächsten Schleifendurchlauf laute das Wort barney. Nun addieren wir eins zu $zaehler{"barney"} und verwandeln so auch dessen Wert von undef in 1.
Im dritten Schleifendurchlauf wird als Wort ein weiteres Mal fred eingegeben. Wir addieren zum Wert von $zaehler{"fred"} also erneut eins hinzu und erhöhen ihn so von 1 auf 2. Das Ergebnis wird auch hier wieder in $zaehler{"fred"} gespeichert, was bedeutet, daß wir fred zweimal gesehen haben.
Wenn wir zum Ende der foreach-Schleife kommen, haben wir also ermittelt, wie oft jedes Wort eingegeben wurde. In unserem Hash gibt es für jedes (einmalige) Wort einen Schlüssel, wobei der dazugehörige Wert die Anzahl der Vorkommen des Wortes wiedergibt.
Als nächstes durchläuft nun die zweite foreach-Schleife die Schlüssel unseres Hashs, also die eingegebenen Wörter. Wie bereits gesagt, kann ein Schlüssel jeweils immer nur einmal vorkommen. Bei jedem Schleifendurchlauf sehen wir also nun ein anderes Wort, für das wir eine Nachricht wie »fred kam 3 mal vor.« ausgeben.
Wollen Sie sich mit dieser Lösung ein paar Zusatzpunkte verdienen, können Sie die Schlüssel (keys) vor der Ausgabe noch sortieren. Enthält die Ausgabeliste mehr als ein Dutzend Elemente, ist eine Sortierung empfehlenswert, da die Person, die das Programm debuggen muß, das gesuchte Element leichter finden kann.

Lösungen zu den Übungen in Kapitel 6

  1. So könnte eine mögliche Lösung aussehen:
print reverse <>;
Das ist aber einfach! Diese Formulierung funktioniert, weil print eine Liste von Strings erwartet. Diese bekommt es, indem es reverse im Listenkontext aufruft. reverse sucht seinerseits nach einer Liste von Strings, die es umdrehen kann. Diese bekommt es, indem es die vom Raumschiff-Operator zurückgegebenen Daten ebenfalls im Listenkontext benutzt. Der Raumschiff-Operator gibt eine Liste aller Zeilen der Dateien zurück, die der Benutzer angegeben hat. Diese Liste entspricht der Ausgabe von cat. reverse dreht diese Liste nun um, und print gibt sie aus.
  1. So könnte eine mögliche Lösung aussehen:
print "Geben Sie einige Zeilen ein, und druecken Sie dann Ctrl-D:\n";
# oder Ctrl-Z
chomp(my @zeilen = <STDIN>);

print "1234567890" x 7, "12345\n"; # 75 Zeichen breites "Lineal"

foreach (@zeilen) {
printf "%20s\n", $_;
}
Wir beginnen damit, alle Textzeilen einzulesen und mittels chomp die Newline-Zeichen zu entfernen. Als nächstes geben wir unsere »Lineal«-Zeile aus. Da wir dies nur als Hilfe beim Debuggen brauchen, wird diese Zeile normalerweise auskommentiert, wenn unser Programm fertiggestellt ist. Wir hätten nun die Zeile "1234567890" so oft eingeben können, wie wir sie brauchen, oder sie einfach kopieren und einfügen können, um die »Lineal«-Zeile zu erzeugen. Statt dessen benutzen wir einfach den x-Operator.
Schließlich iteriert die foreach-Schleife über unsere Liste und gibt dabei jede Zahl im Format %20s aus. Sie hätten aber auch ein Format erzeugen können, mit dem die Liste auf einmal ausgegeben wird, ohne dabei eine Schleife benutzen zu müssen:
my $format = "%20s\n" x @zeilen;
printf $format, @zeilen;
Häufig sind die Spalten versehentlich nur 19 Zeichen breit. Das geschieht, wenn Sie sich7 sagen: »Wozu wenden wir chomp auf den Inhalt an, wenn wir die Newline-Zeichen am Ende doch wieder einbauen müssen?«. Folglich lassen Sie das chomp weg und benutzen das Format "%20s" (ohne das Newline-Zeichen).8 Und plötzlich fehlt der Ausgabe ein Zeichen. Wie kann das sein?
Das Problem tritt auf, wenn Perl die Anzahl der Leerzeichen zu berechnen versucht, die gebraucht werden, um Ihre Zeilen rechtsbündig auszugeben. Gibt der Benutzer Hallo und ein Newline-Zeichen ein, sieht Perl sechs Zeichen und nicht fünf, da auch der Zeilenumbruch ein Zeichen ist. Hieraufhin werden vierzehn Leerzeichen ausgegeben und ein String von sechs Zeichen Länge. Das ergibt genau die zwanzig Zeichen, die in der Formatangabe "%20s" vorgegeben waren. Hoppla!
Perl sieht sich den Inhalt eines Strings nicht an, um die Breite zu ermitteln. Statt dessen werden einfach nur die enthaltenen Zeichen gezählt. Ein Newline-Zeichen (aber auch andere Zeichen, wie der Tabulator oder das NUL-Zeichen) bringen die Sache durcheinander.9
  1. So könnte eine mögliche Lösung aussehen:
print "Wie viele Zeichen soll die Spalte breit sein? ";
chomp(my $breite = <STDIN>);

print "Geben Sie einige Zeilen ein, und druecken Sie dann Ctrl-D:\n";
# oder Ctrl-Z
chomp(my @zeilen = <STDIN>);

print "1234567890" x (( $breite + 9 ) / 10), "\n";
# "Lineal"-Zeile nach Bedarf anpassen

foreach (@zeilen) {
printf "%${breite}s\n", $_;
}
Dieses Programm funktioniert fast genau wie das vorige. Der einzige Unterschied besteht darin, daß wir nun den Benutzer einen Wert für die Breite angeben lassen. Wir holen diese Information zuerst ein, da auf manchen Systemen keine weiteren Daten mehr angegeben werden können, nachdem einmal das Dateiende-Zeichen benutzt wurde. In der Praxis würden Sie selbstverständlich eine bessere Methode benutzen, um das Ende der Eingabe zu ermitteln, wie wir in späteren Kapiteln sehen werden.
Ein weiterer Unterschied zum vorigen Programm ist die Linealzeile. Wir haben hier ein bißchen Mathe benutzt, um die Linealzeile mindestens so breit zu machen, wie benötigt. Haben Sie das geschafft, gibt es auch hier einige Zusatzpunkte. Eine weitere Herausforderung besteht darin, zu beweisen, daß unsere Berechnungen auch richtig sind. (Tip: Es können auch Breiten wie 50 und 51 eingegeben werden. Bedenken Sie auch, daß die Zahlenangabe für x abgeschnitten und nicht gerundet wird.)
Um das Format zu erzeugen, haben wir den Ausdruck "%${breite}s\n" verwendet, in dem die Variable $breite interpoliert wird. Die geschweiften Klammern werden hier gebraucht, um den Namen vom folgenden s zu isolieren. Ohne geschweifte Klammern würden wir versuchen, eine Variable mit dem Namen $breites, zu interpolieren. Alternativ hätten Sie für die Erzeugung des Format-Strings auch einen Ausdruck wie '%' . $breite . "s\n" einsetzen können.
Durch die Verwendung von $breite haben wir eine weitere Situation, in der auf jeden Fall chomp verwendet werden muß. Tun wir das nicht, sähe der resultierende Format-String jetzt so aus: "%30\ns\n", was nicht besonders nützlich wäre.
Leute, die printf bereits einmal gesehen haben, denken vielleicht an eine weitere Lösungsmöglichkeit. Da printf seine Wurzeln in C hat, das keine Interpolation in Strings besitzt, können wir den gleichen Trick wie die C-Programmierer anwenden. Wird anstelle einer numerischen Angabe für die Feldbreite ein Sternchen (*) benutzt, ersetzt Perl dieses durch die Angabe im ersten übergebenen Parameter.
printf "%*s\n", $breite, $_;

Lösungen zu den Übungen in Kapitel 7

  1. So könnte eine mögliche Lösung aussehen:
/fred/
Natürlich müssen Sie dieses Muster noch in Ihr Testprogramm eingeben. Das ist nicht so schwer. Wichtiger ist es, das Muster mit verschiedenen Strings auszuprobieren. Das Muster paßt nicht auf Fred, woran wir erkennen können, daß reguläre Ausdrücke zwischen Groß- und Kleinschreibung unterscheiden. (Wir werden nachher sehen, wie Sie das ändern können.) Die Strings frederick und Alfred ergeben Treffer, da beide den aus vier Buchstaben bestehenden String fred enthalten. (Wir werden später sehen, wie Sie die Suche auf ein bestimmtes Wort einschränken können.)
Wenn Ihr Testprogramm korrekt funktioniert,10 sollte es Ihnen die zwei Treffer als |<fred>erick| und |Al<fred>| anzeigen. Die spitzen Klammern bezeichnen hierbei die Stelle, an der fred in den Strings gefunden wurde.
  1. So könnte eine mögliche Lösung aussehen:
/a+b*/
Hiermit werden (wegen des Pluszeichens) ein oder mehr Vorkommen des Buchstabens a gefunden, gefolgt von keinem oder mehr Vorkommen des Buchstabens b. Hiermit ist die Aufgabenstellung erfüllt. Sie hätten aber auch mit einer anderen Lösung Erfolg gehabt. Da hier nach einer beliebigen Anzahl von bs gesucht werden soll, wissen Sie, daß dieses Teilmuster auf jeden Fall zutrifft. Mit dem Muster /a+/ hätten Sie demnach die gleichen Strings gefunden.11
Wollen Sie ein oder mehrere as finden, reicht es demnach aus, wenn das erste a gefunden wird, um einen Treffer zu erzielen. Das Muster /a/ paßt also auf die gleichen Strings wie die ersten zwei Muster. Die Beschreibung »ein beliebiger String, der mindestens ein a, gefolgt von einer beliebigen Anzahl von bs enthält« bedeutet also genau das gleiche wie »ein beliebiger String, der ein a enthält«. Es werden demnach alle Beispielstrings bis auf fred gefunden.
Und dies sind immer noch nicht die einzigen Möglichkeiten, dieses Muster zu konstruieren. Oft müssen Sie sich in einer solchen Situation entscheiden, welches der vielen möglichen Muster Ihre Vorgaben am besten erfüllt.
  1. So könnte eine mögliche Lösung aussehen:
/\\*\**/
Das ist, was in der Aufgabenstellung gefordert war: Ein Backslash (der seinerseits mit einem Backslash geschützt wird12), der keinmal oder öfter vorkommen darf (symbolisiert durch das Sternchen, das hier als Quantifier wirkt), gefolgt von einem literalen Sternchen (weshalb dieser von einem Backslash geschützt wird), das ebenfalls in einer beliebigen Anzahl vorkommen darf (ein weiteres Sternchen als Quantifier). Uff!
Und wie sieht es mit den Beispielstrings aus? Wurden irgendwelche Treffer erzielt? Aber sicher - und zwar bei allen! Das liegt daran, daß sowohl der Backslash als auch das Sternchen beliebig oft vorkommen dürfen, also auch keinmal. Das Muster trifft demnach auch auf den leeren String zu. Die folgende Regel hat also immer Gültigkeit: Paßt ein Muster auf den leeren String, wird es immer einen Treffer erzeugen, da ein leerer String in jedem anderen String enthalten ist. Das Muster trifft hierbei an der ersten Stelle zu, an der Sie suchen.
Das Muster paßt also, wie erwartet, auf alle vier Zeichen in \\**. Auch der leere String am Anfang von fred wird gefunden, was Sie vermutlich nicht erwartet haben. In dem String barney \\\*** wird ebenfalls der leere String am Anfang gefunden. Vermutlich hätten Sie eigentlich die Backslashes und Sternchen am Ende finden wollen, aber dem Muster ist das egal. Es sieht sich den Anfang des Strings an, findet keine (oder mehr) Backslashes, gefolgt von keinen (oder mehr) Sternchen, erklärt dies zu einem Treffer und geht nach Hause, um fernzusehen. Und auch in dem String *wilma\ wird einfach nur das Sternchen am Anfang gefunden, da schon der erste Treffer für einen erfolgreichen Mustervergleich ausreicht.
Bittet Sie also jemand um ein Muster, das auf eine beliebige Anzahl von Backslashes, gefolgt von einer beliebigen Anzahl Sternchen paßt, ist das hier besprochene Muster technisch korrrekt. Es ist jedoch ziemlich wahrscheinlich, daß dies nicht wirklich das Gewünschte ist. Gesprochene Sprachen wie Englisch und Deutsch sorgen gelegentlich für Mißverständnisse. Reguläre Ausdrücke meinen dagegen immer genau das, was sie sagen.
In diesem Fall hat Ihr Auftraggeber vergessen zu sagen, daß immer mindestens ein Zeichen gefunden werden soll. Gibt es mindestens einen Backslash, erzeugt das Muster /\\+\**/ einen Treffer. (Hierbei haben wir das erste Sternchen gegen ein Pluszeichen ausgetauscht und finden nun mindestens einen Backslash.) Ist im zu durchsuchenden String kein Backslash vorhanden, brauchen wir mindestens ein Sternchen, das wir mit dem Muster /\*+/ finden. Nehmen wir diese beiden Teile zusammen, erhalten wir:
/\\+\**|\*+/
Häßlich, was? Reguläre Ausdrücke sind zwar mächtig, aber nicht unbedingt schön. Diese Tatsache hat dazu beigetragen, daß Perl von manchen Uneingeweihten als »unlesbare Sprache« angesehen wird. Um sicherzugehen, daß sich niemand auf diese Art über Ihren Code beschwert, ist es sinnvoll, nicht so offensichtliche Suchmuster in einem Kommentar zu erklären. Haben Sie Muster wie dieses aber erst einmal ein Jahr lang benutzt, werden Sie vermutlich ein anderes Verständnis von »offensichtlich« haben als heute.
Wie verhält sich das neue Muster nun aber zu unseren Beispielstrings? Bei \\** werden alle vier Zeichen gefunden, genau wie beim letzten Mal. fred wird nun nicht mehr gefunden, was nach der Problembeschreibung vermutlich eher korrekt ist. Bei barney \\\*** werden nun, wie erhofft, die sechs Zeichen am Ende gefunden und bei *wilma\ das Sternchen am Anfang.
  1. So könnte eine mögliche Lösung aussehen:
while (<>) {
if (/wilma/) {
print;
}
}
Dieses Programm funktioniert wie grep. Für jede Eingabezeile (in $_) wird überprüft, ob das Muster paßt. Ist das der Fall, geben wir die Zeile wieder aus. In diesem Programm benutzen wir das Standardverhalten von print. Das bedeutet, standardmäßig wird der Inhalt von $_ benutzt, es sei denn, Sie haben etwas anderes angegeben. Wir haben also ein Programm geschrieben, das durchgängig $_ benutzt, dies aber nirgendwo explizit erwähnt. Perl-Programmierer lieben es, diese Defaults zu benutzen und sich so eine Menge Tipperei zu ersparen. Das wird also vermutlich nicht das letzte Programm gewesen sein, in dem Sie so etwas sehen.
Wollten Sie für die Zusatzpunkte auch noch Wilma finden, wäre das Muster /wilma|Wilma/ eine Möglichkeit gewesen, dies zu erreichen. Noch einfacher hätten Sie aber auch (w|W)ilma/ schreiben können. Leute, die reguläre Ausdrücke bereits in anderen Implementierungen benutzt haben, kennen diese »Zeichenklassen» genannte Möglichkeit bereits (siehe nächstes Kapitel), mit der das Obenstehende sogar noch kürzer (und effizienter) hätte dargestellt werden können.13
  1. So könnte eine mögliche Lösung aussehen:
while (<>) {
if (/Wilma/) {
if (/Fred/) {
print;
}
}
}
Hierbei wird nur dann auf /Fred/ getestet, wenn wir /Wilma/ bereits gefunden haben. Fred kann in einer Zeile aber auch vor Wilma stehen. Der eine Test ist hier vom Ergebnis des anderen abhängig.
Wenn Sie den verschachtelten if-Test vermeiden wollten, hätten Sie auch folgendes schreiben können:14
while (<>) {
if (/Wilma.*Fred|Fred.*Wilma/) {
print;
}
}
Das funktioniert, da Wilma entweder vor Fred stehen kann oder aber Fred vor Wilma. Ein Muster wie /Wilma.*Fred/ hätte bei Fred und Wilma Feuerstein keinen Treffer erzeugt, obwohl in dieser Zeile beide Namen vorkommen.
Wir haben dies als Übung für Zusatzpunkte definiert, da viele Leute an dieser Stelle eine mentale Sperre besitzen. Wir haben Ihnen zwar einen »oder«-Operator gezeigt (in Form des vertikalen Balkens »|«), aber keine Möglichkeit, »und« auszudrücken. Der Grund liegt darin, daß es in regulären Ausdrücken kein »und« gibt.15 Wenn Sie herausfinden wollen, ob zwei verschiedene Muster auf einen String zutreffen, müssen Sie beide Muster einzeln überprüfen.

Lösungen zu den Übungen in Kapitel 8

  1. So könnte eine mögliche Lösung aussehen:
/\b(fred|wilma)\s+feuerstein\b/
Wenn Sie die Anker für die Wortgrenze, \b, vergessen haben, ziehen Sie sich einen halben Punkt ab. Ohne die Anker werden auch Strings wie alfreds feuersteine gefunden. In der Aufgabenstellung war jedoch von bestimmten Wörtern die Rede.
  1. Der Grund für diese Übung ist vermutlich nicht gleich ersichtlich. In der Praxis können solche Aufgaben jedoch durchaus einmal auftreten. Eines Tages werden Sie das Pech haben, ein Programm pflegen zu müssen, bei dem Sie nicht gleich wissen, was der Autor mit seinen regulären Ausdrücken beschreiben wollte.16
Das Muster /"([^"]*)"/ paßt auf einen einfachen String in doppelten Anführungszeichen. Hiermit meinen wir aber nicht die Perl-eigenen Strings in doppelten Anführungszeichen. Die können nämlich ihrerseits durch einen Backslash geschützte Anführungszeichen oder andere Backslash-Magie enthalten. Das Muster paßt auf ein Anführungszeichen, gefolgt von einem String, der kein Anführungszeichen enthalten darf, und einem schließenden Anführungszeichen. Dazwischen muß nicht unbedingt etwas stehen. Die runden Klammern werden hier nicht für die Gruppierung benötigt. Das heißt, sie werden vermutlich zum Anlegen einer Speichervariablen benutzt. Wie Sie im folgenden Kapitel sehen werden, wird hiermit der in Anführungszeichen stehende String für eine Benutzung später im Programm vorbereitet. Vielleicht wird der Ausdruck benutzt, um in Anführungszeichen stehende Informationen aus einer Konfigurationsdatei zu lesen. Außerdem wäre es in solch einem Fall ratsam, Anker für die Wortgrenzen zu benutzen.
Das Muster /^0?[0-3]?[0-7]{1,2}$/ paßt auf einen String, der ausschließlich eine oktale Zahl zwischen 0 und 0377 enthält (mit einer optional vorangestellten Null). Wie Sie sehen, ist dieser String sowohl am Anfang wie auch am Ende des Suchmusters verankert. Vor und nach der Zahl dürfen also keine weiteren Zeichen mehr stehen. (Das vorige Muster war nicht verankert, würde also an einer beliebigen Stelle im durchsuchten String einen Treffer erzielen.)
Das Muster /^\b[\w.]{1,12}\b$/ trifft auf Strings zu, die ausschließlich »Wort«-Zeichen (Buchstaben, Zahlen und Unterstriche) und Punkte enthalten, aber nicht mit einem Punkt beginnen dürfen. Die maximale Länge des Strings ist außerdem auf 12 Zeichen beschränkt.
Da Punkte innerhalb von Zeichenklassen keine Sonderbedeutung haben, können wir hier auf die Verwendung eines Backslashs verzichten. Hierdurch trifft die Zeichenklasse auf einfache Buchstaben, Ziffern, Unterstriche und Punkte zu.
Wir benutzen hier die Anker für den Beginn und das Ende des Strings sowie die Anker für Wortgrenzen. Hiermit erreichen wir, daß weder am Anfang noch am Ende des Strings ein Punkt stehen kann, da Punkte nicht zu den »Wort«-Zeichen zählen.
Das Muster paßt also auf Strings wie perl.tar.gz, aber nicht auf ein_unglaublich_langer_dateiname oder perl.tar. oder .profile und auch nicht auf ...17 Das Suchmuster könnte zum Beispiel bei der Überprüfung von Dateinamen nützlich sein, die ein Benutzer eingegeben hat.
  1. So könnte eine mögliche Lösung aussehen:
/^\$[A-Za-z_]\w*$/
Das Dollarzeichen am Anfang des Musters muß mit einem Backslash geschützt werden, da wir hier ein tatsächliches Dollarzeichen meinen. Als nächstes muß ein Buchstabe oder ein Unterstrich folgen, danach eine beliebige Anzahl von »Wort«-Zeichen (Buchstaben, Ziffern und Unterstriche).
  1. Dieses Muster ist erstaunlich schwierig zu schreiben. Daher zeigen wir die Vorgehensweise Schritt für Schritt.
Wir beginnen damit, daß wir ein Wort finden wollen, also benutzen wir anfangs das Muster /\w+/. Außerdem wollen wir das Gefundene später noch einmal benutzen. Daher fügen wir ein paar runde Klammern hinzu: /(\w+)/. Außerdem wollen wir einen Treffer erzielen, wenn das Wort mehr als einmal vorkommt. Dies erreichen wir mit einer Rückwärtsreferenz: /(\w+)\1+/ (das Pluszeichen am Ende bedeutet, daß das in der Rückwärtsreferenz stehende Wort einmal oder mehrmals vorkommen darf.)
Aber wir sind noch nicht fertig. Als nächstes müssen wir dafür sorgen, daß zwischen den Wörtern auch Leerraum-Zeichen (Whitespace) stehen können. Diese sollen nicht gespeichert werden (da sie unterschiedlich oft vorkommen können), also schreiben wir sie außerhalb der runden Klammern: /(\w+)\s\1+/. Da hier eine beliebige Anzahl von Leerraum-Zeichen stehen kann, aber mindestens eins vorhanden sein muß, brauchen wir noch ein Pluszeichen. Unser Suchmuster sieht jetzt also so aus: /(\w+)\s+\1+/.
Das ist aber immer noch nicht richtig. Das letzte Pluszeichen bezieht sich nur auf die Rückwärtsreferenz (in der das wiederholte Wort steht). Wir müssen das Pluszeichen aber auf die Rückwärtsreferenz und das Leerraum-Zeichen zusammen anwenden und bekommen das Muster /(\w+)(\s+\1)+/. Jetzt können wir auch mehr als zweimal vorkommende Wörter finden. Am Anfang findet der Teil im ersten Klammernpaar das erste Vorkommen eines Wortes, danach kann der Teil im zweiten Klammernpaar weitere Vorkommen des gleichen Wortes ermitteln. Jetzt ist es an der Zeit, das Muster auszuprobieren, und tatsächlich werden alle Tests mit unseren Beispielstrings erfolgreich bestanden. Es ist also an der Zeit, das Muster in ein wirkliches Programm einzubauen und das nächste Projekt anzugehen.
Eine Woche später bekommen wir jedoch eine Nachricht, daß unser Programm offenbar doch einen Fehler haben muß. Das Programm meldet bei dem Satz Wir geben eben alles einen Treffer, obwohl es offensichtlich keine doppelten Wörter gibt. Sofort schmeißen wir unser Testprogramm an,18 um zu sehen, ob die Behauptung stimmt - und tatsächlich: |Wir g<eben eben> alles| enthält das doppelte Wort eben, versteckt in einem anderen String.
Dies ist eine klassische Aufgabe für den Wortgrenzen-Anker. Es kann nicht sein, daß ein Wort mitten in einem anderen Wort anfängt. Also fügen wir den verbesserten Mustervergleich /\b(\w+)(\s+\1)+/ in das Programm ein und lehnen uns zurück - sicher, daß diesmal alles in Ordnung ist.
Doch gerade, als Sie mit dem neuen Projekt beginen wollen, kommt eine weitere Nachricht herein. Diesmal wurde das doppelte Wort Bei in Bei Beispielen gefunden. Wir brauchen also auch am Ende des Musters noch eine Wortgrenze, damit dieser Wortteil nicht mehr gefunden wird. Hieraus ergibt sich das Muster /\b(\w+)(\s+\1)+\b/, und endlich funktioniert das Muster tatsächlich, wie es verlangt war.
Was Sie gerade gelesen haben, ist eine wahre Geschichte. Der reguläre Ausdruck ist zwar ein anderer, aber die Fehlernachrichten sind echt. Dinge wie diese kommen öfter vor, als wir gern zugeben würden. Aber selbst, wenn Sie dieses Muster schon seit Jahren schreiben, kann es passieren, daß sich ein Fehler einschleicht. Sie können das Muster zwar mit einer Reihe von Testfällen ausprobieren, es in ein Programm einbauen, vielleicht sogar in einen Perl-Bestseller, nur um irgendwann festzustellen, daß es doch einen Bug enthält.
Die Moral von der Geschicht' ist, daß reguläre Ausdrücke eine besondere Herausforderung darstellen können. Wenn Sie es mit dem Lernen von regulären Ausdrükken ernst meinen (und das sollten alle Perl-Programmierer), möchten wir Ihnen noch einmal das Buch Reguläre Ausdrücke von Jeffrey Friedl (O'Reilly) empfehlen.

Lösungen zu den Übungen in Kapitel 9

  1. So könnte eine mögliche Lösung aussehen:
/($was){3}/
Ist $was einmal interpoliert, haben wir ein Muster wie /(fred|barney){3}/. Ohne die Klammern sähe das Muster so aus: /fred|barney{3}/, was das gleiche bedeutet wie /fred|barneyyy/. Die runden Klammern müssen also auf jeden Fall benutzt werden.
  1. So könnte eine mögliche Lösung aussehen:
@ARGV = '/pfad/zu/perlfunc.pod'; # oder auf der Kommandozeile angegeben

while (<>) {
if (/^=item\s+([a-z_]\w*)/i) {
print "$1\n"; # Identifier-Namen ausgeben
}
}
Nach dem bisher Gezeigten besteht die einzige Möglichkeit, eine Datei zu öffnen, darin, den Diamantoperator zu verwenden (oder vielleicht eine Eingabe-Umleitung zu benutzen). Daher schreiben wir den Pfad zu perlfunc.pod in das Spezial-Array @ARGV.
Das Herz dieses Programms ist das Suchmuster, das in einer =item-Zeile nach Identifiernamen sucht. Die Aufgabenstellung war in diesem Fall etwas zweideutig, da wir nicht gesagt haben, ob =item auch Großbuchstaben enthalten kann. Der Autor des obenstehenden Musters hat sich daher entschieden, den /i-Modifier zu benutzen, um eine Unterscheidung zwischen Groß- und Kleinschreibung zu vermeiden. Wenn Sie die Aufgabe anders verstanden haben, wäre auch das Muster /^=item\s+([a-zA-Z_]\w*)/ richtig gewesen.
  1. So könnte eine mögliche Lösung aussehen:
@ARGV = '/pfad/zu/perlfunc.pod'; # oder auf der Kommandozeile angegeben

my %gesehen; # Hash deklarieren (optional)

while (<>) {
if (/^=item\s+([a-z_]\w*)/i) {
$gesehen{$1} += 1; # für jeden Identifier ein
# neues Element
}
}

foreach (sort keys %gesehen) {
if ($gesehen{$_} > 2) { # mehr als zweimal
print "$_ wurde $gesehen{$_} mal gefunden.\n";
}
}
Dieses Programm beginnt fast genauso wie das vorige, nur daß wir hier den Hash %gesehen vordeklarieren (falls use strict benutzt wird). Wir haben den Namen %gesehen gewählt, da dieser Hash uns sagt, welche Identifier-Namen wir bereits gesehen haben und wie oft. Dies ist eine häufige Verwendungsweise von Hashes. Anstatt in der ersten Schleife die Vorkommen jedes Identifiers gleich auszugeben, werden sie gezählt und in %gesehen abgelegt.
Als nächstes durchläuft die zweite Schleife die Schlüssel von %gesehen (die Identifier-Namen). Um dem Benutzer einen Gefallen zu tun, werden die Schlüssel auch gleich noch sortiert (auch wenn dies nicht Teil der Übung war), so daß die ausgegebene Liste übersichtlicher ist.
Auch wenn das nicht sehr offensichtlich erscheint, hat dieses Programm einen direkten Bezug zu tatsächlichen Programmierproblemen. Stellen Sie sich einmal vor, Sie wollten die 400 MB große Logdatei Ihres Webservers nach bestimmten Informationen durchsuchen. Keinesfalls werden Sie sich die Datei komplett ansehen wollen. Statt dessen schreiben Sie ein Programm, das die Informationen (mit Hilfe eines Suchmusters) aus der großen Datei herausfiltert und diese, gut lesbar formatiert, ausgibt. Mit Perl lassen sich Programme wie dieses ohne größeren Aufwand erstellen.

Lösungen zu den Übungen in Kapitel 10

  1. So könnte eine mögliche Lösung aussehen:
my $geheimzahl = int(1 + rand 100);
# Das Kommentarzeichen in der nächsten Zeile können
# Sie beim Debuggen weglassen.
# print "Nicht weitersagen. Die Geheimzahl ist $geheimzahl.\n";

while (1) {
print "Bitte geben Sie eine Zahl zwischen 1 und 100 ein: ";
chomp(my $versuch = <STDIN>);
if ($versuch =~ /quit|exit|^\s*$/i) {
print "Schade, dass Sie aufgeben. Die Geheimzahl war $geheimzahl.\n";
last;
} elsif ($versuch < $geheimzahl) {
print "Zu klein. Versuchen Sie es noch einmal!\n";
} elsif ($versuch == $geheimzahl) {
print "Richtig geraten. Herzlichen Glueckwunsch!\n";
last;
} else {
print "Zu gross. Versuchen Sie es noch einmal!\n";
}
}
Zu Beginn unseres Programms wählen wir eine geheime Zahl zwischen 1 und 100 aus. Hierfür müssen Sie wissen, daß die Perl-Funktion für Zufallszahlen rand heißt. Die Anweisung rand 100 erzeugt also eine Zufallszahl zwischen 0 und 100 (ohne 100 selbst mit einzubeziehen). Die größte mögliche Zahl, die dieser Ausdruck erzeugen kann, ist also etwas wie 99.999.19 Zählen wir also zu dem von rand erzeugten Wert eins hinzu, bekommen wir einen Bereich zwischen 1 und 100.999. Als nächstes verkürzt die Funktion int das Ergebnis auf den dazugehörigen ganzzahligen Wert, den wir nun als Geheimzahl benutzen können.
Die auskommentierte Zeile kann Ihnen während der Entwicklung und beim Debugging behilflich sein, oder Sie können einfach schummeln. Der Hauptteil des Programms ist eine endlose while-Schleife, die den Benutzer so lange raten läßt, bis die last-Anweisung ausgeführt wird.
Es ist wichtig, daß wir noch vor den Zahlen auf einen möglichen String testen. Können Sie erkennen, was passieren würde, wenn der Benutzer quit eingibt? Die Eingabe würde als Zahl interpretiert werden (was bei eingeschalteten Warnungen für einen entsprechenden Hinwes gesorgt hätte). Da der String als Zahl interpretiert den Wert Null hätte, bekäme der arme Benutzer nun die Meldung angezeigt, daß seine Eingabe zu klein war. In diesem Fall würde der String-Test in unserem Programm unter Umständen niemals ausgeführt.
Eine weitere Möglichkeit, eine Endlosschleife zu erzeugen, bestünde darin, einen nackten Block mit einer redo-Anweisung zu versehen. Dies ist nicht effizienter oder langsamer, sondern nur eine andere Möglichkeit, das gleiche zu tun. Wenn Sie mehrere Schleifendurchläufe zu erwarten haben, ist es in der Regel besser, die Anweisung auch als Schleife zu formulieren. Sollen die Anweisungen aber nur in Ausnahmefällen wiederholt werden, ist die Verwendung eines nackten Blocks angemessener.

Lösungen zu den Übungen in Kapitel 11

  1. So könnte eine mögliche Lösung aussehen:
sub zeile_holen {
# Benutzer zur Eingabe auffordern, Newline-Zeichen am Ende
# entfernen und Zeile zurückgeben.
print $_[0];
chomp(my $zeile = <STDIN>);
$zeile;
}

my $quelle = &zeile_holen("Welche Quelldatei? ");
open REIN, $quelle
or die "Kann '$quelle' nicht zum Lesen oeffnen: $!";

my $ziel = &zeile_holen("Welche Zieldatei? ");
die "Datei existiert bereits. Die ueberschreibe _ich_ nicht!"
if -e $ziel; # optionaler Sicherheitstest
open RAUS, ">$ziel"
or die "Kann '$ziel' nicht schreiben: $!";

my $muster = &zeile_holen("Suchmuster: ");
my $ersetzung = &zeile_holen("Ersetzungsstring: ");

while (<REIN>) {
s/$muster/$ersetzung/g;
print RAUS $_;
}
In diesem Programm müssen wir mehrmals mit dem Benutzer kommunizieren. Daher haben wir uns entschieden, eine Subroutine zu benutzen, die einen Teil dieser Arbeit erledigt. Die Subroutine gibt eine Eingabeaufforderung an den Benutzer aus, die wir der Routine als ersten (und einzigen) Parameter übergeben. Als nächstes wird eine Eingabezeile eingelesen, das Newline-Zeichen am Ende entfernt und die Zeile schließlich zurückgegeben. Dadurch ist es einfach, einen Parameter für unser Programm nach dem anderen einzulesen.
Nachdem wir wissen, welche Quelldatei benutzt werden soll, versuchen wir diese zu öffnen. Eine frühere Version dieses Programms hat zuerst alle Parameter abgefragt. Kann aber schon die Quelldatei nicht geöffnet werden, ist es sinnlos, den Benutzer nach weiteren Informationen zu fragen. Daher lesen wir die Parameter in dieser Version erst ein, wenn sie wirklich gebraucht werden. Wie Sie sehen, haben wir in der die-Nachricht den Dateinamen in Anführungsstriche gestellt. Dies soll ein Auffinden von Tippfehlern bei der Pfadangabe erleichtern, etwa wenn versehentlich Whitespace-Zeichen mit angegeben wurden, die im Dateinamen nicht vorkommen. Haben Sie die Quelldatei anstelle von $quelle als "<$quelle" geöffnet, ist das auch in Ordnung. (Es gibt keinen Anlaß, sich darüber Sorgen zu machen, ob der Benutzer dieses Programms irgendwelche schlimmen Dinge damit anstellt. Alles, was dieses Programm tut, läßt sich nämlich auch auf andere Art bewerkstelligen. Ganz anders sieht es aus, wenn das Programm über das Web ausgeführt werden soll. Hier müssen wir beispielsweise wesentlich vorsichtiger sein, welche Datei der Benutzer zu öffnen versucht.)
Wir hoffen, Sie wissen bereits, wie einfach es ist, versehentlich eine existierende Datei zu überschreiben, indem wir sie zum Schreiben öffnen. Zur Sicherheit führen wir daher einen -e-Test durch, um zu sehen, ob es die Datei womöglich schon gibt. Die dazugehörige die-Nachricht enthält keine $!-Variable, da wir hier keine Meldung über einen fehlgeschlagenen Systemaufruf ausgeben wollen. In diesem Programm ist der -e-Test durchaus sinnvoll. In einer Umgebung, in der mehrere Kopien dieses Programms gleichzeitig laufen sollen (oder verschiedene Programme auf die gleiche Datei zugreifen), wäre er jedoch nicht angemessen. Diese Situation tritt beispielsweise bei Programmen auf, die über das Web ausgeführt werden: Zwei Prozesse versuchen mehr oder weniger gleichzeitig zu ermitteln, ob eine Datei existiert, und beide sehen, daß die Datei nicht existiert. Daraufhin erzeugt einer der Prozesse eine neue Datei, die kurz darauf vom anderen wieder überschrieben wird. Dieses Konkurrenzproblem läßt sich nicht mit einem -e-Test lösen. Statt dessen brauchen wir hier eine Möglichkeit, die Datei zu sperren. (Dieses Thema würde den Rahmen dieses Buches jedoch sprengen.)
Durch den Sicherheitstest ist es nicht mehr möglich, eine Datei versehentlich zu löschen. Ob das wirklich so eine gute Idee war, ist nur schwer zu sagen. Kommt der Benutzer nächste Woche zu Ihnen und sagt »Danke, daß Sie den Test eingebaut haben. Fast hätte ich meine wertvolle Datei überschrieben«, war es eine gute Idee. Sagt der Benutzer statt dessen etwas wie »Grmblfx! Ihr Programm ist aber schwer zu benutzen. Ich habe einen Dateinamen für die Ausgabedatei angegeben, aber Ihr Programm läßt mich nicht in die Datei schreiben, bevor ich sie nicht gelöscht habe!«, war der Test offenbar keine so gute Idee. Diese Entscheidungen sind oft der schwerste Teil der Programmierarbeit. Eventuell wollen Sie das Programm ändern, so daß es statt dessen vor dem Überschreiben fragt: »Wollen Sie die Datei `barney' wirklich überschreiben?« Dies könnte das Standardverhalten sein, während es für den Power-User eine Kommandozeilen-Option gibt, mit der Dateien ohne weiteres Nachfragen überschrieben werden. In der nächsten Version vielleicht.
Nachdem wir sämtliche notwendigen Informationen eingeholt haben und alle Dateien korrekt geöffnet sind, ist der Rest des Programms recht einfach. Das Herz des Programms ist die Schleife am Schluß. Hiermit lesen wir die Zeilen aus der Quelldatei, führen gegebenenfalls einige Änderungen durch und schreiben sie dann in die Zieldatei. Beachten Sie, daß die Ersetzungsfunktion hier die /g-Option benutzt. Haben Sie diese in Ihrem Programm nicht benutzt, funktioniert es nicht richtig. Die Aufgabe lautete, jedes Vorkommen des Suchmusters zu ersetzen, nicht nur das erste auf jeder Zeile.
Konnten Sie im Suchmuster Metazeichen für reguläre Ausdrücke benutzen? Sicher! In der Ersetzung wird $muster interpoliert, um das Suchmuster zu erzeugen. Konnten Sie im Ersetzungsstring Speichervariablen und Backslash-Escapes benutzen? Nein. $ersetzung wird zwar interpoliert, um den Ersetzungsstring zu erzeugen; um aber irgendwelche magischen Zeichen zu benutzen, müßte dessen Inhalt nun ein weiteres Mal interpoliert werden. Enthält $ersetzung zum Beispiel $1, so wird dies in der Ersetzung einfach als ein Dollarzeichen, gefolgt von einer Eins angesehen. Wenn Perl jedesmal so oft interpolieren würde, bis jedes Dollar- oder andere Sonderzeichen aufgelöst ist, wäre es nicht mehr möglich, literale Dollarzeichen oder Backslashes im Ersetzungsstring zu benutzen. (Brauchen Sie aber tatsächlich einmal eine weitere Interpolationsebene, so ist auch das möglich. Nähere Informationen hierzu finden Sie in der perlfaq-Manpage.)
  1. So könnte eine mögliche Lösung aussehen:
foreach my $datei (@ARGV) {
my $attrib = &attribute_ermitteln($datei);
print "'$datei' $attrib.\n";
}

sub attribute_ermitteln {
# Merkmale für die angegebene Datei ermitteln
my $datei = shift @_;
return "existiert nicht" unless -e $datei;

my @attribute;
push @attribute, "lesbar" if -r $datei;
push @attribute, "schreibbar" if -w $datei;
push @attribute, "ausfuehrbar" if -x $datei;
return "existiert" unless @attribute;
'ist ' . join " und ", @attribute; # Rückgabewert
}
Auch hier ist es wieder bequem, eine Subroutine zu benutzen. Die Hauptschleife gibt für jede Datei eine Zeile aus, die die Merkmale enthält, etwa 'cereal-killer' ist ausfuehrbar oder 'sumselprunz' existiert nicht.
Die Subroutine ermittelt die Attribute für die angegebene Datei. Existiert die Datei gar nicht, hat es natürlich keinen Sinn, die Tests durchzuführen. Daher überprüfen wir dies zuerst. Gibt es keine Datei dieses Namens, wird die Subroutine vorzeitig beendet.
Existiert die Datei, erzeugen wir nun eine Liste ihrer Attribute. (Geben Sie sich ein paar Extrapunkte, wenn Sie hier das spezielle Dateihandle _ anstelle von $datei benutzt haben, damit nicht für jeden Test ein neuer Systemaufruf durchgeführt werden muß.) Es ist kein Problem, weitere Tests in die Subroutine einzubauen. Was passiert aber, wenn keiner der Tests erfolgreich ist? Wenn wir sonst nichts zu melden haben, können wir zumindest dem Benutzer mitteilen, daß die Datei existiert. Also tun wir das auch. Die unless-Klausel benutzt hierbei die Tatsache, daß @attribute wahr ist (im Booleschen Sinne), wenn es Elemente enthält.
Haben wir einige Attribute gefunden, hängen wir diese mit einem " und " aneinander und schreiben ein "ist " an den Anfang. Hierdurch bekommen wir einen beschreibenden String, wie zum Beispiel ist lesbar und schreibbar. Diese Methode ist aber nicht perfekt. Gibt es etwa drei Attribute, lautet der String nun ist lesbar und schreibbar und ausfuehrbar. Das sind zwar zu viele unds, aber wir können damit leben. Wollen Sie aber weitere Attribute hinzufügen, sollten Sie das Programm vermutlich dahingehend ändern, daß es Dinge ausgibt wie ist lesbar, schreibbar, ausfuehrbar und enthaelt etwas, sofern Ihnen das wichtig ist.
Beachten Sie: Wurden aus irgendeinem Grund keine Dateinamen auf der Kommandozeile angegeben, produziert das Programm auch keine Ausgaben. Das ergibt einen Sinn. Wenn Sie nach Informationen für keine Datei fragen, bekommen Sie auch keine Ausgaben. Aber vergleichen Sie dies mit dem Verhalten des Programms in der nächsten Übung.
  1. So könnte eine mögliche Lösung aussehen:
die "Kein Dateiname angegeben!\n" unless @ARGV;
my $aelteste_datei = shift @ARGV;
my $hoechstes_alter = -M $aelteste_datei;

foreach (@ARGV) {
my $alter = -M;
($aelteste_datei, $hoechstes_alter) = ($_, $alter)
if $alter > $hoechstes_alter;
}

printf "Die aelteste Datei ist %s mit einem Alter von %.1f Tagen.\n",
$aelteste_datei, $hoechstes_alter;
Dieses Programm fängt gleich damit an, daß es sich beklagt, sofern auf der Kommandozeile kein Dateiname angegeben wurde. Wir machen das, weil wir den Namen der ältesten Datei ermitteln wollen, und das geht nun einmal nur, wenn wir auch Dateien haben, die wir überprüfen können.
Wieder einmal benutzen wir einen »Hochwassermarken«-Algorithmus. Die erste Datei ist mit Sicherheit die älteste, die wir bisher gesehen haben. Auch ihr Alter müssen wir ermitteln, damit wir einen Wert für $hoechstes_alter haben.
Für alle weiteren Dateien ermitteln wir, genau wie bei der ersten Datei, das Alter mit dem -M-Test (nur daß wir hier die Standardvariable $_ für den Dateitest benutzen). In der Regel versteht man unter dem »Alter« einer Datei das letzte Änderungsdatum, auch wenn Sie im Prinzip einen anderen Test benutzen könnten. Ist das Alter der überprüften Datei höher als der Wert von $hoechstes_alter, benutzen wir eine Listenzuweisung, um den Namen und das Alter gemeinsam zu aktualisieren. Wir hätten hier auch mehrere skalare Zuweisungen benutzen können, aber die Listenzuweisung ist für das Aktualisieren mehrerer Variablen einfach praktischer.
Das mit -M ermittelte Alter speichern wir in der Variablen $alter. Was wäre wohl passiert, wenn wir statt dessen jedesmal einen neuen -M-Test durchgeführt hätten? Sofern wir nicht das spezielle Dateihandle _ benutzt hätten, wäre jedesmal eine neue Anfrage an das System ausgeführt worden - eine potentiell langsame Operation (auch wenn Sie das erst merken, wenn Sie Hunderte oder Tausende Dateien bearbeiten müßten und vielleicht selbst dann noch nicht). Viel wichtiger ist, daß wir überlegen, was passiert, wenn jemand eine Datei aktualisiert, während wir gerade den Test durchführen. In diesem Fall sehen wir das Alter einer Datei, die vielleicht das höchste Alter bis jetzt hat. Bevor wir nun -M ein zweites Mal ausführen können, aktualisiert nun jemand diese Datei, wodurch der Zeitstempel auf die gegenwärtige Zeit gesetzt wird. Plötzlich steht nun in $hoechstes_alter der niedrigste nur mögliche Wert. Das Resultat wäre, daß wir nur die älteste der Dateien finden könnten, die ab diesem Moment getestet werden, und die nicht die älteste aller Dateien. Dies ist ein Problem, das nur sehr schwer zu debuggen wäre.
Am Ende des Programms benutzen wir schließlich printf, um den Namen und das Alter der ältesten Datei auszugeben. Hierbei runden wir das Alter auf das nächste Zehntel eines Tages. Geben Sie sich ein paar Extrapunkte, wenn Sie sich die Arbeit gemacht haben, diesen Wert in Tage, Stunden und Minuten umzurechnen.

Lösungen zu den Übungen in Kapitel 12

  1. Eine Möglichkeit besteht darin, einen Glob zu benutzen:
print "Welches Verzeichnis? (Home-Verzeichnis = leere Zeile) ";
chomp(my $verz = <STDIN>);
if ($verz =~ /^\s*$/) { # eine Leerzeile
chdir or die "Kann nicht in Ihr Home-Verzeichnis wechseln: $!";
} else {
chdir $verz or die "chdir nach '$verz' nicht moeglich: $!";
}

my @dateien = <*>;
foreach (@dateien) {
print "$_\n";
}
Zuerst geben wir eine schlichte Eingabeaufforderung aus und lesen den gewünschten Verzeichnisnamen ein, wobei das Newline-Zeichen am Ende gegebenenfalls mit chomp entfernt wird. (Ohne chomp hätten wir unter Umständen versucht, in ein Verzeichnis zu wechseln, dessen Name am Ende ein Newline-Zeichen trägt. Unter Unix ist die Verwendung von Newline-Zeichen in Dateinamen erlaubt. Es kann daher nicht einfach davon ausgegangen werden, daß die Funktion chdir dieses Zeichen einfach ignoriert.)
Wurde der Name eines Verzeichnisses angegeben, versuchen wir nun in dieses Verzeichnis zu wechseln. Gibt es einen Fehler, wird das Programm abgebrochen. Wurde kein Name angegeben, versuchen wir statt dessen in das Home-Verzeichnis zu wechseln.
Ein Glob mit dem Sternchen sorgt schließlich dafür, daß alle Dateinamen des (neuen) Arbeitsverzeichnisses eingelesen werden. Diese werden automatisch alphabetisch sortiert und nacheinander ausgegeben.
  1. So könnte eine mögliche Lösung aussehen:
print "Welches Verzeichnis? (Home-Verzeichnis = leere Zeile) ";
chomp(my $verz = <STDIN>);
if ($verz =~ /^\s*$/) { # Leerzeile
chdir or die "Kann nicht in Ihr Home-Verzeichnis wechseln: $!";
} else {
chdir $verz or die "chdir nach '$verz' nicht moeglich: $!";
}

my @dateien = <.* *>; # .*-Dateien einbeziehen
foreach (sort @dateien) { # sortieren
print "$_\n";
}
Es gibt zwei Unterschiede zur vorigen Übung: Der Glob enthält zusätzlich das Muster »Punkt Sternchen«, wodurch jetzt auch Dateinamen gefunden werden, die mit einem Punkt beginnen. Zweitens müssen wir die Liste nun explizit sortieren, da die Dateinamen, die mit einem Punkt beginnen, in der Liste vor oder nach den anderen Namen stehen sollen.
  1. So könnte eine mögliche Lösung aussehen:
print "Welches Verzeichnis? (Home-Verzeichnis = leere Zeile) ";
chomp(my $verz = <STDIN>);
if ($verz =~ /^\s*$/) { # eine Leerzeile
chdir or die "Kann nicht in Ihr Home-Verzeichnis wechseln: $!";
} else {
chdir $verz or die "chdir nach '$verz' nicht moeglich: $!";
}

opendir PUNKT, "." or
die "Kann das gegenwaertige Verzeichnis nicht oeffnen: $!";
foreach (sort readdir PUNKT) {
# next if /^\./; # "Punkt"-Dateien ggf. ueberspringen
print "$_\n";
}
Auch hier haben wir wieder die gleiche Struktur, wie bei den beiden vorigen Programmen. Diesmal haben wir uns jedoch entschieden, ein Verzeichnishandle zu öffnen. Nachdem wir das gegenwärtige Arbeitsverzeichnis gewechselt haben, wollen wir es öffnen. Dafür soll das Verzeichnishandle PUNKT sorgen.
Warum ausgerechnet PUNKT? Weil der Benutzer auch relative Verzeichnisnamen wie fred eingeben kann und nicht nur absolute wie /etc. Während es bei den absoluten Namen keine Probleme mit dem Öffnen gibt, sieht das bei den relativen schon anders aus. Zuerst versuchen wir beispielsweise mittels chdir nach fred zu wechseln, um es dann mit opendir zu öffnen. Aber das würde fred in dem neuen Verzeichnis öffnen und nicht fred im ursprünglichen Verzeichnis. Der einzige Name, bei dem wir sicher sein können, daß er immer »das gegenwärtige Verzeichnis« bezeichnet, ist ».« (jedenfalls unter Unix-Systemen und deren Verwandten).
Die Funktion readdir gibt alle in dem Verzeichnis enthaltenen Dateinamen zurück. Diese werden nun sortiert und dann ausgegeben. Hätten wir die erste Übung auf diese Weise gelöst, wären die mit einem Punkt beginnenden Dateinamen übersprungen worden. Hierfür muß das Kommentarzeichen in der auskommentierten Zeile in der foreach-Schleife entfernt werden.
Jetzt fragen Sie sich vielleicht, warum wir denn dann überhaupt ein chdir durchgeführt haben. Schließlich lassen sich readdir und dessen Freunde nicht nur mit dem gegenwärtigen Verzeichnis benutzen. Der Hauptgrund besteht darin, daß wir dem Benutzer die Möglichkeit geben wollten, mit einem Tastendruck in sein Home-Verzeichnis zu wechseln. Diese Übung könnte der Anfang eines Hilfsprogramms zur Dateiverwaltung werden. Vielleicht bestünde der nächste Schritt darin, den Benutzer zu fragen, welche Dateien auf ein Backup-Band überspielt werden sollen.

Lösungen zu den Übungen in Kapitel 13

  1. So könnte eine mögliche Lösung aussehen:
unlink @ARGV;
...oder wenn Sie den Benutzer bei eventuellen Problemen warnen wollen:
foreach (@ARGV) {
unlink $_ or warn "Kann '$_' nicht loeschen: $!, Programm geht weiter.\n";
}
Hierbei gelangen alle auf der Kommandozeile übergebenen Argumente nacheinander in die Spezialvariable $_, die wir als Argument für unlink benutzen. Läuft etwas schief, gibt die Warnung darüber Aufschluß.
  1. So könnte eine mögliche Lösung aussehen:
use File::Basename;
use File::Spec;

my($quelle, $ziel) = @ARGV;

if (-d $ziel) {
my $basisname = basename $quelle;
$ziel = File::Spec->catfile($ziel, $basisname);
}

rename $quelle, $ziel
or die "Kann '$quelle' nicht in '$ziel' umbenennen: $!\n";
Das Arbeitspferd dieses Programms ist die letzte Anweisung. Die übrigen Anweisungen sind aber nötig, wenn es sich bei der Zielangabe um ein Verzeichnis handelt. Nach dem Laden der Module geben wir den übergebenen Kommandozeilen-Argumenten selbsterklärende Namen. Ist $ziel ein Verzeichnis, müssen wir den Basisnamen aus $quelle ermitteln und diesen dem Verzeichnis in $ziel hinzufügen. Nachdem $ziel die nötigen Informationen enthält, können wir nun rename benutzen, um unsere Aufgabe zu vollenden.
  1. So könnte eine mögliche Lösung aussehen:
use File::Basename;
use File::Spec;

my($quelle, $ziel) = @ARGV;

if (-d $ziel) {
my $basisname = basename $quelle;
$ziel = File::Spec->catfile($ziel, $basisname);
}

link $quelle, $ziel
or die "Harter Link zwischen '$quelle' und '$ziel' nicht moeglich: $!\n";
Im Tip zu dieser Aufgabe haben wir gesagt, dieses Programm habe sehr große Ähnlichkeit mit dem vorangehenden. Tatsächlich besteht der einzige Unterschied darin, daß wir anstelle von rename die Funktion link benutzen. Unterstützt Ihr System keine harten Links, haben Sie als letzte Zeile in Ihrem Programm vermutlich folgendes stehen:
print "Versucht, einen Link zwischen '$quelle' und '$ziel' anzulegen.\n";
  1. So könnte eine mögliche Lösung aussehen:
use File::Basename;
use File::Spec;

my $symlink = $ARGV[0] eq '-s';
shift @ARGV if $symlink;

my($quelle, $ziel) = @ARGV;

if (-d $ziel) {
my $basisname = basename $quelle;
$ziel = File::Spec->catfile($ziel, $basisname);
}

if ($symlink) {
symlink $quelle, $ziel
or die "Symlink zwischen '$quelle' und '$ziel' nicht moeglich: $!\n";
} else {
link $quelle, $ziel
or die "Harter Link zwischen '$quelle' und '$ziel' nicht moeglich: $!\n";
}
Die ersten paar Zeilen des Programms (nach den beiden use-Anweisungen) sehen sich das erste übergebene Kommandozeilen-Argument an. Ist dies »-s«, soll ein symbolischer Link angelegt werden, und wir vermerken dies mit einem wahren Wert in $symlink. Nachdem wir die »-s«-Option gesehen haben, müssen wir sie aus @ARGV entfernen, um an die übrigen Argumente zu gelangen. Das machen wir in der darunterstehenden Zeile. Die nun folgenden Zeilen haben wir wörtlich aus den früheren Übungen übernommen. Enthält $symlink einen wahren Wert, versuchen wir am Schluß des Programms einen symbolischen Link anzulegen. Ist der Wert von $symlink falsch, wird statt dessen versucht, einen harten Link anzulegen.
  1. So könnte eine mögliche Lösung aussehen:
foreach (<.* *>) {
my $ziel = readlink $_;
print "$_ -> $ziel\n" if defined $ziel;
}
Alle vom Glob zurückgegebenen Dateinamen sind nacheinander in $_ zu finden. Ist eines der Elemente ein symbolischer Link, gibt readlink einen definierten Wert zurück und die Dateien werden angezeigt. Ist die Bedingung nicht erfüllt, wird das Element übersprungen.

Lösungen zu den Übungen in Kapitel 14

  1. So könnte eine mögliche Lösung aussehen:
chdir "/" or die "Kann nicht ins Root-Verzeichnis wechseln: $!";
exec "ls", "-l" or die "exec ls nicht moeglich: $!";
Mit der ersten Zeile machen wir das Root-Verzeichnis zum gegenwärtigen Arbeitsverzeichnis. In diesem Beispiel ist der Wert hartcodiert. In der zweiten Zeile rufen wir die exec-Funktion mit mehreren Argumenten auf und geben das Ergebnis auf der Standardausgabe aus. Wir hätten exec hier auch in der Ein-Argument-Form aufrufen können, aber die gezeigte Methode tut keinem weh.
  1. So könnte eine mögliche Lösung aussehen:
open STDOUT, ">ls.out" or die "Kann ls.out nicht schreiben: $!";
open STDERR, ">ls.err" or die "Kann ls.err nicht schreiben: $!";
chdir "/" or die "Kann nicht ins Root-Verzeichnis wechseln: $!";
exec "ls", "-l" or die "exec ls nicht moeglich: $!";
In den ersten zwei Zeilen verbinden wir STDOUT und STDERR mit Dateien im gegenwärtigen Verzeichnis (bevor wir versuchen, das Verzeichnis zu wechseln). Nachdem wir den Wechsel vollzogen haben, wird ls ausgeführt, dessen Ausgabe wir in die zuvor angelegte Datei im ursprünglichen Verzeichnis schicken.
Und wo würde die Ausgabe des letzten die landen? Selbstverständlich in ls.err, da wir die Ausgaben von STDERR dorthin umgeleitet haben. Das gleiche würde mit der die-Nachricht der chdir-Anweisung geschehen. Kann die Umleitung von STDERR in der zweiten Zeile dagegen nicht vorgenommen werden, wird statt dessen das alte STDERR benutzt. Für alle drei Standarddateihandles (STDIN, STDOUT und STDERR) gilt: Kann eine Umleitung nicht vorgenommen werden, ist das ursprüngliche Dateihandle weiterhin offen.
  1. So könnte eine mögliche Lösung aussehen:
if (`date` =~ /^S/) {
print "Gehen Sie spielen!\n";
} else {
print "Gehen Sie arbeiten!\n";
}
Diese Möglichkeit funktioniert aus zwei Gründen. Zum einen gibt das date-Kommando als erstes den Wochentag aus. Zum einen beginnen Saturday und Sunday beide mit einem S. Daher müssen wir nur überprüfen, ob die Ausgabe mit einem S beginnt. Es gibt viele noch schwierigere Möglichkeiten, dieses Programm zu schreiben. Die meisten davon kennen wir aus unseren Kursen.
Hätten wir den obenstehenden Code in einem echten Programm benutzt, wäre vermutlich eher das Muster /^(Sat|Sun)/ zum Einsatz gekommen. Das ist zwar ein wenig ineffizienter, aber das ist ziemlich unerheblich. Außerdem ist diese Schreibweise für den Wartungsprogrammierer wesentlich leichter zu verstehen.

Lösungen zu den Übungen in Kapitel 15

  1. So könnte eine mögliche Lösung aussehen:
my @zahlen;
push @zahlen, split while <>;
foreach (sort { $a <=> $b } @zahlen) {
printf "%20g\n", $_;
}
Kommt Ihnen die zweite Zeile zu verwirrend vor? Tja, das haben wir mit Absicht gemacht. Auch wenn wir Ihnen empfehlen, klar verständlichen Code zu schreiben, gibt es Leute, die ihre Programme gern so schwer verständlich gestalten wie möglich.20 Daher möchten wir, daß Sie auf das Schlimmste vorbereitet sind. Irgendwann werden auch Sie einmal mit so verwirrendem Code wie dem obenstehenden arbeiten müssen.
Der while-Modifier in der betreffenden Zeile hat die gleiche Bedeutung wie die folgende Schreibweise:
while (<>) {
push @zahlen, split;
}
Das ist zwar schon besser, aber immer noch ein bißchen unklar. Die while-Schleife liest die Eingaben zeilenweise ein (abhängig von den Benutzerangaben, wie am Diamantoperator zu erkennen ist). Die Funktion split trennt einen String standardmäßig an Whitespace-Zeichen auf und gibt eine Liste von Wörtern zurück, beziehungsweise in unserem Fall eine Liste von Zahlen. Letztlich besteht die Eingabe aus einer Reihe von durch Whitespace-Zeichen getrennten Zahlen. Unabhängig von der verwendeten Schreibweise sorgt die while-Schleife also dafür, daß das Array @zahlen mit den Zahlen aus der Eingabe gefüllt wird.
Die foreach-Schleife übernimmt die sortierte Liste und gibt die einzelnen Werte unter Benutzung des numerischen Formats %20g in einer rechtsbündigen Spalte aus. Sie hätten auch das Format %20s benutzen können. Das ist allerdings ein Format für Strings, was bedeutet, daß die Strings in der Ausgabe nicht verändert worden wären. Ist Ihnen aufgefallen, daß die Beispieldaten sowohl 1.50 als auch 1.5 sowie 04 und 4 enthielten? Hätten Sie diese Zahlen als Strings ausgegeben, wären die zusätzlichen Nullen mit ausgegeben worden. %20g ist dagegen ein numerisches Format, was bedeutet, daß gleiche Zahlen in der Ausgabe auch gleich aussehend dargestellt werden. Je nachdem, was Sie machen wollen, können beide Formate korrekt sein.
  1. So könnte eine mögliche Lösung aussehen:
# Vergessen Sie nicht, den Hash %nachname einzubauen.
# Sie finden ihn entweder im Beispieltext oder in der
# heruntergeladenen Datei.

my @schluessel = sort {
"\L$nachname{$a}" cmp "\L$nachname{$b}" # nach Nachnamen
# sortieren
or
"\L$a" cmp "\L$b" # nach Vornamen sortieren
} keys %nachname;

foreach (@schluessel) {
print "$nachname{$_}, $_\n"; # Geroellheimer, Bam-Bam
}
Zu dieser Lösung gibt es nicht viel zu sagen. Wir bringen die Schlüssel in die richtige Reihenfolge und geben sie dann aus. Wir geben hier den Nachnamen zuerst aus, weil wir gerade Lust dazu haben. Die Aufgabenstellung überläßt das Ihnen.
  1. So könnte eine mögliche Lösung aussehen:
print "Bitte geben Sie einen String ein: ";
chomp(my $string = <STDIN>);

print "Bitte geben Sie einen Substring ein: ";
chomp(my $sub = <STDIN>);

my @positionen;

for (my $pos = -1; ; ) {
# Trickreiche Verwendung der dreiteiligen for-Schleife
$pos = index($string, $sub, $pos + 1); # nächste Position finden
last if $pos == -1;
push @positionen, $pos;
}

print "Fundorte von '$sub' in '$string' waren: @positionen\n";
Dieses Programm beginnt recht einfach, indem die vom Benutzer eingegebenen Strings eingelesen werden und ein Array deklariert wird, das die Fundorte des Substrings aufnehmen soll. Aber auch hier scheint der Code »auf Schlauheit optimiert« zu sein, was man eigentlich nur zum Spaß tun sollte, aber niemals in einem richtigen Programm. Dennoch wird hier eine gültige Technik gezeigt, die in einigen Fällen recht nützlich sein kann, daher wollen wir sie uns einmal näher ansehen:
Der Geltungsbereich der my-Variablen $pos ist auf die for-Schleife beschränkt und hat zu Beginn den Wert -1. Um Sie nicht länger auf die Folter zu spannen: Diese Variable enthält die Position des Substrings innerhalb des größeren Strings. Die Test- und Inkrementierungsteile der Schleife sind leer, wodurch wir eine Endlosschleife bekommen (aus der wir selbstverständlich irgendwann wieder mit last herausspringen).
Die erste Anweisung des Schleifenkörpers sucht nach einem Vorkommen des Substrings, der an der Position $pos +1 oder dahinter beginnt. Das bedeutet: Bei der ersten Iteration (in der $pos noch den Wert -1 hat) beginnt die Suche an Position 0, dem Anfang des Strings. Den Fundort des Substrings legen wir in $pos ab. War das -1, ist jetzt bereits die Arbeit der for-Schleife zu Ende, und wir steigen mit dem Aufruf von last aus. Ist der Wert jedoch nicht -1, legen wir die gefundene Position in @positionen ab und führen einen weiteren Schleifendurchlauf durch. Jetzt bedeutet $pos + 1 die Suche nach dem Substring direkt hinter dem Fundort des ersten Vorkommens. So bekommen wir nach und nach die gesuchten Antworten, und die Welt ist wieder glücklich und zufrieden.
Wollen Sie die trickreiche for-Schleife nicht benutzen, können Sie das gleiche Ergebnis folgendermaßen erreichen:
{
my $pos = -1;
while (1) {
... # der gleiche Schleifenkörper wie bei der for-Schleife
# im vorigen Beispiel
}
}
Der umgebende nackte Block beschränkt den Geltungsbereich von $pos. Das ist zwar nicht notwendig, oft aber eine gute Idee, um jede Variable im kleinstmöglichen Geltungsbereich zu deklarieren. Hierdurch sind weniger Variablen während des gesamten Programms »am Leben«, was die Gefahr verringert, daß Sie den Namen $pos versehentlich für einen anderen Zweck wiederverwenden. Aus dem gleichen Grund sollten Sie den Variablen, die einen größeren Geltungsbereich haben müssen, einen längeren Namen geben, wodurch die Wahrscheinlichkeit einer versehentlichen Wiederverwendung ebenfalls herabgesetzt wird. Vielleicht wäre in unserem Fall ein Name wie $substring_position nicht schlecht.
Wenn Sie dagegen versuchen wollten, Ihren Code so verwirrend wie möglich zu gestalten (Schande über Sie!), könnten Sie ein Monster wie das untenstehende erschaffen (Schande über uns!).
for (my $pos = -1; -1 !=
($pos = index
+$string,
+$sub,
+$pos
+1
);
push @positionen, ((((+$pos))))) {
'for ($pos != 1; # ;$pos++) {
print "Position $pos\n";#;';#' } pop @positionen;
}
Dieser noch trickreichere Code ersetzt die for-Schleife aus dem ersten Lösungsansatz. Inzwischen sollten Sie genug wissen, um diesen Code selbst zu entziffern, oder auch, um Ihren eigenen verworrenen Code zu schreiben, um Ihre Freunde damit zu verblüffen und Ihre Feinde zu verwirren. Stellen Sie sicher, diese Kräfte nur für Gutes, aber nie für Schlechtes zu benutzen.
Ach ja! Welche Ergebnisse haben Sie bei der Suche nach dem Substring d in Dies ist ein Test bekommen? Es gab keinen Treffer an Position 0, da das d klein geschrieben war und folglich nicht auf das D in Dies passen kann.

Lösungen zu den Übungen in Kapitel 16

  1. So könnte eine mögliche Lösung aussehen:
open PF, '/pfad/zu/perlfunc.pod'
or die "Kann perlfunc.pod nicht lesen: $!";
dbmopen my %DB, "pf_daten", 0644
or die "Kann DBM-Datei nicht anlegen: $!";

%DB = ( ); # ältere Daten gegebenenfalls löschen

while (<PF>) {
if (/^=item\s+([a-z_]\w*)/i) {
$DB{$1} = $DB{$1} || $.;
}
}

print "Fertig!\n";
Diese Übung ähnelt früheren Programmen, in denen mit perlfunc.pod gearbeitet wurde. Hier öffnen wir jedoch eine DBM-Datei mit dem Namen pf_daten, die mit dem DBM-Hash %DB verbunden ist. Für den Fall, daß die Datei noch Daten eines früheren Prgrammaufrufs enthält, weisen wir dem Hash eine leere Liste zu. Dies geschieht normalerweise eher selten, hier wollen wir aber, daß die komplette Datenbank gelöscht wird, falls ein früherer Programmaufruf ungültige und veraltete Daten hinterlassen hat. (Schließlich ändert sich perlfunc.pod mit jeder Perl-Version.)
Wird ein Identifier gefunden, müssen wir dessen Zeilennummer (zu finden in $.) in der Datenbank ablegen. Die dafür notwendige Operation benutzt den Short-Circuit-Operator || (logisches ODER) mit hoher Präzedenz. Enthält die Datenbank bereits einen Wert für dieses Element, ist die Auswertung der linken Seite des Ausdrucks wahr und der alte Wert wird weiterbenutzt. Gibt es aber noch keinen Eintrag für diesen Identifier in der Datenbank, wird der Wert auf der rechten Seite des Ausdrucks ($.) benutzt. Wir hätten diese Zeile folgendermaßen auch noch kürzer schreiben können:
$DB{$1} ||= $.;
Ist das Programm mit seiner Arbeit fertig, teilt es uns das mit. Das war in der Aufgabenstellung zwar nicht verlangt, hilft uns aber festzustellen, ob das Programm überhaupt etwas getan hat. Ohne diese Zeile hätte es überhaupt keine Ausgaben gegeben. Woher wollen wir aber wissen, ob das Programm auch richtig funktioniert hat? Aus der nächsten Übung.
  1. So könnte eine mögliche Lösung aussehen:
dbmopen my %DB, "pf_daten", undef
or die "Kann DBM-Datei nicht oeffnen: $!";
my $zeile = $DB{$ARGV[0]} || "nicht gefunden";

print "$ARGV[0]: $zeile\n";
Ist die Datenbank erst einmal angelegt, lassen sich die Informationen ganz einfach auslesen. Beachten Sie, daß wir in diesem Programm als drittes Argument für dbmopen den Wert undef benutzt haben. Dies ist nötig, da die Datei bereits existieren muß, um unser Programm richtig ausführen zu können.
Wurde für $ARGV[0] (den ersten auf der Kommandozeile angegebenen Parameter) kein Eintrag gefunden, geben wir statt des Fundortes die Nachricht nicht gefunden aus, indem wir das logische ODER hoher Präzedenz benutzen.
  1. So könnte eine mögliche Lösung aussehen:
dbmopen my %DB, "pf_daten", undef
or die "Kann DBM-Datei nicht oeffnen: $!";

if (my $zeile = $DB{$ARGV[0]}) {
exec 'less', "+$zeile", '/pfad/zu/perlfunc.pod'
or die "Kann Pager-Programm nicht ausfuehren: $!";
} else {
die "Unbekannter Eintrag: '$ARGV[0]'.\n";
}
Dieses Programm beginnt wie das davor, nur wird hier exec benutzt, um ein Pager-Programm aufzurufen. Geht das nicht, bricht das Programm mittels die die Ausführung ab.

Lösungen zu den Übungen in Kapitel 17

  1. So könnte eine mögliche Lösung aussehen:
my $dateiname = 'pfad/zu/sample_text';
open FILE, $dateiname
or die "Kann '$dateiname' nicht lesen: $!";
chomp(my @strings = <FILE>);

while (1) {
print "Bitte geben Sie ein Suchmuster ein: ";
chomp(my $muster = <STDIN>);
last if $muster =~ /^\s*$/;
my @treffer = eval {
grep /$muster/, @strings;
};
if ($@) {
print "Fehler: $@";
} else {
my $zaehler = @treffer;
print "Es wurden $zaehler passende Strings gefunden:\n",
map "$_\n", @treffer;
}
print "\n";
}
Hier benutzen wir einen eval-Block, um Fehler abzufangen, die bei der Benutzung der regulären Ausdrücke auftreten können. Innerhalb des Blocks benutzen wir grep, um die passenden Strings aus der Liste der Strings herauszufiltern.
Ist der eval-Block beendet, können wir eventuelle Fehlermeldungen ausgeben. Ist kein Fehler aufgetreten, geben wir jetzt die passenden Strings aus. Wie Sie sehen, haben wir map benutzt, um an die Strings vor der Ausgabe wieder ein Newline-Zeichen anzuhängen.

Fußnoten

1

Falls Sie nach einer formelleren Art von Konstanten suchen, sollten Sie sich einmal das constant-Pragma ansehen.

2

Dies wurde durch eine Gesetzesänderung im US-Staat Indiana tatsächlich einmal fast gemacht. Details finden Sie unter http://www.urbanlegends.com/legal/pi_indiana.html.

3

Wir haben die Leute von O'Reilly gefragt, ob sie nicht zusätzlich Geld ausgeben wollten, um die Einfügemarke mit blinkender Tinte zu drucken. Der Vorschlag wurde leider abgelehnt.

4

Mampfen (chomping) ist wie kauen - es ist nicht immer nötig, tut meistens aber auch keinem weh.

5

Jedenfalls nicht ohne einige fortgeschrittene Tricks anzuwenden. Es ist aber selten, daß sich etwas in Perl überhaupt nicht erledigen läßt.

6

Zumindest in einigen Versionen von Perl sorgt die Kurzform dafür, daß keine Warnung über die Benutzung eines undefinierten Werts ausgegeben wird. Verwenden Sie den Autoinkrement-Operator, ++, gibt es ebenfalls keine Warnung, auch wenn Sie diesen bis jetzt noch nicht kennen.

7

Oder Larry, falls der neben Ihnen steht.

8

Es sei denn, Larry sagt Ihnen, das nicht zu tun.

9

Wie Larry Ihnen vermutlich inzwischen erklärt hat.

10

Funktioniert das Testprogramm nicht richtig, liegt das vermutlich daran, daß Sie es nicht, wie empfohlen, heruntergeladen, sondern einfach abgetippt haben. Und Sie haben Ihr Getipptes offenbar auch nicht, wie ebenfalls empfohlen, vor der Benutzung getestet. Das heißt, Sie haben vermutlich auch die Übung nicht absolviert, sondern schlagen hier einfach nur die Antwort am Ende des Buches nach. Das Testprogramm (das Sie nicht benutzt haben) hat demnach problemlos funktioniert, was bedeutet, daß diese Fußnote eigentlich nutzlos ist.

11

Um sicherzugehen: Das Muster paßt vermutlich auf verschiedene Teile des Teststrings. Jeder String, der von dem Muster /a+b*/ gefunden wird, erzeugt auch mit /a+/ einen Treffer und umgekehrt.

12

Immer wenn Sie in Perl einen literalen Backslash meinen, müssen Sie zwei eingeben. Ein einzelner Backslash heißt immer, daß Sie etwas Magisches zu tun versuchen. Ein zweiter Backslash hebt diese Magie wieder auf und läßt Sie den Backslash wie ein beliebiges anderes Zeichen benutzen.

13

Schämen Sie sich, wenn Sie einfach die Unterscheidung zwischen Groß- und Kleinschreibung für das gesamte Muster ausgeschaltet haben. Das haben wir bis jetzt noch nicht gelernt. Ein solches Muster würde zudem auch WILMA finden, was nach der Aufgabenstellung nicht sein sollte.

14

Jemand, der bereits den logischen UND-Operator kennt, hätte die Tests auf /fred/ und /wilma/ im gleichen Bedingungsblock durchführen können. Diese Methode ist nicht nur effizienter und besser skalierbar als die hier gezeigte Methode, sondern auch insgesamt einfach besser. Da diese Methode jedoch erst in Kapitel 10 vorgestellt wird, können wir sie hier noch nicht anwenden.

15

Es gibt ein paar trickreiche und fortgeschrittene Methoden, etwas zu erreichen, das landläufig als »und«-Operation bezeichnet wird. Diese sind aber vermutlich nicht so effizient wie das logische UND von Perl. Das hängt ganz davon ab, welche Optimierungen Perl und seine Regex-Maschine durchführen können.

16

Wenn Sie besonders viel Pech haben, ist das Ihr eigenes Programm, das Sie gerade vor zehn Minuten erst geschrieben haben.

17

Es ist Ihnen vielleicht bekannt, daß Datei- und Verzeichnisnamen, die mit einem Punkt beginnen, auf Unix-Systemen standardmäßig nicht angezeigt werden, und daß der spezielle Verzeichnisname .. für das höher liegende Verzeichnis steht.

18

Wir haben keinen Witz gemacht, als wir sagten, das Programm kann Ihnen auch später noch von Nutzen sein.

19

Der größtmögliche Wert hängt hierbei von Ihrem System ab. Wenn Sie wirklich mehr wissen müssen, finden Sie mehr Informationen, als Sie vermutlich brauchen, unter http://www.cpan.org/doc/FMTEYEWTK/random.

20

Normalerweise empfehlen wir, in einem richtigen Programm keinen verwirrenden Code zu benutzen. Es kann aber ein nettes Spiel sein, verwirrenden Code zu schreiben. Es kann aber auch lehrreich sein, die verworrenen Beispiele anderer Leute zu studieren und ein oder zwei Wochenenden damit zu verbringen herauszufinden, was dieser Code tut. Wenn Sie ein paar interessante Codeschnipsel sehen wollen und vielleicht etwas Hilfe bei deren Interpretation brauchen, fragen Sie beim nächsten Treffen der Perl Mongers. Oder suchen Sie im Web nach JAPHs. Sie können aber auch ausprobieren, wie gut Sie das Beispiel am Ende der Lösungen zu diesem Kapitel entziffern können.


TOC PREV Übungen NEXT INDEX

Copyright © 2002 by O'Reilly Verlag GmbH & Co.KG