SELFHTML: Perl / Perl-Sprachelemente / Schleifen
Author: Redaktion SELFHTML, selfhtml81@selfhtml.org
Description: Wie Sie in Perl das Ausführen von Anweisungen mit Hilfe von Bedingungen wiederholen können.
Keywords: SELFHTML, Perl, Schleifen, for, foreach, while, do until, do while, Traversion, Rekursion
Date: 2005-11-09T00:29:27+01:00

| # Schleifen
| for-Schleifen foreach-Schleifen while-Schleifen do-Schleifen Schleifen bei Hashes Rekursion

## for-Schleifen

Solche Schleifen eignen sich vor allem für Fälle, in denen es einen Anfangswert, einen Endwert und einen Iterationswert gibt, also beispielsweise "Jede Zahl zwischen 1 und 100".

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";

    for(my $i = 1; $i <= 100; $i++) {
     print "<span style=\"font-size:$i\".\"pt\">$i pt</span><br>\n";
    }

    print "</body></html>\n";

### Erläuterung:

Das Script gibt einen Text insgesamt 100 mal aus. Dazu werden hinter dem Schlüsselwort `for`, das eine `for`-Schleife einleitet, in Klammern insgesamt drei kleine Anweisungen notiert. Die erste Anweisung deklariert eine Zählervariable `$i` und initiiert sie mit dem Wert 1. Die zweite Anweisung ist eine Bedingung. Sie lautet: "`$i` kleiner gleich 100". Die dritte Anweisung zählt zum aktuellen Wert von `$i` 1 dazu. Die Schleife wird nun so oft durchlaufen, wie die Bedingung in der Mitte wahr ist. Da `$i` im Beispiel zunächst den Wert 1 hat und dann bei jedem Schleifendurchlauf wegen `$i++` um 1 erhöht wird, wird die Schleife insgesamt 100 mal durchlaufen. Beim 101. mal ist `$i` höher als 100, die Bedingung ist nicht mehr wahr, und die Schleife wird beendet.

Im Anschluss an das `for`-Konstrukt folgt ein Anweisungsblock, markiert wie üblich durch geschweifte Klammern `{` und `}`. Dazwischen können beliebig viele Anweisungen stehen. Diese Anweisungen werden so oft ausgeführt, wie die Schleife durchlaufen wird, im Beispiel also 100 mal. Im Beispiel wird mit `print` HTML-Code erzeugt. Dieser enthält in einem `span`-Element eine CSS-Formatdefinition zur Schriftgröße (`font-size`). Bei der Zuweisung an diese CSS-Eigenschaft wird der Skalar `$i` verwendet. Das bewirkt im Beispiel, dass der Text 100 mal ausgegeben wird, und zwar jedesmal mit einer etwas größeren Schrift. Die erste ausgegebene Zeile ist nur 1pt, also ein Punkt groß, was wohl kaum jemand wird lesen können. Jede ausgegebene Zeile wird aber um einen Punkt größer, und die letzte Zeile ist mit 100pt Größe schon recht fensterfüllend.

Um Bedingungen wie die in der zweiten Anweisung im Konstrukt der `for`-Schleife zu formulieren, brauchen Sie entweder zwei Werte, die Sie vergleichen möchten, oder Sie fragen direkt, ob ein in den Klammern stehender Ausdruck wahr oder falsch ist. Im Beispiel werden in der Bedingung zwei Werte verglichen, nämlich der Wert von `$i` mit der Zahl 100. Dazu brauchen Sie Vergleichsoperatoren wie im Beispiel den Kleiner-Als-Operator `<`.

## foreach-Schleifen

Diese Sorte Schleifen ist in Perl speziell für das Durchlaufen von Listen und Arrays gedacht. Eine Liste wird dabei Element für Element abgeklappert. Abhängig davon können Sie Anweisungen ausführen.

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";

    my @Sachen = ("mein Haus","mein Auto","mein Boot");
    foreach (@Sachen) {
     print "$_<br>\n";
    }

    my @Schwaechen = ("Nikotin","Alkohol","das andere Geschlecht");
    my $Schwaeche;
    foreach $Schwaeche (@Schwaechen) {
     print "$Schwaeche<br>\n";
    }

    print "</body></html>\n";

### Erläuterung:

