Folgende Zeilen sollten jedes Perl-Script einleiten:
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use 5.010;
Die erste Zeile sollte klar sein.
use strict;
führt die Behandlung ein, dass Variablen nicht einfach aus der Luft erzeugt werden, sondern immer deklariert werden müssen. Dieser Extraaufwand an Tipperei hilft, Vertippern vorzubeugen und erleichtert das Debugging enorm.
use warnings;
zeigt zusätzlich Warnungen an, die auf übliche Programmierfehler hinweisen können, wenn perl
zum Beispiel Programmcode als gültig erachtet, aber durch den Datenfluss ungültige Daten (undef
-Werte in Stringverkettungen oder dergleichen) im Programm vorkommen. Derartige Probleme sind ebenfalls schwer zu debuggen bei komplexeren Datenflüssen.
use utf8;
informiert perl
darüber, dass der Quelltext des Scriptes, also die Scriptdatei selber in UTF-8 codiert sind. Damit kann man innerhalb des Scriptes Sonderzeichen und Umlaute in Strings oder RegExes verwenden und diese werden intern korrekt behandelt. Aber Achtung! use utf8;
hat nichts mit der Ein- oder Ausgabe des Scriptes selber zu tun. Es handelt sich wirklich nur darum, festzulegen, wie die Scriptdatei selber codiert und behandelt wird. Das Encoding für Ein- und Ausgaben muss gesondert behandelt werden, je nach dem, wo die Daten herkommen oder wo die Daten hin gehen (Dateien, Netzwerk/HTTP, Datenbank). Außerdem muss selbstverständlich die Scriptdatei dann auch von Texteditor, den man verwendet, in UTF-8 geschrieben werden, sonst macht das ja auch wieder keinen Sinn.
use 5.010;
wird dazu verwendet, um eine Mindestversion von Perl vorzugeben, mit der das Script laufen soll. bei 5.010
bricht das Script mit einer Fehlermeldung ab, wenn eine Version von Perl kleiner als 5.10.0 verwendet wird. 5.10.0 sollte aber mittlerweile auf jedem System zur Verfügung stehen. Ist das nicht der Fall, sollte man sich über ein Update gedanken machen, da 5.10.0 bereits über 7 Jahre alt ist und seit dem einige wichtige Änderungen in Perl hinein geflossen sind.
Folgende Module sind im Perl-Core mindestens seit Version 5.10.0 enthalten und erleichtern die Arbeit mit Dateien für portable und stabile Scripte. Für Scripte, die mit Dateien arbeiten müssen, sollten unbedingt diese Module verwendet werden. Irgendwelche Hacks mit Shellaufrufen und dergleichen sollten vermieden werden, da diese sehr stark vom Betriebssystem, der Umgebung und der Verfügbarkeit von Systemtools und Shells abhängen. Außerdem sind solche Hacks gegenüber Veränderungen der Umgebung sehr anfällig und es ist sehr schwer, mit derartigen Hacks stabile und zuverlässige Scripte mit einer verlässlichen Fehlerbehandlung zu erstellen. Also wenn Dateien behandelt werden sollen, ist man immer auf der sicheren Seite, wenn man sich der folgenden Module bedient.
use File::Spec;
use File::Temp;
use FindBin;
use File::Copy;
use File::Path;
Der Aufrufpfad zum Script kann mit der Variable $0
abgefragt werden. Oftmals benötigt man jedoch den Pfad zum Script, zum Beispiel um mit Dateien und Verzeichnissen relativ zum Scriptpfad zu arbeiten. Dafür gibt es das Modul FindBin
. Diese Modul liefert die Variable $FindBin::Bin
, welche die Pfadangabe (absolut oder relativ, je nach dem, wie das Script aufgerufen wurde) zur Scriptdatei liefert. Um Tipparbeit zu sparen, kann man sich $FindBin::Bin
auch einfach in seinen Namensraum importieren, das geht dann so:
use FindBin qw(Bin);
print qq(Das Script liegt im Verzeichnis "$Bin");
Wie binde ich einen Pfad zu Perl-Modulen unterhalb meines Projektverzeichnisses portabel ein. Im Beispiel heißt der Modulpfad projlib
und befindet sich direkt unterhalb des Pfades, in dem das Perlscript selber liegt.
use FindBin qw(Bin);
use File::Spec qw(splitpath catfile);
use lib catfile(splitpath($Bin), 'projlib');
Als Erweiterung des vorhergehenden Beispieles hier noch der Fall, wenn das Script, welches aufgerufen wird, selber in einem Unterverzeichniss im Projektordner liegt (im Beispiel projbin
). Die Bibliothek liegt ebenfalls in einem Unterverzeichniss parallel zu projbin
.
use FindBin qw(Bin);
use File::Spec qw(splitpath catfile);
use lib catfile(splitpath($Bin), '..', 'projlib'); # FIXME: Ist das so richtig, dass da einfach '..' drin steht, oder gibt es da etwas portableres?
Um Dateiinhalte zu lesen, verwendet man die Funktion open
. Diese Funktion kann mit zwei oder drei Argumenten aufgerufen werden. Die zwei-Argumente-Version sollte jedoch nur für Piping verwendet werden und dann auch nur, wenn man weiß, was man tut. Für Interprozesskommunikation gibt es hier stattdessen das viel sicherere und stabilere Modul IPC::Open2 oder IPC::Open3 sowie IPC::Cmd. Zum Einlesen von Dateien wird das Dreiargumente-open
verwendet.
Das erste Argument ist das Filehandle, hier kann ein Glob verwendet werden, ein globales Objekt, welches oft großgeschrieben wird und ohne Sigil (Variablenanfangssonderzeichen) daher kommt. Üblicher ist jedoch, ein lokales Filehandleobjekt in einem Scalar zu verwenden. Dieses kann direkt im open
-Aufruf mit my $filehandle
eingebaut werden. Auf Filehandleobjekte kann genau wie auf Globs mit my $line = <$filehandle>;
zugegriffen werden. Außerdem wird die Datei automatisch geschlossen, wenn das Filehandleobjekt aus seinem Scope raus fällt. Das kann man sich zu Nutze machen, um Dateioperationen in einem eigenen Block abzufackeln und das abschließende close
dabei implizit über das Scopeende erledigen zu lassen, allerdings ist diese Praxis nicht so aussagekräftig und sollte für gut wartbaren Code vermieden werden.
Danach kommt die Lese- oder Schreibrichtung: '<'
für Lesen, '>'
für Schreiben. Diese sind das eine Extra-Argument, was gegenüber dem Zweiargumente-open
hinzu kommt. Dieses Argument wird extra betrachtet und vom eigentlichen Dateinamen getrennt, da man so sicher sein kann, dass im Dateinamen keine Ausgabeumleitungszeichen und der gleichen für die Shell enthalten sind, die ggf. den Programmlauf stören oder böse Dinge tun, wenn der Dateiname von außen in das Script kommt.
Als letztes kommt der Pfad zur gewünschten Datei.
Wichtig ist die Fehlerbehandlung mit or die qq(Datei "$file" konnte nicht geöffnet werden: $!);
ganz am Ende des Aufrufs. Das niedrig priorisierte or
ist hierbei ein Short-Circuit. Sollen Fehler ignoriert werden, so ist das an der Stelle des or
nach dem open
der falsche Weg, da dass Programm dann an der Stelle weiter läuft, Warnungen wirft und mit falschen Werten arbeitet. Fehlerbehandlung muss daher für den umschließenden Block eingebaut werden zum Beispiel mit dem Modul Try::Tiny aus dem CPAN oder mit dem Block-eval
, falls das Modul nicht zur Verfügung steht.
close
Arbeitet mit dem Filehandle und schließt dieses. <$filehandle>
liest jeweils einen Abschnitt bis zum Trennerzeichen. Das Trennerzeichen ist standardmäßig auf Zeilenumbruch oder Dateiende eingestellt und kann mit $/
geändert werden ($/
kann man sich merken, weil der /
ist eine Rutsche und die Rutsche geht nach innen, hinein in das Scalarsigil).
sub my_configopen {
my $file = shift;
open my $filehandle, '<', $file
or die qq(Could not read file "$file": $!);
my @output;
while ( my $line = <$filehandle> ) {
next if $line =~ m/\A\s*\#/xmsi; # schmeißt Kommentare mit '#' am Anfang raus
chomp $line;
push @output, $line;
}
close $filehandle;
return @output;
}
print join ' : ', my_configopen('/etc/specialprogram.conf');
Um eine Datei komplett einzulesen, kann man den Eingabeseperator $/
auf undef
setzen. Dies sollte man aber nur im lokalen Scope tun, um Seiteneffekte zu vermeiden, die andere Programmteile stören. Danach funktioniert <$filehandle>
so, dass es den Inhalt der Datei komplett in einem Rutsch einliest. Als Scope kann man zum Beispiel das Block-do
verwenden, welches einen Scope erzeugt und die Rückgabe des letzten Statements direkt ausgibt. Mittels local $/;
wird der Eingabeseperator innerhalb des Scopes und in den in ihm aufgerufenen Funktionen und Routinen auf undef
gesetzt. Das ist ein Shortcut für local $/ = undef;
.
my $filecontent = do {
local $/;
open my $filehandle, '<', '/tmp/dummyfile.txt'
or die "Could not open dummy file: $!";
<$filehandle>;
};
Alternativ gibt es auf CPAN auch das Modul File::Slurp, welches diesen Zweck und noch viele weitere erfüllt.
Jedes Perl-Programm hat automatisch Zugriff auf bestimmte globale Variablen. Eine der gängigsten ist dabei die Variable, welche Umgebungsvariablen des Systems bereit stellt. Diese Variable ist ein Hash und heißt %ENV
. Ein Hash deswegen, klar, weil das die Datenstruktur ist, in der Umgebungsvariablen als Schlüssel-Wert-Paare am besten dargestellt werden können. Auf die Daten der Variable kann einfach wir auf die Daten jedes anderen Hashes zugegriffen werden, $ENV{HOME}
liefert also das Home-Verzeichnis des Benutzers.
%ENV
kann auch verändert werden, um zum Beispiel die Umgebung für das Perl-Script selber, andere Teile oder Module des Scriptes oder gar externe Programmaufrufe anzupassen. Die Änderungen gelten dann aber nur für das Script selber und beim Beenden des Scriptes stehen die Werte der Systemumgebung wieder so zur Verfügung, wie vor dem Aufruf des Scriptes.
Beim Veränderung von Umgebungsvariablen innerhalb des Scriptes wird jeder dringend empfohlen, die Werte dynamisch in einem eigenen abgeschlossenen Scope mittels local
zu verändern, um eventuelle unbeabsichtigte Seiteneffekte zu vermeiden, welche subtile und schlecht zu findenende Fehler in das Programm einbringen. Die folgende Subroutine nimmt einen Dateinamen entgegen, den es auf nicht näher bekannte Weise in einen Pfad verwandelt. Dann soll diese Datei im Texteditor 'vim' geöffnet werden. Für das Öffnen der Datei steht uns eine Subroutine call_editor_on()
zur Verfügung, welche den in der Umgebungsvariable "EDITOR" hinterlegten Texteditor mit dem angegebenen Pfad öffnet. Damit diese Funktion aber den Texteditor 'vim' statt vielleicht 'emacs' verwendet, muss vorher die Umgebungsvariable umgesetzt werden. Das machen wir in einem extra Block { ... }
, in dem wir den Hash-Wert $ENV{EDITOR}
mit local
verändern. Dieses Vorgehen mit extra Scope und local
dient dazu, damit bei späteren Aufrufen von call_editor_on()
die vom System vorgegebenen Umgebung wieder besteht und es zu keinen Überraschungen kommt.
sub edit_with_vim {
my $file = shift;
my $path = prepare_file($file);
{
local $ENV{EDITOR} = 'vim';
print "Textdatei in VIM öffnen\n";
call_editor_on($path);
}
print "Textdatei mit dem voreingestellten Editor öffnen\n";
call_editor_on($path);
}
Um Verzeichnisinhalte auszulesen gibt es zwei Wege. Zum einen ist da glob
(welches auch über den Operator <>
verfügbar ist, wie in <"*.txt">
). Der Operator <...>
ist bereits bekannt, um Dateiinhalte auszulesen. Das tut er, wenn ihm ein Filehandle übergeben wurde, dann ruft der im hintergrund readline
auf. Wird dem Operator jedoch ein String übergeben, dann wird dieser als File-Pattern ähnlich wie an einer Shell. Es kann nur ein String übergeben werden, dieser kann aber, durch Leerzeichen getrennt, mehrere Patterns enthalten, welche als "Oder-Verknüpfung" ausgeführt werden. Das Beispiel verdeutlicht das. glob
oder <...>
gibt eine Liste von Dateieinträgen zurück und arbeitet im aktuellen Arbeitsverzeichnis, welches mit chdir
gewechselt werden kann.
chdir "$ENV{HOME}/Bilder";
my @jpegs = <"*.jpg *.jpeg">;
Dieses Beispiel wechselt in das Verzeichnis "Bilder" unterhalb des eigenen Home-Verzeichnisses und liefert eine Liste der Dateien, die auf ".jpg" oder ".jpeg" enden. Die Liste der Dateien wird im Beispiel in @jpegs
gespeichert und enthält nur die betreffenden Verzeichniseinträge (Dateinamen).
Um Verzeichnisinhalte auszulesen kann man außerdem die Funktionen opendir
, readdir
und closedir
verwenden. Das Beispiel macht im Prinzip das selbe, wie eben, nur kann man hier eben noch viel mehr Programmlogik unterbringen, wenn dafür Bedarf besteht, ich denke, das sieht man recht schnell. Außerdem ist opendir
nicht auf das aktuelle Arbeitsverzeichnis beschränkt und verändert dieses auch nicht im Programmkontext. Außerdem wird, wie beim Einlesen von Dateien, das Verzeichnishandle automatisch geschlossen, wenn es aus dem Scope fällt. Ein extra closedir
entfällt, wird aber dennoch empfohlen, damit das Programm klarer bleibt.
my @jpegs;
opendir my $dirhandle, $path
or die qq(Could not open path "$path": $!);
while ( my $file = readdir $dirhandle ) {
next unless $file =~ m/\A\./xms; # Versteckte Dateien werden ignoriert
next unless $file =~ m/\.jpg\z/xms or $file =~ m/\.jpeg\z/xms; # Ja, das geht auch schöner, aber so ist es deutlicher zu sehen
next unless -r catfile($path, $file); # Hier schaue ich, ob ich für diese Datei überhaupt Leseberechtigung habe, sonst wird die auch ignoriert
push @jpegs, $file;
}
closedir;
Dateien werden automatisch angelegt, wenn sie geöffnet und beschrieben werden.