Tuesday, September 29, 2009

Daten Scheibchenweise...

Hier mal wieder ein Artikel aus einer früheren Ausgabe des Perl-Magazins "$foo":

Daten Scheibchenweise...

Ein nützliches Feature von Perl ist es, sogenannte "Slices" verwenden zu können. Wörtlich übersetzt heißt es "Scheiben". Diese "Scheiben" sind bei Listen/Arrays häufig gewünscht.

Slices im Alltag

Folgende Situation: Eine Person A war beim Arzt und benötigt Medikamente. A geht zur Apotheke X, um diese Sachen zu besorgen. X ist wie ein großes Array (1. Element: Nasenspray, 2. Element: Aspirin,...). A möchte natürlich nicht die ganze Apotheke (nicht das ganze Array) haben, sondern nur einzelne Sachen daraus. A hat jetzt zwei Möglichkeiten:

1. A sagt dem Apotheker ein Medikament, lässt den Apotheker das Medikament holen, nennt danach das nächste Element und so weiter bis A alle Sachen bekommen hat.

2. A gibt dem Apotheker eine Liste mit den Arzneinamen und lässt den Apotheker alle Medikamente holen. Der Apotheker kommt mit einem ganzen Korb voll mit den Arzneipäckchen.

Slices in Perl

So etwas kann ganz einfach in Perl nachgebildet werden, wie die Listings x-y zeigen.

# die Apotheke
my @apotheke = ('Nasenspray', 'Aspirin', 'Hustensaft', 'Allergietabletten');

# A benötigt folgende Medikamente
# hier sind aber die Arzneinamen schon
# durch den Index in der Apotheke ersetzt
my @rezept = (0,2);

# A gibt dem Apotheker und bekommt den Korb voll mit Medikamenten
my @korb = @apotheke[ @rezept ];

print $_,"\n" for @korb;

Beim Heraussuchen der Slices ( @apotheke[ @rezept ]; ) muss das "@" vor apotheke stehen, weil diese Operation eine Liste zurückliefert (im Gegensatz zu dem Zugriff auf einzelne Elemente - $apotheke[$index]).

Wichtig ist auch, dass die Elemente in der gleichen Reihenfolge zurückgeliefert werden, wie die Indexliste ist, in diesem Fall also "Nasenspray" und dann "Hustensaft". Wäre das Rezept anders herum ( my @rezept = (2,0) ), wäre zuerst der Hustensaft im Korb und dann das Nasenspray.

Hashslices

Analog zu den Arrayslices oben gibt es in Perl auch noch die Hashslices, die aber im Prinzip genauso funktionieren. Hier eine etwas andere Situation:

Ein Rechtsanwalt kommt in das Gerichtsarchiv und möchte die Unterlagen zu mehreren Fällen einsehen. Dazu hat er sich in seiner Kanzlei schon eine Liste mit Aktenzeichen angefertigt. Auch hier hat der Anwalt wieder die zwei Möglichkeiten, den Archivar einzeln losschicken oder dem Archivar einfach die Liste geben und der Archivar kommt mit einer ganzen Ladung an Aktenordnern zurück. Die erste Möglichkeit würden den Archivar wahrscheinlich ärgern, die zweite Möglichkeit ist aber optimal.

Das ganze in Perl:

my %archiv = (
az0815 => 'Akte zum Aktenzeichen 0815',
az2373 => 'Akte zum Aktenzeichen 2373',
az2371 => 'Akte zum Aktenzeichen 2371',
);

my @liste = qw(az0815 az2371);

my @akten = @archive{ @liste }

Slices als lvalue

Die Slices kann man nicht nur für das Auslesen von Daten verwenden, sondern auch für Zuweisungen. Soll zum Beispiel ein Hash verwendet werden, der später für eine reine Existenzüberprüfung dient, kann man das so machen:

my @keys = qw(key1 key2 key3);
my %hash;

@hash{ @keys } = (1) x scalar @keys;

Jetzt wird für jeden Schlüssel ein Eintrag im Hash erstellt. Jeder Schlüssel soll den Wert "1" erhalten. Deswegen muss eine Liste mit "1"en zugewiesen werden.

Ein häufiger Fehler, der gemacht wird, ist folgender:

@hash{ @keys } = 1;

Hier wird aber nicht für jeden Schlüssel ein "1" gespeichert, sondern nur für den ersten Schlüssel. Die restlichen Schlüssel erhalten den Wert "undef". Mit dem "(1) x scalar @keys" wird eine Liste erzeugt, die genauso lang ist wie die Liste der Schlüssel.

Analog funktioniert es für Arrayslices.

my @array = (1..10);
@array[2,4,6] = (99,98,97);

