# Dateisystem

Bei der Verarbeitung von Daten ist es häufig nötig oder gewünscht, diese auch speichern zu können. Dies kann entweder in einer Datenbank oder in einer Datei erfolgen. Perl bietet für die Arbeit mit dem Dateisystem viele umfangreiche Funktionen, was aber nicht dazu führt, dass die Verwendung schwierig wird. So ist es problemlos möglich, dem Benutzer das Hochladen einer Datei zu ermöglichen oder seinen Kommentar in einem Gästebuch zu speichern.

# Verzeichnisliste

Gerade dann, wenn Sie Dateien auf einem Webserver zum Download bereitstellen möchten, müssen Sie wissen, welche Dateien in welchem Verzeichnis liegen bzw. welche Verzeichnisse überhaupt existieren. Sie könnten natürlich alle zum Download bereitstehenden Dateien in einem einzigen Verzeichnis ablegen und diese Dateien in einem HTML-Dokument von Hand eintragen. Wesentlich eleganter und flexibler ist es jedoch, diese Informationen mit einem Perl-Script zu sammeln und anschließend in HTML-Form aufbereitet auszugeben.

Um dieses Ziel zu erreichen, benötigen Sie drei verschiedene Funktionen:

  • opendir
    Mit der Funktion opendir können Sie ein Verzeichnis zum Lesen öffnen. Gleichzeitig wird ein Handle erzeugt, über das Sie anschließend auf das Verzeichnis zugreifen können.
  • readdir
    Die Funktion readdir ermöglicht das Lesen des geöffneten Verzeichnisses. Das mit opendir erzeugte Handle spielt dabei eine wichtige Rolle. Es muss der Funktion readdir nämlich als Parameter übergeben werden. Als Rückgabewert liefert die Funktion readdir eine Liste mit dem Inhalt des Verzeichnisses.
  • closedir
    Zum Schluss muss das Verzeichnis-Handle wieder gelöscht werden, d. h. das Verzeichnis wieder für andere bzw. weitere Zugriffe freigegeben werden. Das Handle wird dabei als Parameter an die Funktion closedir übergeben.

Um ein Verzeichnis mit der Funktion opendir öffnen zu können, müssen Sie zwei verschiedene Parameter angeben: zuerst den Bezeichner der Variablen, in der das Handle für den Verzeichniszugriff gespeichert werden soll, und als zweiten Parameter, welches Verzeichnis geöffnet werden soll. Tritt bei der Ausführung der Funktion ein Fehler auf, gibt die Funktion FALSE zurück, und die genaue Fehlermeldung wird im Skalar $! gespeichert. Andernfalls gibt die Funktion TRUE zurück.

boolean opendir(handle, string directory)

Ob Sie das Verzeichnis nun als absoluten oder relativen Pfad übergeben, ist egal. Achten Sie bei der Angabe von Verzeichnissen jedoch darauf, den in Windows üblichen Backslash \ durch einen normalen Schrägstrich / zu ersetzen, z. B. c:/windows.

my $result = opendir(DIR,"/dateien/dokumente");  
  # relative Pfadangabe  
my $result = opendir(DIR,"c:/dateien/dokumente");  
  # absolute Pfadangabe

Es hat sich übrigens eingebürgert, den Bezeichner eines Datei- oder Verzeichnis-Handles in Großbuchstaben zu schreiben. Dies ermöglicht ein schnelleres Wiederfinden und Erkennen im Quelltext des Scripts.

Den Inhalt eines Verzeichnisses auszulesen, ist sehr einfach. Sie übergeben der Funktion readdir einfach das Verzeichnis-Handle und weisen den Rückgabewert der Funktion einer Liste oder einem Skalar zu.

mixed readdir(handle)

Je nachdem, welchem Variablentyp Sie den Rückgabewert zuweisen, erhalten Sie unterschiedliche Ergebnisse. Würden Sie die Funktion aufrufen, indem Sie den Rückgabewert einem Skalar zuweisen, z. B.

$eintrag = readdir(DIR);

erhalten Sie den jeweils nächsten Eintrag des Verzeichnisses. Sie müssten die Funktion dann jedoch so oft aufrufen, wie es Einträge im Verzeichnis gibt. Weisen Sie den Rückgabewert der Funktion jedoch einer Liste zu, z. B.

my @eintraege = readdir(DIR);

erhalten Sie alle Verzeichniseinträge auf einmal. Übrigens werden in beiden Fällen auch die symbolischen Verzeichniseinträge . und .. zurückgegeben. Der Eintrag . entspricht dabei dem gerade aktiven Verzeichnis und .. dem übergeordneten Verzeichnis.

Als Parameter erwartet die Funktion closedir lediglich das Verzeichnis-Handle. Die Funktion liefert außerdem keinen Rückgabewert.

closedir(handle)

Ein Beispiel:

closedir(DIR);

Da nun alle drei Funktionen eingehend besprochen worden sind, ist es an der Zeit, ein paar erste Schritte zu unternehmen. Ich werde Ihnen nun zeigen, wie Sie ein Verzeichnis einlesen und anschließend im Browser ausgeben können.

#!/usr/bin/perl -w
use CGI qw(:standard);

my $result = opendir(DIR,"..");
if($result)
{
  my @eintraege = readdir(DIR);
  print header();
  foreach(@eintraege)
  {
    print "$_<br>";
  }
  closedir(DIR);
}
else
{
  print header();
  print $!;
}

Listing 7.1: Perl-Script, das das übergeordnete Verzeichnis einliest und im Browser ausgibt