Das Beispiel zeigt zwei leicht abweichende Varianten, mit einer `foreach`-Schleife umzugehen. In beiden Fällen wird jeweils ein Array deklariert und mit Anfangswerten versehen, einmal `@Sachen` und einmal `@Schwaechen`. Hinter dem Schlüsselwort `foreach` wird in Klammern einfach der Array angegeben. In dem Anweisungsblock, der dahinter in geschweiften Klammern folgt, können beliebig viele Anweisungen stehen.

Im ersten der obigen Beispiele wird von der vordefinierten Variablen `$_` Gebrauch gemacht. In ihr ist im Anweisungsblock einer `foreach`-Schleife stets der aktuelle Wert des Schleifendurchlaufs gespeichert, was in diesem Fall das jeweils aktuelle Element des Arrays `@Sachen` ist.

Im zweiten Beispiel wird anstelle von `$_` ein eigener Skalar namens `$Schwaeche` benutzt. Wenn ein solcher Skalar zwischen dem Schlüsselwort `foreach` und der Klammer mit dem Array notiert wird, ist im Anweisungsblock in diesem Skalar jeweils der aktuelle Wert des Schleifendurchlaufs enthalten, im zweiten Beispiel also der jeweils aktuelle Wert aus `@Schwaechen`.

### Beachten Sie:

Die Schlüsselwörter `for` und `foreach` besitzen zwar jeweils einen semantisch anderen Hintergrund, sind aber syntaktisch beliebig gegeneinander austauschbar. Perl erkennt selbständig, was für einen Typ Schleife Sie verwenden wollen. So können Sie beispielsweise auch folgendes schreiben:
`for(1..1000) {
print "tausendmal berührt\n";
}`
Der Code gibt einfach tausendmal den Text aus, ist aber im Grunde eine `foreach`-Schleife, welche die Liste der Zahlen von 1 bis 1000 abarbeitet.

## while-Schleifen

Diese Art von Schleifen eignet sich, wenn Sie vorher nicht wissen, wie oft die Schleife durchlaufen wird. Sie formulieren einfach eine Bedingung, und die Schleife wird so oft durchlaufen, wie die Bedingung wahr ist. Dass die Bedingung irgendwann falsch und die Schleife beendet wird, dafür müssen Sie im Anweisungsblock, der von der Schleife abhängig ist, selber sorgen.

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";

    my $Startzeit = time();
    my $Endzeit = $Startzeit + 1;
    my $Jetztzeit = 0;
    my $i = 0;

    while ($Jetztzeit <= $Endzeit) {
     $Jetztzeit = time();
     $i++;
    }
    print "$i mal durchlaufen<br>";

    print "</body></html>\n";

### Erläuterung:

Das Script ermittelt zunächst mit der Perl-Funktion time den aktuellen Zeitpunkt und speichert das Ergebnis im Skalar `$Startzeit`. Gespeichert wird dabei eine Zahl, nämlich die Anzahl Sekunden vom 1.1.1970, 0.00 Uhr bis zum aktuellen Zeitpunkt. Dann wird ein Skalar `$Endzeit` deklariert, der einen Wert zugewiesen bekommt, der um `1` höher ist als der von `$Startzeit`. Zwei weitere Skalare `$Jetztzeit` und `$i` werden deklariert und mit `0` initialisiert.

Die Schleife wird durch das Schlüsselwort `while` eingeleitet. In Klammern wird eine Bedingung formuliert. Im Beispiel wird die Bedingung "`$Jetztzeit` kleiner oder gleich `$Endzeit`" formuliert. Hinter der Bedingung folgt in geschweiften Klammern ein Anweisungsblock mit beliebig vielen Anweisungen. Ausgeführt werden diese Anweisungen so oft, wie die Schleife durchlaufen wird und die Bedingung noch wahr ist.

Die Schleifenbedingung ist im Beispiel ja zunächst auf jeden Fall wahr, da `$Jetztzeit` mit `0` initialisiert wurde und daher auf jeden Fall kleiner ist als `$Endzeit`. Innerhalb der Schleife bekommt `$Jetztzeit` jedoch durch Aufrufen der Funktion `time` einen neuen Wert zugewiesen, der logischerweise mindestens so hoch ist wie der von `$Startzeit`. Die Schleife wird dadurch so lange durchlaufen, bis `$Jetztzeit` durch den `time`-Aufruf einen Wert zugewiesen bekommt, der größer ist als `$Endzeit`. Dann wird die Schleife beendet. Wie oft das der Fall ist, wissen Sie natürlich vorher nicht, insofern ist die `while`-Schleife hier ideal.

