Codeschnipsel

Perl: Auftrennen einer Zeichenkette

Um in Perl eine Zeichenkette in einzelne Teile zu zerlegen, steht die Funktion split() zur Verfügung. Diese verwendet jedoch einen regulären Ausdruck, um die Zeichenkette aufzutrennen. Davon mal abgesehen, dass reguläre Ausdrücke ein Script verlangsamen können, sind sie leicht fehleranfällig und vor allem für Anfänger ein wenig schwer durchschaubar – oder man weiß gar nicht, dass man einen regulären Ausdruck verwenden muss und wundert sich anschließend. Ich habe mich zum Beispiel bei den Arbeiten an der Perl-Version des Redirector halb tot danach gesucht, wie man split() dazu bringen kann, beim | aufzutrennen (Antwort: Man muss ein Backslash davorsetzen).

Außerdem hat split() ein ziemlich dummes Problem: Wenn sich die Trennzeichenkette am Ende der aufzutrennenden Zeichenkette befindet, so entstehen nicht die leeren Array-Elemente, von denen man meinen könnte, dass sie entstehen.
Einfacher erklärt heißt das, dass das Ergebnis von

join("s",split(/s/,"tststsss"))

nicht tststsss, sondern tstst ist! Wenn man sich das Ergebnis von split(/s/,"tststsss") mit Hilfe von Data::Dumper ansieht, ergibt sich folgendes:

$VAR1 = 't';
$VAR2 = 't';
$VAR3 = 't';

Da fehlen ein paar leere Zeichenketten am Schluss! preg_split() von PHP hat dieses Problem übrigens interessanterweise nicht.

Wie ich im Oktober 2011 erfahren habe, kann man das Problem mit den leeren Elementen umgehen, indem man split() einen negativen dritten Parameter übergibt. Aber als ich diesen Artikel irgendwann im Jahr 2005 oder 2006 geschrieben hatte, wusste ich davon nichts.

Wie auch immer, die Funktion mit dem Namen explode(), die ich Ihnen nun vorstellen werde, behebt diese Probleme. Sie funktioniert genauso wie die explode()-Funktion von PHP: Sie teilt eine Zeichenkette mit Hilfe einer ganz normalen Zeichenkette auf. Wahlweise kann man einstellen, dass nur maximal n Teile entstehen sollen. Die Funktion ist komplett ohne reguläre Ausdrücke realisiert worden, sie arbeitet lediglich mit index() und substr().

sub explode($$;$)
{
 my ($separator,$string,$limit) = @_;
 my @splitted;

 my $x       = 1;
 my $offset  = 0;
 my $sep_len = length($separator);

 while((my $pos = index($string,$separator,$offset)) >= 0 && (!$limit || $x < $limit))
 {
  my $part = substr($string,$offset,$pos-$offset);
  push(@splitted,$part);

  $offset = $pos+$sep_len;

  $x++;
 }

 push(@splitted,substr($string,$offset,length($string)-$offset));

 return @splitted;
}

Zunächst werden einige Variablen angelegt:

Jetzt wird eine while-Schleife begonnen. Zunächst wird von Beginn der Zeichenkette das erste Vorkommen der Trennzeichenkette bestimmt (wenn es nicht gefunden werden konnte, wird die Schleife sofort abgebrochen) und in der Variable $pos gespeichert. Im nächsten Schritt wird der Text von Beginn der Zeichenkette bis zum ersten Vorkommen der Trennzeichenkette mit Hilfe von substr() „ausgeschnitten“ und in $part gespeichert. Diese Daten werden nun an das Array @splitted angehängt. Nun wird in $offset die neue Suchposition gespeichert, und zwar die Stelle, an der die Trennzeichenkette endet, also zur Position der Trennzeichenkette wird die Länge ebendieser addiert.
Jetzt beginnt das ganze von vorne, allerdings mit dem Unterschied, dass nicht mehr direkt am Beginn der Zeichenkette gesucht wird, sondern hinter dem Vorkommen der ersten Trennzeichenkette. Das ganze läuft jetzt so lange weiter, bis keine Vorkommnisse der Trennzeichenkette mehr gefunden werden oder das Limit überschritten wurde.