Zu Beginn des Scripts aus Listing 7.1 wird versucht, das übergeordnete Verzeichnis .. vom gerade aktuellen Verzeichnis zum Lesen zu öffnen. Das Ergebnis wird in der Variable $result gespeichert. Ist der Wert der Variable $result gleich TRUE, wird der if-Anweisungsblock ausgeführt; wenn der Wert FALSE ist, wird der else-Anweisungsblock ausgeführt.

Konnte das Verzeichnis erfolgreich zum Lesen geöffnet werden, werden mit der readdir-Funktion alle Verzeichniseinträge in der Liste @eintraege gespeichert. Nach der Ausgabe des HTML-Headers (print header();) wird jedes Element der @eintrage-Liste mit der print-Anweisung und einem HTML-Zeilenumbruch <br> im Browser ausgegeben. Zum Schluss wird das Verzeichnis mit der closedir-Funktion für andere Zugriffe wieder freigegeben. Das Verzeichnis-Handle ist immer in der Variablen DIR verfügbar.

Für den Fall, dass das Verzeichnis nicht zum Lesen geöffnet werden kann, wird im else-Anweisungsblock die in $! gespeicherte Fehlermeldung ausgegeben.

Die HTML-Ausgabe des Listing 7.1 könnte folgendermaßen aussehen:

.
..
cgi-bin
db
docs
error
listings
index.html

Beachten Sie, dass auch die beiden symbolischen Einträge . und .. mit ausgegeben werden. Mit einer if-Anweisung könnten Sie natürlich verhindern, dass diese beiden Einträge im Browser ausgegeben werden.

# Rekursion

Für den Fall, dass Sie durch ein Script eine Verzeichnisliste darstellen möchten, die alle Dateien auflistet und sich dabei nicht nur auf ein Verzeichnis beschränken, sondern auch die Unterverzeichnisse mit einbeziehen soll, müssen Sie die Verzeichniseinträge unterscheiden. Und zwar müssen Sie zwischen Verzeichnissen und Dateien unterscheiden.

Für solche Unterscheidungen verwendet man Dateitestoperatoren. Natürlich wäre es dann auch angenehm, die Größe einer Datei zu ermitteln usw. Auch dies ist über die Dateitestoperatoren möglich. Sie können entweder in einem logischen oder in einem skalaren Kontext aufgerufen werden, da sie entweder TRUE/FALSE oder einen Zahlenwert zurückgeben, je nachdem, welche Eigenschaft überprüft wurde. Die folgende Tabelle bietet eine Übersicht über diese Dateitestoperatoren.

Operator Typ Erklärung Beispiel
-d Boolean Überprüft, ob der Eintrag ein Verzeichnis ist. if -d »cgi-bin«...
-e Boolean Überprüft, ob ein Verzeichnis existiert. if -e »cgi-bin«...
-s Integer Ermittelt die Größe einer Datei in Bytes. print -s »index.htm«;

Tabelle 7.1: Die wichtigsten Dateitestoperatoren

Es gibt natürlich weit mehr als nur drei Dateitestoperatoren. Die meisten davon sind jedoch UNIX-spezifisch und funktionieren deshalb nicht in Windows-Umgebungen. Sie wurden aufgrund der Kompatibilität nicht weiter berücksichtigt.

Die beiden Operatoren, die im Folgenden verwendet werden, sind -d und -s. Bevor also ein Verzeichniseintrag im Browser ausgegeben wird, wird überprüft, ob es sich um ein Verzeichnis handelt. Ist dies der Fall, wird das Verzeichnis geöffnet und ebenfalls eingelesen, und zwar so lange, bis es kein Unterverzeichnis mehr gibt. Dies nennt man auch Rekursion.

Genauer betrachtet, bedeutet die Rekursion, dass eine Routine einen kleinen Teil einer Aufgabe selbst löst, diese Aufgabe in mehrere Teile zerlegt und sich selbst aufruft, um einen weiteren Teil der Aufgabe lösen zu können. Dies ermöglicht einen gut lesbaren und kurzen Quelltext, ist jedoch ein wenig komplizierter in der Programmierung, da es ein gewisses »Um-die-Ecke-Denken« verlangt.

Eine solche Rekursion lässt sich immer gut an Verzeichnissen darstellen. Die Aufgabe dabei ist, ein angegebenes Verzeichnis inklusive aller Unterverzeichnisse und Dateien aufzulisten. Der erste Teil der Aufgabe wäre dann, die Dateien des Startverzeichnisses auszugeben, und die anderen Teile der Aufgabe wären, die Dateien der Unterverzeichnisse aufzulisten. Dazu ruft die Routine sich selbst auf und erhält das Unterverzeichnis als Aufgabe. Solange die Routine auf Unterverzeichnisse stößt, ruft sie sich selbst auf, bis es keine Unterverzeichnisse mehr gibt.

An dieser Stelle sollten Sie ein kleineres Problem von rekursiven Aufrufen betrachten. Eine Rekursion kann sich unter Umständen sehr stark vertiefen. Wenn Sie innerhalb einer Subroutine also eine Menge Daten in Variablen speichern und diese Rekursion immer tiefer voranschreitet, kann es zu einem Fehler kommen – und zwar dann, wenn der zur Verfügung stehende Speicher nicht ausreicht, und der Webserver oder gar das Betriebssystem sich in dem Moment aufhängt. Ein weiteres Problem kann eine nicht zu lösende Aufgabe sein, also eine endlose Rekursion. Ein Beispiel dafür:

rekursion();  
sub rekursion  
{  
  my $var = 1024 ** 1024;  
  rekursion();  
}

Da zu keinem Zeitpunkt ein Ende der Subroutine und somit der Rekursion erreicht wird und obendrein ein sehr großer Zahlenwert in der lokal gültigen Variable $var gespeichert wird, läuft die Rekursion so lange durch, bis von außen ein Abbruch, auf welche Art auch immer, erfolgt. Seien Sie also vorsichtig!

Das vollständige Script, um ein Verzeichnis und alle Unterverzeichnisse rekursiv aufzurufen, sieht folgendermaßen aus:

#!/usr/bin/perl -w
use CGI qw(:standard);

# Ermitteln des Wurzelverzeichnisses
my $wverz = $ENV{'DOCUMENT_ROOT'};

# Ausgabe des HTML-Headers und des Anfangs des HTML-Dokumentes
print header();
print "<html><head><title>Wurzelverzeichnis: $wverz</title></head><body>";
print "<h1>Wurzelverzeichnis: $wverz</h1><table border=\"1\">";

# Erster Aufruf der Subroutine "Unterverzeichnis"
Unterverzeichnis($wverz);

# Abschluss des HTML-Dokumentes
print "</table></body></html>";

# Definition der Subroutine Unterverzeichnis
sub Unterverzeichnis
{
  my $subverz = shift;
  print "<tr><td>$subverz</td><td>";
  if(opendir(DIR,$subverz))
  {
    local @verz;
    while($eintrag = readdir(DIR))
    {
      next if($eintrag eq "." || $eintrag eq "..");
      if(-d "$subverz/$eintrag")
      {
        push(@verz,$eintrag);
      }
      else
      {
        my $groesse = -s "$subverz/$eintrag";
        print "<a href=\"$subverz/$eintrag\">$eintrag</a> ($groesse Bytes)<br>";
      }
    }
    print "</td></tr>";
    foreach(@verz)
    {
      Unterverzeichnis("$subverz/$_");
    }
    closedir(DIR);
  }
}

Listing 7.2: Beispiel für das rekursive Einlesen eines Verzeichnisses und aller Unterverzeichnisse

Ich werde Ihnen das Listing 7.2 nun Schritt für Schritt erläutern. Ganz am Anfang wird dem Skalar $wverz das Element $ENV{'DOCUMENT_ROOT'} zugewiesen. Dieses Element des %ENV-Hashs enthält den vollständigen Pfad des Wurzelverzeichnisses für alle Dateien, die über den Webserver abrufbar sind. Anschließend folgen drei print-Anweisungen, die den HTML-Header, den Beginn des HTML-Dokuments, den vollständigen Kopfteil und eine Überschrift ausgeben sowie den Beginn einer Tabelle. Danach folgt lediglich der Aufruf der Subroutine Unterverzeichnis, der das Verzeichnis übergeben wird, das im Skalar $wverz gespeichert wurde. Die danach folgende print-Anweisung schließt die Tabelle, das body-Element und schlussendlich das HTML-Dokument.

Das Wichtigste an Listing 7.2 ist die Subroutine Unterverzeichnis. Die Anweisung

$subverz = shift;

speichert im Skalar $subverz das Verzeichnis, das bei dem Aufruf der Routine übergeben wurde. Mit der darauf folgenden Anweisung

print "<tr><td>$subverz</td><td>";

werden eine neue Zeile in der Tabelle und zwei Zellen erzeugt. In der ersten Zelle wird das Verzeichnis ausgegeben, die zweite Zelle wird lediglich geöffnet. Der nächste Schritt beinhaltet den Versuch, das im Skalar $subverz gespeicherte Verzeichnis zu öffnen. Das daraus resultierende Verzeichnis-Handle wird in der Variable DIR gespeichert. War das Öffnen erfolgreich, wird der if-Anweisungsblock ausgeführt. In diesem wird zuerst die Liste @verz definiert. An diese Liste werden später alle Unterverzeichnisse angehängt, die im Verzeichnis $subverz gefunden wurden. Anschließend folgt die Anweisung:

while($eintrag = readdir(DIR))

Dies führt dazu, dass die einzelnen Verzeichniseinträge nacheinander abgearbeitet werden können. Der gerade aktuelle Verzeichniseintrag wird im Skalar $eintrag gespeichert. Innerhalb des Anweisungsblocks folgt die noch unbekannte Art der Anweisung

next if($eintrag eq "." || $eintrag eq "..");

Dies bedeutet, dass next nur dann ausgeführt wird, wenn der Wert des Skalars $eintrag entweder . oder .. entspricht. Ein solches Konstrukt nennt sich nachgestellte Bedingungsprüfung. Sie funktioniert auch bei allen anderen Anweisungen und ist kürzer und übersichtlicher, wenn bei einer Bedingung nur eine einzelne Anweisung ausgeführt werden soll. Im Übrigen führt die Anweisung dazu, dass der Schleifendurchlauf übersprungen wird, wenn der Verzeichniseintrag einem der symbolischen Einträge für das aktuelle oder dem übergeordneten Verzeichnis entspricht. Der Rest des while-Anweisungblocks

if(-d "$subverz/$eintrag")  
{  
  push(@verz,$eintrag);  
}  
else  
{  
  my $groesse = -s "$subverz/$eintrag";  
  print "<a href=\"$subverz/$eintrag\">$eintrag</a>   
    ($groesse Bytes)<br>";  
}

beschränkt sich auf die Überprüfung, ob der im Skalar $eintrag gespeicherte Verzeichniseintrag einem Verzeichnis oder einer Datei entspricht. Ist der Eintrag ein Verzeichnis, wird der @verz-Liste der aktuelle Eintrag mittels der push-Funktion angehängt. Entspricht der aktuelle Eintrag einer Datei, wird die Größe der Datei mit der Anweisung

my $groesse = -s "$subverz/$eintrag";

ausgelesen und mittels der print-Anweisung ein Hyperlink ausgegeben, der als Verweisziel und als Beschreibung diesen Eintrag zugewiesen bekommt. Vor dem Zeilenumbruch wird dann die Größe der Datei in Bytes notiert. Da in HTML Werte für Attribute in doppelten Anführungsstrichen notiert werden, müssen diese mit einem vorangestellten Backslash \ entwertet werden, damit der Perl-Interpreter diese nicht irrtümlicherweise als Ende der auszugebenden Zeichenkette interpretiert. Nachdem alle Verzeichniseinträge abgearbeitet und Verzeichnisse der @verz-Liste hinzugefügt bzw. Dateien im Browser ausgegeben wurden, werden die Zelle und die Zeile mit der Anweisung

print "</td></tr>";

geschlossen. Dann folgt eine foreach-Schleife, die alle Elemente der @verz-Liste abarbeitet. Im Anweisungsblock wird dann die Subroutine Unterverzeichnis aufgerufen. Sie erhält als Parameter das gerade aktuelle Verzeichnis in Verbindung mit dem aktuellen Listeneintrag. An dieser Stelle entsteht dann die Rekursion, da sich die Subroutine selbst aufruft und einen weiteren Teil der Aufgabe löst. Nachdem schließlich alle Unterverzeichnisse des aktuellen Verzeichnisses abgearbeitet worden sind, wird das Verzeichnis-Handle mit der Anweisung

closedir(DIR);

freigegeben.

Dieses Perl-Script ist bestimmt noch verbesserungswürdig, demonstriert aber sehr gut die Rekursion in Verbindung mit Verzeichnissen. Außerdem ist die Ausgabe jeder beliebigen Datei sicherlich ein sehr großes Sicherheitsproblem. Alternativ könnte man die Ausgabe der Dateien auf Bilder und/oder Textdateien beschränken.

# Weitere Verzeichnisfunktionen

Zu den bisher vorgestellten Funktionen opendir, readdir und closedir sowie den Dateitestoperatoren gibt es noch weitere Funktionen, die das Arbeiten mit Verzeichnissen ermöglichen. An dieser Stelle folgt eine kurze Übersicht.

# Verzeichnis wechseln

Mit der Funktion chdir (change directory) können Sie in ein anderes Verzeichnis auf dem Server wechseln. Das Verzeichnis, in das gewechselt werden soll, wird dabei als Parameter an die Funktion übergeben. Ob Sie nun relative oder absolute Pfadangaben machen, ist egal, wichtig ist nur, dass Sie Backslashs \ durch einen Schrägstrich / ersetzen müssen bzw. sollten.

chdir("..");  
  # wechselt in das übergeordnete Verzeichnis  
chdir("d:/wwwroot/cgi-bin");  
  # wechselt in das Verzeichnis d:\wwwroot\cgi-bin

# Verzeichnis erstellen

Um ein Verzeichnis zu erstellen, müssen Sie die Funktion mkdir (make directory) verwenden. Diese Funktion erwartet zwei Parameter:

  • Verzeichnisname
    Als erster Parameter wird der Verzeichnisname, gegebenenfalls mit Pfadangabe, erwartet. Die Pfadangabe kann wiederum absolut oder relativ sein.
  • Zugriffsrechte
    Dieser Parameter wird erwartet, kommt jedoch nur unter UNIX/Linux-Umgebungen zum Tragen. Die Rechte werden dabei als Oktalzahl erwartet. Wenn Sie Zweifel haben, ob UNIX/Linux oder Windows verwendet wird (z. B. bei einem Provider), sollten Sie die Rechte 0777 vergeben. Diese ermöglichen allen Benutzern den Zugriff auf das Verzeichnis.

Einige Beispiele:

mkdir("texte",0777);  
  # erstellt das Verz. texte im gerade aktiven Verzeichnis  
mkdir("../dokumente/texte",0777);  
  # relative Pfadangabe, erstellt Verz. texte  
mkdir("d:/wwwroot/dokumente/texte",0777);  
  # absolute Pfadangabe, erstellt Verz. texte

Konnte das Verzeichnis erstellt werden, liefert die Funktion TRUE zurück, wenn nicht, dann FALSE. Prüfen Sie unbedingt auf diesen Wert, denn wenn Sie eine Datei in dem Verzeichnis anlegen möchten und das Verzeichnis existiert nicht, bricht Perl mit einer Fehlermeldung ab. Anhand der ausgegebenen Fehlermeldung können Sie jedoch keine Rückschlüsse darauf ziehen, aus welchem Grund abgebrochen wurde.

# Verzeichnis löschen

Die Funktion rmdir (remove directory) ermöglicht das Löschen eines Verzeichnisses, das als Parameter übergeben wurde. Auch hier kann wieder, wenn nötig, eine absolute oder relative Pfadangabe vorangestellt werden.

rmdir("texte");  
  # löscht das Verz. texte  
rmdir("../dokumente/texte");  
  # relative Pfadangabe, löscht das Verz. texte  
rmdir("d:/wwwroot/dokumente/texte");  
  # absolute Pfadangabe, löscht das Verz. texte