Innerhalb der Schleife wird außerdem noch `$i` als Zählervariable mit `$i++` jeweils um 1 erhöht. Der aktuelle Wert von `$i` wird nach Ablauf der Schleife ausgegeben. Im Fenster des aufrufenden Browsers wird man also am Ende sehen können, wie oft die Schleife durchlaufen wurde.

## do-Schleifen

Bei `while`-Schleifen kann es passieren, dass die abhängigen Anweisungen nie ausgeführt werden, nämlich dann, wenn die Schleifenbedingung schon beim ersten Schleifendurchlauf unwahr ist. Eine `do`-Schleife sorgt dafür, dass die Anweisungen auf jeden Fall einmal ausgeführt werden, da die Bedingung der Schleife erst am Ende abgeprüft wird.

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";

    my $Bedingung = "Abbruch";
    my $Irgendwas;
    do {
     $Irgendwas = $Bedingung;
     print "Hier steht $Irgendwas";
    } until ($Irgendwas eq $Bedingung);

    print "</body></html>\n";

### Erläuterung:

Das Beispiel demonstriert die typische Funktionsweise einer solchen Schleife. Zunächst wird ein Skalar `$Bedingung` mit dem Anfangswert `Abbruch` versehen. Ein weiterer Skalar namens `$Irgendwas` wird deklariert, erhält aber keinen Wert. Die Schleife wird mit `do` eingeleitet. Dahinter folgt in geschweiften Klammern ein Anweisungsblock, der beliebig viele Anweisungen enthalten kann. Im Beispiel wird dem Skalar `$Irgendwas` gleich zu Beginn der Wert von `$Bedingung` zugewiesen, also `Abbruch`. Anschließend wird dieser Inhalt zur Kontrolle ausgegeben. Nach der schließenden geschweiften Klammer, die den Anweisungsblock beendet, ist das Wort `until` notiert und dahinter in Klammern die eigentliche Schleifenbedingung. Im Beispiel wird abgeprüft, ob `$Irgendwas` und `$Bedingung` gleich sind, also den gleichen Inhalt haben. Da dies ja innerhalb der Schleife zugewiesen wurde, ist die Schleifenbedingung also erfüllt. Damit wird die Schleife abgebrochen. Denn `until` ist wie "solange bis" zu lesen. Im Gegensatz zur `while`-Schleife, deren Anweisungsblock ausgeführt wird, solange die Bedingung wahr ist, wird hier der Anweisungsblock ausgeführt, bis die Schleifenbedingung wahr ist.

Im Beispiel wird die Schleife einmal durchlaufen, obwohl die Schleifenbedingung gleich im ersten Durchlauf wahr ist. Der Grund ist eben, dass zuerst der abhängige Code ausgeführt und erst dann die Bedingung überprüft wird.

### Beachten Sie:

`do`-Schleifen sind eigentlich keine echten Schleifen, weshalb dort Sprungbefehle wie `last`, `next` und `redo` nicht funktionieren.

Es gibt in Perl auch `do`-Schleifen, deren Bedingung kein `until`, sondern ein `while` vorangestellt ist. Dann müssen Sie die Schleifenbedingung negativ formulieren.

## Schleifen bei Hashes

So wie sich Arrays prima mit foreach-Schleifen "traversieren", also Element für Element durchlaufen lassen, besteht dieser Wunsch natürlich auch bei Hashes. Da ein Hash-Element jedoch immer aus zwei Werten besteht, von denen der erste der Schlüssel ist und der zweite der eigentliche Datenwert, ist ein einfaches Traversieren wie mit `foreach` nicht möglich. Deshalb gibt es für Hashes eine eigene Schleifen-Syntax.

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";

    my %Familie = (Frau => "Eva", Tochter => "Anja", Sohn => "Florian");
    my $Schluessel;
    my $Wert;

    while (($Schluessel, $Wert) = each(%Familie)) {
      print "$Schluessel hei&szlig;t $Wert<br>\n";
    }

    while ($Schluessel = each(%Familie)) {
      print "$Schluessel hei&szlig;t $Familie{$Schluessel}<br>\n";
    }

    print "</body></html>\n";

### Erläuterung:

Das Beispiel deklariert einen Hash namens `%Familie` und weist ihm drei Schlüssel-Wert-Paare zu. Anschließend werden zwei Skalare `$Schluessel` und `$Wert` deklariert, die innerhalb der Schleife benötigt werden. Die Schleife wird als `while`-Schleife formuliert. Innerhalb der Schleifenbedingung wird jedoch die Perl-Funktion each aufgerufen. Diese liefert wahlweise eine Liste mit zwei Elementen, nämlich dem jeweils nächsten Schlüssel und dem zugehörigen Wert, oder - im skalaren Kontext - nur den jeweils nächsten Schlüssel des übergebenen Hashes.