Wenn die Schleife beendet ist, wird der Rest der Zeichenkette (also alles vom letzten gespeicherten Offset bis zum Ende) noch an @splitted angehängt und das Ergebnis zurückgegeben.

Perl: Einfacher Parser für Konfigurationsdateien

Wenn man ein komplexes Script programmiert, kann es durchaus nützlich sein, Konfigurationsvariablen in eine externe Datei auszulagern, damit es etwas übersichtlicher wird. Da Konfigurationseinstellungen allerdings auch häfig durch Nicht-Programmierer verändert werden müssen, macht es Sinn, diese in eine Datei auszulagern, die einfacher aufgebaut ist als ein Script und nicht irgendwelche für Programmiersprachen typische Eigenschaften aufweist (Dollar-Zeichen für Variablen, Semikolon am Ende einer Anweisung).

Der folgende (sehr) einfache Parser ist im Rahmen des Dev-Editor-Projektes entstanden (der Dev-Editor verwendet mittlerweile einen erweiterten Parser, der auch mit den aus Windows-INI-Dateien bekannten Sektionen umgehen kann). Er erlaubt es, Konfigurationsdateien einzulesen, die aus Schlüssel-Werte-Paaren bestehen, die wiederum durch ein Gleichheitszeichen voneinander getrennt sind. Kommentare sind ebenfalls möglich: Zeilen, die mit einem #-Zeichen beginnen (vor diesem Zeichen dürfen beliebig viele Leerzeichen stehen), werden ignoriert.