Beachten Sie, dass das zu löschende Verzeichnis leer sein muss, also keinerlei Dateien oder Unterverzeichnisse enthalten darf. Konnte das Verzeichnis erfolgreich gelöscht werden, lautet der Rückgabewert der Funktion TRUE, andernfalls FALSE.

# Datei lesen

Mit Perl können Sie nicht nur schnell und einfach Verzeichnisse verwalten, sondern auch mit Dateien arbeiten. Um eine Datei zum Lesen öffnen zu können, müssen Sie die Funktion open verwenden. Diese Funktion erwartet genau zwei Parameter: zuerst eine Variable, in der das Datei-Handle gespeichert werden soll. Dies kennen Sie bereits von Verzeichnissen. Auch hier gilt, dass Datei-Handles immer großgeschrieben werden und kein Sonderzeichen vorangestellt bekommen. Als zweiten Parameter erwartet die Funktion die Datei, die zum Lesen geöffnet werden soll, entweder mit oder ohne Pfad. Der Pfad kann im Übrigen relativ oder absolut angegeben werden. Je nachdem, ob der Versuch erfolgreich war oder nicht, liefert die Funktion entweder TRUE oder FALSE zurück.

boolean open(handle, string filename)

Die Anweisung

$result = open(FILE, "datei.dat");

würde versuchen, die Datei datei.dat zu öffnen und in der Variable FILE das Datei-Handle zu speichern. Ist der Vorgang erfolgreich gewesen, erhält $result den Wert TRUE. Konnte die Datei nicht geöffnet werden oder wurde sie nicht gefunden, erhält $result den Wert FALSE.

Das Lesen aus der geöffneten Datei ist sehr einfach, da Sie noch nicht einmal eine Funktion dafür verwenden müssen. Um alle Zeilen einer geöffneten Datei auslesen zu können, notieren Sie einfach den Namen des Datei-Handles in spitzen Klammern und weisen das Konstrukt mit dem Zuweisungsoperator = einer Liste zu.

my @zeilen = <FILE>;

Die geöffnete Datei können Sie nach Abschluss des Lesevorgangs mit der Funktion close wieder schließen. Als Parameter übergeben Sie der Funktion das Datei-Handle.

close(FILE);

Versuchen Sie, eine geöffnete Datei immer sofort nach dem Lesevorgang zu schließen. Damit geben Sie belegte Systemressourcen wieder frei, und es können auch andere Programme wieder auf diese Datei zugreifen.

Ansonsten sollten Sie bei dem Versuch, eine Datei zu öffnen, immer eine Fehlerbehandlung mit einbinden. Überprüfen Sie deshalb immer den Rückgabewert der open-Funktion, und beenden Sie notfalls das Perl-Script mit der Anweisung die. Dieser Funktion können Sie zusätzlich noch einen Text übergeben, der dann ausgegeben wird.

$result = open(FILE,"datei.dat");  
if(!$result)  
{  
  die("Datei konnte nicht geöffnet werden.");  
}

# Praxisanwendung

Hier ein praktisches Anwendungsbeispiel. Unser Ziel ist es, eine Datei zu öffnen, den Inhalt einzulesen, zu sortieren und zum Schluss im Browser auszugeben.

#!/usr/bin/perl -w
use CGI qw(:standard);

my $fahrzeuge = "fahrzeuge.txt";

$result = open(FILE,$fahrzeuge);
if($result)
{
  my @datei = <FILE>;
  close(FILE);
  print header();
  print "<h1>Unsortiert</h1>";
  foreach(@datei)
  {
    print "$_<br>";
  }
  print "<h1>Sortiert</h1>";
  @datei = sort(@datei);
  foreach(@datei)
  {
    print "$_<br>";
  }
}
else
{
  die("Die Datei $fahrzeuge konnte nicht geöffnet werden!");
}

Listing 7.3: Perl-Script, das die Datei fahrzeuge.txt einliest und einmal unsortiert und anschließend sortiert ausgibt

Der Inhalt der Datei fahrzeuge.txt sieht folgendermaßen aus:

Mercedes Benz SL500  
Audi A4  
VW Golf  
Audi A8  
VW Phaeton  
Ferrari F355 Berlinetta  
BMW Z4  
Mercedes Benz S55  
VW Käfer  
BMW 5er

Zu Beginn des Listing 7.3 wird dem Skalar $fahzeuge die Zeichenkette fahrzeuge.txt zugewiesen. Anschließend wird mit der Anweisung

$result = open(FILE,$fahrzeuge);

versucht, die Datei zu öffnen und das Datei-Handle in der Variable FILE zu speichern. Das Ergebnis der Anweisung wird der Variable $result zugewiesen. Nach der Überprüfung mit if, ob der Wert der Variable TRUE ist, wird der Inhalt der Datei mit der Anweisung

my @datei = <FILE>;

in der Liste @datei gespeichert. Das Datei-Handle wird mit der Anweisung

close(FILE);

geschlossen. Die beiden folgenden print-Anweisungen geben den HTML-Header und eine Überschrift der 1. Ordnung aus. Mit der foreach-Schleife wird die Liste @datei ausgegeben. Anschließend folgt erneut die Ausgabe einer Überschrift der 1. Ordnung, und die Liste wird durch die Anweisung

@datei = sort(@datei);

sortiert und erneut mit Hilfe einer foreach-Schleife ausgegeben. Sollte die Datei fahrzeuge.txt nicht geöffnet werden können, wird eine Fehlermeldung ausgegeben und das Script beendet.

# Datei schreiben

Auch das Schreiben in eine Datei ist natürlich möglich, und der Weg entspricht dem des Dateilesens. Es gibt nur einen kleinen, aber feinen Unterschied: Sie müssen spezielle Steuerkommandos vor den Pfad- und Dateinamen der zu schreibenden Datei notieren. Die folgende Tabelle enthält eine Übersicht über diese Steuerkommandos.

Zeichen Beispiel Erklärung
< open(FILE,"<datei.dat"); Datei nur zum Lesen öffnen. Wenn Sie kein Steuerzeichen notieren, ist dies die Standard-Einstellung.
> open(FILE,">datei.dat"); Datei zum Schreiben öffnen. Wenn die Datei bereits existiert, wird sie überschrieben, ansonsten angelegt.
>> open(FILE,">>datei.dat"); Datei zum Schreiben öffnen, der neue Inhalt wird jedoch zum alten hinzugefügt.
+> open(FILE,"+>datei.dat"); Datei zum Lesen und Schreiben öffnen.

Tabelle 7.2: Steuerkommandos zum Öffnen von Dateien

Die Angabe der Steuerzeichen allein genügt jedoch noch nicht, um auch wirklich Daten in eine Datei schreiben zu können. Dazu müssen Sie die print-Anweisung ein wenig abwandeln. Nach print folgen das Datei-Handle und dann die Zeichenkette, die in die Datei geschrieben werden soll.

print handle string

Ein Beispiel:

print FILE "Zeichenkette, welche in die Datei geschrieben";  
  # Beispiel mit konstanter Zeichenkette  
print FILE $eine_zeichenkette;  
  # Beispiel mit einem Skalar

In Bezug auf Webseiten werden die Lese- und Schreiboperationen häufig für Gästebücher und Zugriffsstatistiken verwendet. Ich werde Ihnen nun die Programmierung eines solchen Gästebuchs zeigen.

Damit das Gästebuch unabhängig von HTML-Dokumenten bleibt, ist das folgende Script so gestaltet, dass am Anfang überprüft wird, ob Daten an das Script übermittelt worden sind. Andernfalls wird ein HTML-Formular inklusive der bereits vorhandenen Gästebucheinträge ausgegeben.

#!/usr/bin/perl -w
use CGI qw(:standard);

# Definition der Datei für das Gästebuch
my $gb = "gbuch.txt";

if(param())
{
  # Wenn Daten übermittelt wurden, werden Sie der gbuch.txt hinzugefügt
  my $gb_name = param("name");
  my $gb_email = param("email");
  my $gb_comment = param("comment");
  open(FILE,">>$gb") || die("Fehler beim öffnen.");
  print FILE "$gb_name|$gb_email|$gb_comment\n";
  close(FILE);
  print header();
  print "<p>Ihr Eintrag wurde hinzugefügt!</p>";
  print "<a href=\"/cgi-bin/list7.4.pl\">Zur&uuml;ck</a>";
}
else
{
  # Wenn keine Daten übermittelt wurden, werden die vorhandenen
  # Einträge und ein Formular ausgegeben
  open(FILE,"<$gb") || die("Fehler beim öffnen!");
  my @comments = <FILE>;
  close(FILE);
  print header();
  print "<html><head><title>G&auml;stebuch</title></head><body>";
  print "<h1>G&auml;stebuch</h1>";
  print "<table border=\"0\">";
  foreach(@comments)
  {
    my @parts = split(/\|/,$_);
    print "<tr>
             <td><a href=\"mailto:$parts[1]\">$parts[0]</a>: </td>
             <td>$parts[2]</td>
           </tr>";
  }
  print "</table>";
  print "<h3>Ihr G&auml;stebucheintrag</h3>
         <form action=\"/cgi-bin/list7.4.pl\" method=\"post\">
         <table border=\"0\">
         <tr>
           <td>Name:</td>
           <td><input name=\"name\" type=\"text\" size=\"30\"></td>
         </tr>
         <tr>
           <td>Email:</td>
           <td><input name=\"email\" type=\"text\" size=\"30\"></td>
         </tr>
         <tr>
           <td>Kommentar:</td>
           <td><textarea name=\"comment\" cols=\"23\" rows=\"10\">
               </textarea></td>
         </tr>
         <tr>
           <td><input type=\"submit\"></td>
           <td><input type=\"reset\"></td>
         </tr>
         </table></form></body></html>";
}

Listing 7.4: Beispiel für ein Gästebuch in Perl

Zu Beginn des Scripts wird die Variable $gb definiert, die den Namen der Datei zugewiesen bekommt, in der die Gästebucheinträge gespeichert werden. Anschließend folgt die Überprüfung, ob mit einem HTML-Formular Daten an das Script übergeben wurden.

In dem Fall, dass Daten übermittelt wurden, wird der if-Anweisungsblock ausgeführt. Zuerst werden den Skalaren $gb_name, $gb_email und $gb_comment die übermittelten Formulardaten zugewiesen. Danach folgt eine neue Schreibweise, die ich Ihnen etwas später noch erklären werde. Falls die Funktion open FALSE zurückgibt, ist es sicherer, mit einer Oder-Verknüpfung || eine Anweisung auszuführen, die auf diesen Fehler reagiert. In diesem Fall wird der Script-Ablauf sofort beendet und eine Fehlermeldung ausgegeben. Konnte mit der Anweisung

open(FILE,">>$gb")

die Datei gbuch.txt erfolgreich zum Schreiben (und Anhängen) geöffnet werden, wird das Datei-Handle in der Variable FILE gespeichert. Anschließend wird mit der Anweisung

print FILE "$gb_name|$gb_email|$gb_comment\n";