Das Beispiel-Script zeigt beide Varianten. In der ersten Variante wird die Liste mit den beiden Elementen in dem Ausdruck `($Schluessel, $Wert)` gespeichert. `$Schluessel` enthält dann den jeweils aktuellen Schlüssel des Hashs, und `$Wert` den zugehörigen Datenwert. Im Beispiel wird die Schleife dreimal durchlaufen und gibt solche Sätze aus wie `Frau heißt Eva`.

In der zweiten Variante wird die `each`-Funktion im skalaren Kontext aufgerufen, da der Rückgabewert nur in `$Schluessel` gespeichert wird. Die innerhalb der Schleife formulierte `print`-Anweisung gibt daher das Gleiche aus wie in der ersten Variante. Diesmal ist jedoch keine Variable `$Wert` verfügbar. Über ein Konstrukt wie `$Familie{$Schluessel}` kann aber auf den jeweils aktuellen Wert zugegriffen werden.

## Rekursion

Rekursion ist dann ein Mittel, wenn man mit Schleifen nicht mehr weiter kommt. Ein typischer Anwendungsfall für Rekursion ist das Traversieren von baumartigen Strukturen. Auf gut Deutsch: wenn Sie beispielsweise einen ganzen Verzeichnisbaum einlesen wollen, ohne die Datei- und Verzeichnisstruktur vorher zu kennen, dann ist das ein typischer Fall für eine rekursive Anwendung. Bei der Rekursion wird eine Subroutine definiert, innerhalb derer es eine Anweisung gibt, die die Subroutine von neuem aufruft. Dadurch entsteht ein Verschachtelungseffekt. Rekursion ist allerdings aus Computersicht nicht ganz unkritisch. Deshalb muss sie sauber programmiert sein.

Das folgende Beispiel zeigt, wie Sie eine Datei- und Verzeichnisstruktur ab einem gegebenen Startverzeichnis einlesen und an den aufrufenden Browser HTML-formatiert übermitteln lassen können. Das Beispiel ist allerdings nicht ganz trivial.

### Beispiel eines vollständigen CGI-Scripts in Perl:

Anzeigebeispiel: So sieht's aus (Zum Aufruf des Scripts ist eine Internet-Verbindung erforderlich)

    #!/usr/bin/perl -w

    use strict;
    use CGI::Carp qw(fatalsToBrowser);

    my $Startverzeichnis = "/var/www/selfhtml.org/de/dokumente/perl";
    my @Alle;
    my $Totalbytes = 0;

    print "Content-type: text/html\n\n";
    print '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">', "\n";
    print "<html><head><title>Testausgabe</title>\n";
    print "</head><body>\n";
    print "<h1>Dateibaum</h1>\n";
    print "<pre>Startverzeichnis: <b>$Startverzeichnis</b></pre>\n";
    print "<hr noshade size=\"1\"><pre>\n";

    Ermitteln($Startverzeichnis);
    @Alle = sort(@Alle);
    foreach (@Alle) {
      print "$_\n";
    }
    print "</pre><hr noshade size=\"1\">\n";
    print "<pre>Insgesamt: [$Totalbytes Bytes]</pre>\n";
    print "</body></html>\n";

    sub Ermitteln {
      my $Verzeichnis = shift;
      my $Eintrag;
      my $Pfadname;
      my $HTML_Eintrag;
      my $Bytes;
      local *DH;

      unless (opendir(DH, $Verzeichnis)) {
       return;
      }
      while (defined ($Eintrag = readdir(DH))) {
       next if($Eintrag eq "." or $Eintrag eq "..");
       $Pfadname = $Verzeichnis."/".$Eintrag;
       if( -d $Pfadname) {
        $HTML_Eintrag = $Verzeichnis."/".$Eintrag." [VERZEICHNIS]";
       }
       else {
        $Bytes = -s $Pfadname;
        $Totalbytes += $Bytes;
        $HTML_Eintrag = $Verzeichnis."/".$Eintrag." [$Bytes Bytes]";
       }
       push(@Alle, $HTML_Eintrag);
       Ermitteln($Pfadname) if(-d $Pfadname);
      }
     closedir(DH);
    }

### Erläuterung:

Zunächst werden drei wichtige Variablen deklariert: `$Startverzeichnis` speichert das Verzeichnis, ab dem die Suche starten soll, `@Alle` ist die Liste, in der später die eingelesenen Einträge gespeichert werden, und `$Totalbytes` ermittelt die Bytezahlen aller Dateien.

Danach wird mit der HTML-Ausgabe begonnen. Unterhalb davon steht die Anweisung `Ermitteln($Startverzeichnis);`. Dies ist ein Aufruf der Subroutine `Ermitteln`, die etwas weiter unten mit `sub Ermitteln` eingeleitet wird. Diese Subroutine ist zugleich diejenige, die sich in dem Anweisungsblock, den sie einschließt, in ihrer vorletzten Anweisung selbst wieder aufruft und so die Rekursion bewirkt.

Mit der Anweisung `Ermitteln($Startverzeichnis);` passiert damit das gesamte Einlesen der Datei- und Verzeichnisstruktur. Anschließend wird die Liste mit der Funktion `sort` alphabetisch sortiert und dann Eintrag für Eintrag aus einer `foreach`-Schleife heraus ausgegeben.

Das Herzstück des Scripts ist die Subroutine `Ermitteln`. Darin wird zunächst eine Reihe von Arbeitsvariablen deklariert. Da die Subroutine sich ja selber wieder aufruft, stellt sich die Frage, ob es dabei nicht zu einem Kuddelmuddel mit den Namen der Variablen kommt. Die Antwort ist nein. Denn jedes Ausführen der Subroutine erzeugt eine eigene Instanz der Routine im Arbeitsspeicher, und da die Variablen lokal mit `my` deklariert sind, bleibt ihre Gültigkeit auf eine Instanz beschränkt.
Eine offensichtliche Ausnahme bildet die Anweisung `local *DH`, die das Verzeichnishandle `DH` lokal deklariert. Da `my` nicht auf Datei-/Verzeichnishandles (bzw. Typeglobs) angewendet werden kann, wird hier zu dieser Lösung gegriffen, die intern zwar etwas anders arbeitet, aber den gewünschten Effekt hat. Eine andere Variante wäre, das Standardmodul `Symbol` zu verwenden und sich in jeder Instanz der Subroutine ein neues Verzeichnishandle zu schaffen. Das Verfahren mag "sauberer" erscheinen, ist es aber im Endeffekt nicht. Außerdem ist die Variante mit `local` bedeutend schneller.
Die vielen Instanzen der Subroutine bei vielen Verzeichnissen führen aber auch dazu, dass immer mehr Arbeitsspeicher benötigt wird. Das ist ein wichtiger Nachteil der Rekursion. Konstrukte mit vielen rekursiven Selbstaufrufen sollten Sie daher in CGI-Scripts, die auf öffentlichen Webservern sehr häufig und in mehreren Prozessen gleichzeitig aufgerufen werden können, vermeiden.

Die Subroutine `Ermitteln` erwartet einen Verzeichnispfadnamen, der ihr übergeben wird. Mit `$Verzeichnis = shift;` wird der übergebene Pfadname im Skalar `$Verzeichnis` gespeichert (siehe dazu auch die Perl-Funktion shift). Anschließend wird mit der Funktion opendir das übergebene Verzeichnis geöffnet. Seine Einträge werden in einer `while`-Schleife mit der Funktion readdir eingelesen. Die beiden Einträge mit den Werten `.` und `..`, die in jedem Verzeichnis vorkommen und das aktuelle bzw. das übergeordnete Verzeichnis symbolisieren, werden mit dem Sprungbefehl next übersprungen. Andernfalls würde sich die Rekursion in einer Endlosschleife verheddern.

Mit dem Dateitestoperator `-d` in `if( -d $Pfadname)` wird abgefragt, ob der jeweils aktuelle Verzeichniseintrag wieder ein Verzeichnis, also ein Unterverzeichnis, ist. Abhängig davon wird ein HTML-Eintrag für die auszugebende Liste vorbereitet. Weiter unten wird dann mit `-d` noch einmal abgefragt, ob der Eintrag ein Unterverzeichnis ist, und davon abhängig die Subroutine `Ermitteln` mit dem Unterverzeichnis erneut aufgerufen.

Nachdem die Verzeichnisstruktur abgearbeitet ist und alle Instanzen der Subroutine `Ermitteln` beendet sind, geht es im oberen Teil des Scripts weiter mit `@Alle = sort(@Alle);`.

| Sprungbefehle
| Bedingte Anweisungen

© 2007 Impressum