Hier werden in dem Array die Elemente mit den Indizes 2,4,6 durch die Zahlen in der zugewiesenen Liste ersetzt. $array[2] ist dann 99, $array[4] ist dann 98 und so weiter.

Wednesday, September 23, 2009

Weitere Hooks: Perl::Tidy und Mercurial

Als Reaktion auf meinen "Perl::Critic-Regeln durchsetzen"-Artikel hat Roland Schmitz ja schon den Hook für Mercurial als Kommentar gepostet. Vor ein paar Minuten hat er mir noch einen Hook geschickt, den er - ebenfalls mit Mercurial - einsetzt, um Perl::Tidy-Regeln einzufordern:


Und in der hgrc des Projekts dann das hier:
[hooks]
pretxncommit.msglen = test `hg tip --template {desc} | wc -c` -ge 10
pretxncommit.perl_critic = ~/bin/critic_check.pl
pretxncommit.perl_tidy = ~/bin/tidy_check.pl

Danke an Roland, dass ich das posten durfte.

Tuesday, September 22, 2009

Noch mehr zu "open"...

In meinem letzten Blog-Post bin ich ja schon auf verschiedene Sachen bezüglich der Perl-Funktion open eingegangen. Das bezog sich aber alles auf mögliche Probleme.

Mit dem heutigen Blog-Post möchte ich noch zwei Themen ganz kurz ansprechen, die open sehr mächtig machen:

I/O-Layer

Seit Perl 5.6.0 gibt es sogenannte I/O-Layer, mit denen man die Daten durch einen "Umwandler" schicken kann. Mit den I/O-Layern kann man z.B. sehr einfach Dateien in ISO-8859-1 sehr leicht in UTF-8 umwandeln:

open my $fh, '<:encoding(iso-8859-1)', $file or die $!;
open my $ofh, '>:encoding(utf-8)', $new_file or die $!;
print $ofh $_ while <$fh>;
close $ofh;
close $fh;

So etwas verwende ich teilweise in der Vorbereitung für neue Ausgaben vom Perl-Magazin.

In einigen alten Quellen wird als utf8-Layer auch einfach ":utf8" verwendet. Das kann aber Probleme machen, da die Daten nicht richtig (de-)kodiert werden, sondern einfach das UTF-8-Flag setzt.

Sollen alle Aufrufe von open mit einem bestimmten I/O-Layer verwendet werden, kann man auch das open-Pragma verwenden:

use open ":encoding(utf-8)";

Mit den Layern kann man aber nicht nur mit "Zeichensätzen" arbeiten, sondern auch die Daten z.B. gleich verschlüsseln wenn sie in die Datei geschrieben werden und beim Auslesen entschlüsseln. Dafür gibt es dann Module wie PerlIO::via::CBC

open mit Pipe

Wofür man "open" noch verwende kann, ist eine einfache Art und Weise, mit einem anderen Programm zu "kommunizieren". Wie man also Daten an andere Programme schicken kann, die diese auf STDIN erwarten. Oder Werte von dem Programm empfangen, die dieses auf STDOUT schreibt. Dazu kann man die Pipe verwenden.

So kann man z.B. einfach Daten an ein Programm schicken:

open my $fh, '| /ein/programm.exe' or die $!;
print $fh "Mein Name\n";
# programm.exe erwartet einen Namen auf STDIN
close $fh;

Siehe auch:

Wednesday, September 16, 2009

Verschiedenes zu "open"...

Der heutige Blog-Post ist Code geschuldet, den ich in letzter Zeit anschauen durfte. Dabei ist mir immer wieder die Verwendung von open aufgefallen. In der freien Wildbahn - und leider auch in einigen Perl-Tutorials im Internet - ist diese Form von open sehr häufig anzutreffen:

open( FH, $datei );

Diese Form hat aber ein paar Probleme:


Globale Filehandles

Globale Filehandles haben ein Problem: Sie sind global! In "Spaghetticode" ist das häufig kein Problem, aber sobald der Einstieg in Subroutinen da ist, wird es oft zum Problem. Gerade Perl-Einsteiger verwenden immer wieder den gleichen Namen für das Filehandle - auch weil in den Tutorials eigentlich durchgängig FH genommen wird.

Um das Problem mit globalen Filehandles zu verdeutlichen, nehmen wir den folgenden Code:


Hier wird eine Datei eingelesen, die verschiedene Dateinamen enthält, die auch wieder eingelesen werden sollen usw. In readFile() wird die Datei wie oben beschrieben geöffnet. Der Filehandle verweist auf die geöffnete Datei. Dann wird die erste Zeile eingelesen und diese Datei wird dann im nächsten Aufruf von readFile() geöffnet. FH ist jetzt ein Filehandle, das auf diese zweite Datei zeigt. Dann wird FH geschlossen und der Code geht zurück zum ersten Aufruf von readFile(). Perl soll jetzt die nächste Zeile auslesen. Mittlerweile ist FH aber geschlossen worden. Dadurch erscheint die Fehlermeldung