der neue Gästebucheintrag hinzugefügt. Damit die einzelnen Felder (Name, E‑Mail und Kommentar) später auch auseinander gehalten werden können, werden sie mit einer Trennlinie | voneinander getrennt. Sie können auch jedes beliebige andere Zeichen verwenden. Neu ist jedoch die Zeichenfolge \n am Ende der Zeichenkette. Sie bedeutet, dass an dieser Stelle in der Datei ein Zeilenumbruch erfolgen soll. Somit entspricht später jede Zeile einem Gästebucheintrag. Nach dem Schreibvorgang wird die Datei mit

close(FILE);

für weitere Operationen freigegeben, und es werden eine Meldung über die Eintragung und ein Link, der zurück zum Gästebuch führt, ausgegeben.

Wurden keine Daten an das Script übermittelt, wird der else-Anweisungsblock ausgeführt. In diesem wird zunächst die Datei gbuch.txt zum Lesen geöffnet und alle Zeilen der Datei in die Liste @comments geladen. Natürlich wird die Datei danach wieder freigegeben. Nachdem der HTML-Header und ein paar Zeilen HTML-Quelltext ausgegeben wurden, folgt eine foreach-Schleife. Da jede Zeile der Datei gbuch.txt einem Element der Liste @comments entspricht, wird nun also jede Zeile bzw. jeder Gästebucheintrag einzeln abgearbeitet.

foreach(@comments)  
{  
  my @parts = split(/\|/,$_);  
  print "<tr><td><a href=\"mailto:$parts[1]\">  
           $parts[0]</a>:</td><td>$parts[2]</td></tr>";  
}

Das gerade aktuelle Element wird mit der split-Funktion anhand der Trennlinie | in drei Teile zerlegt und der Liste @parts zugewiesen. Das erste Element der Liste enthält dann den Namen, das zweite die E‑Mail-Adresse und das dritte den eigentlichen Kommentar. Der Name und die E‑Mail-Adresse werden zusammen als Link ausgegeben, der Kommentar folgt danach in einer extra Zelle. Nachdem alle vorhandenen Gästebucheinträge ausgegeben worden sind, folgt schlussendlich der Rest des HTML-Dokuments inklusive des Formulars zum Eintragen in das Gästebuch.

Freilich ist dieses Gästebuch-Script nicht frei von Sicherheitslücken und sollte so niemals im Internet verwendet werden. Jedoch demonstriert es ganz deutlich die Arbeitsweise der Steuerkommandos zum Öffnen einer Datei und zeigt, wie sowohl lesend als auch schreibend auf eine Datei zugegriffen werden kann.

Das Gästebuch im Browser Abbildung 7.1: Das Gästebuch im Browser

# Datei-Uploads

Mit HTML-Fomularen können Sie den Benutzern einer Webseite auch das Hochladen von Dateien auf den Server ermöglichen. Dafür sind jedoch ein paar wichtige Details genauer zu betrachten.

Dass es Unterschiede zwischen Windows und UNIX/Linux gibt, ist bekannt. Diese gibt es auch bei der Behandlung von Dateien. Während UNIX/Linux lediglich Binärdateien kennt, unterscheidet Windows zwischen Textdateien und Binärdateien. Unter Windows liegen Grafiken in binärer Form vor und müssen auch so behandelt werden. Dateien, die reinen lesbaren Text enthalten, sind jedoch als Textdateien zu behandeln. Der Grund ist unter Windows die Behandlung des Zeichens für ein Zeilenende. UNIX/Linux kennt lediglich LF (engl. line feed, dt. Zeilenvorschub/Zeilenumbruch). Dies ist auch bei Binärdateien üblich. Textdateien besitzen unter Windows am Ende einer Zeile jedoch die Zeichenfolge CR (engl. carriage return, dt. Wagenrücklauf) und LF. Perl verfügt über einen Automatismus, der die Zeichenfolge CR/LF in ein einfaches LF beim Lesen und ein LF in CR/LF beim Schreiben einer Datei umwandelt. Damit dieser Automatismus jedoch nicht bei Binärdateien greift, müssen Sie diesen deaktivieren. Die Anweisung dafür lautet binmode (engl. binary mode). Dahinter wird das Datei-Handle notiert oder die Variable, die den Inhalt der Datei enthält.

binmode (handle||string)

Diese Anweisung muss aber sowohl auf die Datei angewendet werden, in die geschrieben werden soll, als auch auf die Datei, die per HTML-Formular übertragen worden ist.

