Häufige Fehler und Fallen
Inhalt:
Einleitung
Dieser Artikel listet Programmierfehler auf, die häufig auftreten und im Forum oft beobachtet werden.
Zum Einen gibt es verbreitete Anfängerfehler. Zum Anderen gibt es aber auch Fehler, die auf (teilweise historisch bedingte) Eigenheiten von Perl zurückgehen, die gelegentlich auch Fortgeschrittenen nicht vollständig bewusst sind.
Reguläre Ausdrücke
Ende des Strings
Das Ende des Strings findet man mit
\z und nicht mit
$ (Dollar). Eine ausführlichere Beschreibung dieses Unterschiedes steht in "
Was bedeutet $ wirklich?"
$1
Wenn man $1 benutzt, muss man vorher sicherstellen, dass es auch gesetzt wird. Bei einem erfolglosen regulären Ausdruck wird es nicht gesetzt und so gelassen, wie es vorher war.
Folgendes Konstrukt ist also zu vermeiden:
$var =~ /(\d)/;
print $1;
Mehr dazu (incl. verschiedener Lösungsansätze) steht bei "
Warum bekomme ich Treffer anzeigt, obwohl der Ausdruck nicht matchen kann?"
Ferner sollte man beachten, dass $1 nicht lange gültig ist:
$var = /(\d)/ or die "no match";
foobar();
print $1;
Mit dem "die" wird hier zwar der oben beschriebene Fehler vermieden. Hier gibt es aber ein neues Problem: Falls in der Subroutine foobar() reguläre Ausdrücke benutzt werden, wird $1 verändert, da es eine globale Variable ist. Besser ist daher:
$var = /(\d)/ or die "no match";
my $match = $1;
foobar();
print $match;
Oder ganz ohne $1:
my ($match) = /(\d)/ or die "no match";
foobar();
print $match;
Variablen in regulären Ausdrücken
Achtung bei einbetten von Variablen in regulären Ausdrücken:
if ($passwort =~ /^$eingabe\z/) {
showGeheimeDaten();
}
Hier wird
$eingabe als regulärer Ausdruck gewertet. Wenn
$eingabe also
"?" oder
"(?s:).*" ist, wird der Ausdruck unabhängig von
$passwort immer matchen!
Um dies zu vermeiden, kann man
/^\Q$eingabe\E\z/ schreiben (zwischen "\Q" und "\E" werden Sonderzeichen in Variablen nicht ausgewertet).
Bei diesem Beispiel ist es allerdings noch sinnvoller, einfach
$passwort eq $eingabe zu schreiben.
Abgesehen davon, dass reguläre Ausdrücke auf unerwartende Sachen matchen können, können fehlerhafte Regexes wie "(" auch zu einer Exception bzw. einem Programmabbruch führen. Ferner ist es auch möglich, mit regulären Ausdrücken Code auszuführen, das lässt sich aber nur dann ausnützen, wenn "use re 'eval';" aktiv ist.
Allgemeines
Fehlerbehandlung
Viele Anweisungen können fehlschlagen. Am erwähnenswertesten sind davon (vor allem in Anfängerskripten) jegliche Arten von Dateizugriffen. Insbesondere "open" kann in diversen Situationen fehlschlagen, wie beispielsweise fehlende Dateien oder fehlende Berechtigungen.
Daher muss man immer eine Fehlerbehandlung einbauen. In vielen Fällen reicht es, das Skript (wie im Codebeispiel) mit "die" abzubrechen, jedoch sollte man sich immer Gedanken machen, ob es in manchen Situationen sinnvoller ist, auf andere Weise auf Fehler zu reagieren.
open my $fh, '<', $file or die "open: $file: $!";
# oder:
open (my $fh, '<', $file) || die "open: $file: $!";
Im zweiten Beispiel sind die Klammern wichtig, da "||" stärker bindet als "or".
Wenn beim Schreiben in Dateien die Festplatte voll wird, kann "print" fehlschlagen, jedoch kann (wegen der Pufferung) der Fehler auch erst bei "close" auftreten. Daher ist es empfehlenswert, beides auf Fehler zu prüfen.
Beim Schreiben auf ein Terminal ist Fehlerüberprüfung im Allgemeinen nicht wichtig oder sinnvoll. Jedoch kann man sich nicht darauf verlassen, dass STDOUT auf ein Terminal geht. Wenn es die Chance gibt, dass jemand die Ausgabe Deines Scriptes in eine Datei umleitet, kann es nicht schaden, "print" auf Fehler zu prüfen. Wegen der schon erwähnten Pufferung sollte man in dem Fall auch explizit
close STDOUT or die $!; schreiben, da ansonsten Fehler unentdeckt bleiben können.
Gerade bei einer voll gewordenen Festplatte möchte derjenige, der das Script ausführt, gerne darauf hingewiesen werden, dass nicht alle Daten geschrieben werden konnten. Ansonsten geht er davon aus, dass alle Daten in der Datei gelandet sind, was desaströse Auswirkungen haben kann. Beispiel:
sort < datei1 > datei2 && rm datei1
Wenn "sort" vernünftig auf Schreibfehler (auch bei STDOUT) prüft, wird die Originaldatei nicht gelöscht, wenn die Festplatte voll wird. Falls sie aufgrund von mangelhalfter Fehlerprüfung doch gelöscht wird, sind alle Daten futsch.
Lesen aus Datei und $_
Oft zu sehen ist ein solches Konstrukt:
while (<F>) {
...
}
Hierbei wird jede Zeile im Schleifenkörper über
$_ zugänglich, ähnlich wie bei einer foreach-Schleife. Jedoch (im Gegensatz zur foreach-Schleife) wird
$_ am Ende nicht wieder auf den vorherigen Wert zurückgesetzt. Da
$_ eine globale Variable ist, kann dies in Einzelfällen verheerende Auswirkungen haben.
Man betrachte folgendes Beispiel:
use strict;
use warnings;
sub isblacklisted {
my ($word) = @_;
open my $fh, '<', 'blacklist.txt' or die $!;
while (<$fh>) {
chomp;
return 1 if $_ eq $word;
}
return 0;
}
my @words = qw(Hallo Welt);
@words = grep { !isblacklisted($_) } @words;
print "@words";
Selbst wenn "blacklist.txt" eine leere Datei ist, gibt dieses Script nichts aus. Genauer gesagt werden ein Leerzeichen und aufgrund von "use warnings" nur folgende Warnungen ausgegeben:
Use of uninitialized value in join or string at badscript.pl line 17.
Use of uninitialized value in join or string at badscript.pl line 17.
Was ist hier passiert? Die magische while-Schleife in "sub isblacklisted" hat die implizite Variable von "grep" und damit alle
@words auf "undef" gesetzt.
Daher sollte man stets eine der beiden Abhilfen verwenden:
local $_;
while (<$fh>) { tu_was_mit($_); }
# Oder noch besser:
while (my $line = <$fh>) { tu_was_mit($line); }
Eine ausführliche Diskussion zu diesem Thema gibt es in
diesem Thread.
--
ChristophBussenius - 16 Aug 2008