sub parse_config($)
{
 my $file = shift;
 local *CF;

 open(CF,'<'.$file) or die "Open $file: $!";
 read(CF, my $data, -s $file);
 close(CF);

 my @lines  = split(/\015\012|\012|\015/,$data);
 my $config = {};
 my $count  = 0;

 foreach my $line(@lines)
 {
  $count++;

  next if($line =~ /^\s*#/);
  next if($line !~ /^\s*\S+\s*=.*$/);

  my ($key,$value) = split(/=/,$line,2);

  # Remove whitespaces at the beginning and at the end

  $key   =~ s/^\s+//g;
  $key   =~ s/\s+$//g;
  $value =~ s/^\s+//g;
  $value =~ s/\s+$//g;

  die "Configuration option '$key' defined twice in line $count of configuration file '$file'" if($config->{$key});

  $config->{$key} = $value;
 }

 return $config;
}

Zunächst wird die als Funktionsparameter angegebene Datei zum Lesen geöffnet, der komplette Inhalt in die Variable $data eingelesen und die Datei anschließend wieder geschlossen. Der in $data gespeicherte Dateiinhalt wird nun zeilenweise aufgeteilt, die Zeilen werden in @lines gespeichert. Dann wird eine leere Hash-Referenz mit dem Namen $config angelegt, die später die einzelnen Konfigurationsoptionen beinhalten wird. Außerdem wird eine Variable namens $count angelegt und erhält den Wert 0. Diese Variable ist für das eigentliche Parsen der Konfigurationsdatei eher unwichtig, sie enthält lediglich die aktuelle Zeilennummer, die dann in Fehlermeldungen mit ausgegeben wird.

Jetzt beginnt das eigentliche Parsen der Datei. Die einzelnen Zeilen werden in einer foreach-Schleife folgendermaßen abgearbeitet:
Zunächst wird einmal die Variable $count um 1 erhöht. Dann wird die aktuelle Zeile daraufhin überprüft, ob sie nicht eine Kommentarzeile ist und ob sie den Anforderungen entspricht (s.o.). Sollte sie eine Kommentarzeile sein oder nicht den Anforderungen entsprechen, geht es mit Hilfe von next in den nächsten Schleifendurchlauf, sprich in die nächste Zeile.
Die Zeile wird nun mit split() am Gleichheitszeichen aufgetrennt, es werden aber nur zwei Elemente erzeugt, so dass Gleichheitszeichen in den Werten vorkommen können. Anschließend werden an Anfang und Ende von Schlüssel und Wert evtl. vorhandene Leerzeichen entfernt.
Nun wird überprüft, ob in der Hash-Referenz $config schon einmal ein Wert mit dem aktuellen Schlüsselnamen abgespeichert wurde. Ist dies der Fall, wird das Script mit einer Fehlermeldung, die den Schlüsselnamen und die aktuelle Zeilennummer enthält, abgebrochen. Ist dies nicht der Fall, werden Schlüssel und Wert in der Hash-Referenz gespeichert und es wird die nächste Zeile geparst.

Wenn alle Zeilen abgearbeitet sind, wird die Hash-Referenz zurückgegeben.

Wenn Sie diese Funktion in ein Modul auslagern möchten, empfehle ich, die croak()-Funktion aus dem Carp-Modul einzubinden (use Carp qw(croak);) und alle Aufrufe von die() durch croak() zu ersetzen.

Perl: Lesbare Dateirechte

Wenn man mit einer Unix-Shell arbeitet und in einem beliebigen Verzeichnis ls -l eingibt, bekommt man aufgelistet, welche Dateien sich in diesem Verzeichnis befinden. Zusätzlich bekommt man noch wertvolle Informationen – zum Beispiel die Dateigröße, das änderungsdatum und die Rechte. Die Rechte werden dabei typischerweise in einer Form wie rw-r--r-- angegeben, damit sie für einen Menschen einigermaßen verständlich sind.

Wenn man über irgendeine Programmiersprache an die Rechte einer Datei kommen will, bekommt man diese aber als Zahl, in der jeweils Bits für bestimmte Rechte gesetzt sind. Diese Zahlen sind für einen Menschen natürlich nicht verständlich. Natürlich kann man beispielsweise mit sprintf() diese Zahl in eine Oktalzahl umwandeln – die oktale Schreibweise dieser Zahlen dürfte nämlich für die meisten Menschen schon verständlicher sein, wenn sie sich mit dem chmod()-Befehl auskennen. Aber es ist auch möglich, diese Zahl in eine Zeichenkette umzuwandeln, wie sie von ls -l bekannt ist. Die folgende Funktion, die im Rahmen des Dev-Editor-Projektes entstanden ist, macht es möglich. Sie erkennt neben den normalen Rechten auch, ob das SetUID-, das SetGID- und das Sticky-Bit gesetzt sind. Der Dateityp, wie es von ls -l bekannt ist, wird allerdings nicht ermittelt.

sub mode_string($)
{
 my $mode   = shift;
 my $string = '';

 # User

 $string  = ($mode & 00400) ? 'r' : '-';
 $string .= ($mode & 00200) ? 'w' : '-';
 $string .= ($mode & 00100) ? (($mode & 04000) ? 's' : 'x') :
                               ($mode & 04000) ? 'S' : '-';

 # Group

 $string .= ($mode & 00040) ? 'r' : '-';
 $string .= ($mode & 00020) ? 'w' : '-';
 $string .= ($mode & 00010) ? (($mode & 02000) ? 's' : 'x') :
                               ($mode & 02000) ? 'S' : '-';

 # Other

 $string .= ($mode & 00004) ? 'r' : '-';
 $string .= ($mode & 00002) ? 'w' : '-';
 $string .= ($mode & 00001) ? (($mode & 01000) ? 't' : 'x') :
                               ($mode & 01000) ? 'T' : '-';

 return $string;
}

Zunächst wird die leere Zeichenkette $string angelegt. Sie wird später die vollständige Rechteangabe enthalten. Anschließend wird die Zahl mit den Rechteangaben darauf geprüft, ob die einzelnen Bits gesetzt sind. Damit es etwas überschaubarer ist, wird die Oktalschreibweise für diese Bits verwendet. Was die einzelnen Bits jeweils bedeuten, können Sie der Man-Page von chmod() entnehmen. Das Ergebnis dieser Prüfung ist entweder ein Buchstabe, der anzeigt, ob das jeweilige Bit gesetzt ist oder ein Bindestrich, wenn es nicht gesetzt wurde. Dieser Buchstabe wird an das Ende der Zeichenkette $string angehängt. Wenn alle relevanten Bits überprüft wurden, wird die fertige Zeichenkette zurückgegeben.

Nutzen können Sie die Funktion beispielsweise folgendermaßen:

my @stat = stat("MeineDatei");
print mode_string($stat[2]);

So würden Sie die Rechte von MeineDatei als lesbare Zeichenkette ausgeben.

PHP: Binärdateien erkennen

Wenn man beispielsweise versucht, einfache Datei-Manager zu programmieren, kann es nützlich sein, wenn man Dateien vor dem öffnen daraufhin untersucht, ob sie Binärdaten enthalten, damit keine wirren Zeichen am Bildschirm angezeigt werden oder die Datei beschädigt wird, weil sie jemand „bearbeiten“ wollte. Natürlich kann man auch es sich auch einfach machen und in dem Programm eine Liste mit Datei-Erweiterungen definieren, die typisch sind für Text- oder Binärdateien. Aber das ist zu unflexibel und auf Dauer ist es nervig, diese Liste zu verwalten.

Diese Funktionen basieren hauptsächlich auf dem Tipp „test if a file or string is text or binary“ aus dem ActiveState Python Cookbook. Die Python-Funktion aus dem verlinkten Tipp wurde wiederrum anhand der Definition der Dateitest-Operatoren -B und -T von Perl erstellt. Diese Definition besagt, dass eine Datei eine Binärdatei ist, wenn sich im ersten Block der Datei zu mehr als 30% Steuerzeichen und Zeichen mit einem hohen Bit-Wert befinden oder wenn das Zeichen mit dem ASCII-Wert 0 in diesem Block vorkommt. Leere Dateien werden als Textdateien behandelt.

function is_textfile($file,$blocksize=512)
{
 $fp = fopen($file,'rb');
 flock($fp,LOCK_SH);
 $string = fread($fp,$blocksize);
 fclose($fp);

 return is_text($string);
}

function is_text($s)
{
 $text_characters = array_merge(array_map('chr',range(32,127)),array("\012","\015","\t","\b"));

 if(strpos($s,"\0") === true) return;
 if(!$s)                      return 1;

 $t = $s;

 foreach($text_characters as $text_character)
 {
  $t = str_replace($text_character,'',$t);
 }

 if(strlen($t) / strlen($s) > 0.3) return;
 return 1;
}

Das ganze besteht aus zwei Funktionen: is_text() wendet die oben beschriebene Prüfung auf irgendeine Zeichenkette an, is_textfile() liest die ersten 512 Bytes (oder je nachdem, was angegeben wurde) und gibt diese dann an is_text() weiter.

Zunächst wird das Array $text_characters erstellt. Es enthält die erlaubten Zeichen. Hierbei handelt es sich um die Zeichen mit den ASCII-Werten 32 bis 127 (Buchstaben und Ziffern), das Zeilenumbruchzeichen, der Wagenrücklauf, das Tab-Zeichen und das Backspace-Zeichen.
Anschließend wird mit strpos() überprüft, ob das Zeichen mit dem ASCII-Wert 0 in der Zeichenkette vorkommt. Ist dies der Fall, so wird die Funktion abgebrochen und die Daten als Binärdaten gekennzeichnet. Dann wird geprüft, ob die Zeichenkette überhaupt Daten enthät. Wenn sie keine Daten enthält, so werden die Daten als Textdaten gekennzeichnet und die Funktion beendet.
Nun wird die Zeichenkette in die Variable $t kopiert. Nun wird mit foreach() das Array mit den erlaubten Zeichen durchgegangen und in $t jedes dieser Zeichen entfernt (es wird mit str_replace() durch eine leere Zeichenkette ersetzt). Danach wird die Länge der neuen Zeichenkette, in der alle erlaubten Zeichen entfernt wurden durch die Länge der ursprünglichen Zeichenkette geteilt. Wenn das Ergebnis größer als 0.3 ist, so werden die Daten als Binärdaten erkannt, ansonsten handelt es sich um Textdaten.