Damit der Benutzer nun aber auch eine Datei auswählen kann, müssen Sie eine spezielle Form des input-Elements verwenden. Als Wert für das Attribut type wird file notiert. Außerdem sollten Sie noch zwei weitere Attribute setzen, und zwar accept und maxlength. Mit dem accept-Attribut können Sie festlegen, welche Dateitypen übertragen werden dürfen. Aus Sicherheitsgründen sollten Sie den Typ text/* oder image/* notieren. Dadurch können nur Textdateien oder Grafiken an den Server übertragen werden. Mit dem Attribut maxlength können Sie die maximale Größe der Datei festlegen. Die Angabe erfolgt in Bytes.

<input type="file" name="file_upload" accept="text/*" maxlength="2097152">

Der Browser zeigt nun in der Regel ein Eingabefeld mit einer Schaltfläche zum Auswählen einer Datei an. Die Größe der Datei ist übrigens auf 2 MB beschränkt.

Für das form-Element sollten Sie zusätzlich das enctype-Attribut notieren und als Wert multipart/form-data angeben, damit die Daten korrekt formatiert übermittelt werden.

Das vollständige Script sieht folgendermaßen aus:

#!/usr/bin/perl -w
use CGI qw(:standard);

if(param())
{
  my $file_upload = param("file_upload");
  my $filename = "d:/foxserv/www/upload/file_".time.".tmp";
  
  open(FILE,">$filename") || die("Fehler beim Schreiben!");
  
  binmode $file_upload;
  binmode FILE;
  
  read($file_upload,my $data,$ENV{'CONTENT_LENGTH'});
  print FILE $data;
  close(FILE);
  
  print header();
  print "<html><head><title>Dateiupload</title></head><body>";
  print "<h2>Datei wurde unter $filename hochgeladen</h2>";
  print "<h4>Inhalt der Datei:</h4>";
  print "<pre>--- BEGIN OF $filename ---\n$data\n---- END OF $filename ----</pre>";
  print "</body></html>";
}
else
{
  print header();
  print "<html><head><title>Dateiupload</title></head><body>
         <form action=\"$PROGRAM_NAME\" method=\"post\" enctype=\"multipart/form-data\">
         <input type=\"file\" name=\"file_upload\" accept=\"text/*\" maxlength=\"2097152\"><br>
         <input type=\"submit\">
         </form></body></html>";
}

Listing 7.5: Beispiel für den Upload von Dateien mit Perl und CGI

Auch in diesem Script wird zu Beginn überprüft, ob Daten an das Script übermittelt wurden. Wurde also eine Datei übertragen, wird der if-Anweisungsblock ausgeführt. Zuerst wird im Skalar $file_upload der Inhalt des Formularfelds file_upload gespeichert. Anschließend werden der Name und der Pfad der anzulegenden Datei festgelegt und im Skalar $filename hinterlegt. Die Funktion time ermöglicht in diesem Fall einen eindeutigen Dateinamen. Mit der Anweisung

open(FILE,">$filename")

wird dann die Datei geöffnet, in der die übermittelte Datei gespeichert wird.

Der Grund, warum nicht der Originaldateiname verwendet wird, ist der mögliche Schaden, der entstehen könnte. Wäre die Datei, die übertragen wird, z. B. ein Perl- oder PHP-Script, könnte jemand auf Ihrem Server Quelltext ausführen und an Passwörter oder Ähnliches herankommen. Dateien, die auf .tmp enden, sind in aller Regel nicht mit einem Programm verknüpft und können, egal welchen Inhalt sie beherbergen, keinen Schaden anrichten.

Anschließend werden sowohl die empfangene als auch die zu schreibende Datei in den Binärmodus geschaltet.

binmode $file_upload;  
binmode FILE;

Der wichtige Teil des Scripts folgt nun:

read($file_upload,my $data,$ENV{'CONTENT_LENGTH'});

Um den Inhalt der übertragenen Datei lesen zu können, müssen Sie die Funktion read verwenden. Die Variable $file_upload wird dann zum Identifizieren des Eingabekanals verwendet. Der Inhalt der Datei wird im Skalar $data gespeichert. Der Inhalt wird hinterher mit der Anweisung

print FILE $data;

in die Zieldatei auf dem Server geschrieben und mit

close(FILE);

wieder geschlossen. Die nachfolgenden print-Anweisungen geben lediglich ein HTML-Dokument aus, das noch mal den übermittelten Inhalt in einem pre-Element darstellt und über den Erfolg des Vorgangs informiert.

Wurden keine Daten übermittelt, wird ein entsprechendes Formular für den Upload ausgegeben.

Vollkommene Sicherheit bietet auch dieses Script nicht. So wird z. B. nicht auf einen gültigen Dateinamen geprüft. Auch die Endung sollte sicherheitshalber überprüft werden, und es sollten nur bestimmte Endungen zugelassen werden, wie .txt, .gif, .jpg, .png und .dat. Diese Endungen sind in der Regel ungefährlich.

# Zusammenfassung

  • Mit den Funktionen opendir, readdir und closedir können Sie auf Verzeichnisse zugreifen und deren Verzeichniseinträge auslesen. Die Ansteuerung erfolgt dabei über Verzeichnis-Handles.
  • Eine Rekursion dient der Lösung einer Aufgabe. Die Aufgabe wird dabei in kleinere Aufgaben unterteilt, und diese werden immer wieder an die gleiche Funktion zur Lösung vergeben.
  • Verzeichnisse werden mit chdir gewechselt, mit mkdir erstellt und mit rmdir gelöscht.
  • Zum Lesen oder Schreiben einer Datei werden die Befehle open und close sowie print benötigt. Je nachdem, wie auf eine Datei zugegriffen werden soll, müssen entsprechende Steuerzeichen vor dem Pfad- und Dateinamen notiert werden.
  • Für Datei-Uploads sollten Sie dringend beachten, dass beide Dateien – Quell- und Zieldatei – in den binären Modus geschaltet werden müssen. Dies ermögicht die Anweisung binmode.

# Fragen und Übungen

  1. Schreiben Sie ein Script, das den Inhalt eines Verzeichnisses einliest und dabei nur Dateien berücksichtigt. Die gefundenen Dateien sollen einer Liste hinzugefügt, sortiert und ausgegeben werden.
  2. Welche Steuerkommandos gibt es für den Zugriff auf Dateien?
  3. Erweitern Sie das Script aus Frage/Übung 1 um eine Rekursion. Dadurch sollen auch alle Dateien der Unterverzeichnisse ausgelesen und ausgegeben werden – jedoch nur Dateien!