readline() on closed filehandle ...

In diesem Code-Beispiel ist der Fehler schnell ersichtlich, aber was ist wenn das Öffnen der Datei über den langen Code verteilt ist? Dann ist die Fehlersuche nicht wirklich ein Spaß.

Was lernen wir daraus? Lexikalische Filehandles nehmen:
open( my $fh, $datei );

Der Code ist immer noch nicht wirklich gut, also weiter...

Mehr zum Thema Lexikalische Filehandles gibt es auch im Wiki von Perl-Community.de.

Lesend? Schreibend?

Das nächste Problem beim ursprünglichen Code ist, dass Perl hier nicht explizit gesagt bekommt, ob die Datei lesend oder schreibend geöffnet werden soll. Perl impliziert hier, dass die Datei lesend geöffnet werden soll.

Dieser Code


gibt also einfach nur die erste Zeile aus test.txt aus. Soweit noch kein wirkliches Problem. Ein Problem wird das, wenn ein Benutzer nach einem Dateinamen gefragt wird und dann z.B. "> test.txt" eingibt, so dass sich das hier ergibt:


Das ausgeführt bringt die Fehlermeldung

Filehandle $fh opened only for output at ...

Was, wenn der Benutzer "> /etc/passwd" oder ähnliches eingibt? Darum sollte man immer explizit angeben, wie die Datei geöffnet wird.

open( my $fh, '<', $datei ); # lesend öffnen
open( my $fh, '>', $datei ); # schreibend öffnen

In Perls die älter als Version 5.6.0 sind, ist diese 3-Argumenten-Form nicht möglich. Dort sollte man das dann so schreiben:

open( my $fh, "< $datei" ); # lesend öffnen
open( my $fh, "> $datei" ); # schreibend öffnen

Die 3-Argumenten-Form hat aber noch einen weiteren Vorteil: Man kann Dateien mit führenden oder angehängten Leerzeichen im Namen öffnen. Die alte Form schneidet einfach alle Leerzeichen vor und hinter dem Dateinamen ab.

Fehlerbehandlung

Es sollte zum guten Ton in jedem Programm gehören, dass man Fehler abfängt. Der ursprüngliche Code hat das Problem, dass Perl einfach weitermacht, egal ob ein Problem auftaucht oder nicht. Was könnten hier Probleme sein? Die Datei könnte nicht existieren oder das Programm darf die Datei wegen fehlender Rechte nicht öffnen.

Ein einfacher Weg der Fehlerbehandlung ist ein einfaches "or die $!". In $! steckt die Fehlerbeschreibung (siehe auch perldoc perlvar).

Oder man kann das open in eine if-Bedingung stecken und dann im else-Teil (wenn ein Fehler auftritt) eine eigene Fehlerbehandlung machen - z.B. Logging der Fehler.

Eine einfache Methode, Fehler automatisch zu behandeln, ist die Verwendung von autodie. Wie das eingesetzt wird, wird in Ausgabe 9 vom Perl-Magazin $foo beschrieben.

Das open würde jetzt zum Schluss so aussehen:

open( my $fh, '<', $datei ) or die $!;

Friday, September 11, 2009

Perl::Critic-Regeln durchsetzen...

Auf der diesjährigen FrOSCon habe ich im Perl-Raum Perl::Critic vorgestellt. Dabei habe ich auch kurz gezeigt, wie man diese Regeln durchsetzen kann. Eine Möglichkeit ist, die Überprüfung gleich beim Einchecken von Code durchzuführen - wenn denn ein Versionsverwaltungstool verwendet wird.

Das ist natürlich dann die ganz strenge Variante, aber es ist eine gute Variante, wenn man sichergehen will. In diesem Blog-Post zeige ich kurz, wie man mit einem SVN-Hook den commit vermeiden kann, wenn der Code nicht den eigenen Regeln entspricht.

Das ganze habe ich für einen SVN-Server unter Windows gemacht, sollte aber analog auch auf anderen Umgebungen funktionieren. Dort muss man statt des Batch-Skripts halt ein Shell-Skript machen.

Das Batch-Skript wird als "pre-commit.bat" im "hooks"-Verzeichnis des SVN-Servers gespeichert und sieht so aus:

Das Batch-Skript ruft also ein Perl-Skript auf. In diesem Perl-Skript wird dann die eigentliche Überprüfung des Codes vorgenommen. Da es ein "pre-commit"-Hook ist, kann man den eigentlichen Commit verhindern.

Das Perl-Skript sieht wie folgt aus:

Und wenn der Code, der eingecheckt werden soll, nicht sauber ist, bekommt man einen Fehler:



Siehe auch: