Perl bietet eine Fülle wenig bekannter Funktionen an, um elegant und portabel mit Dateien in Dateisystemen herumzuspielen. Hier kommt die längst fällige Einführung.
Fangen wir an mit einem
scheinbar trivialen Thema: Wenn das Verzeichnis, in dem eine Datei
liegt, mit $dir
gegeben ist und der Dateiname in $file
liegt --
wie lautet dann der vollständige Pfad zur Datei? "$dir/$file"
ist
nicht immer richtig! Was ist, wenn $dir
das Root-Verzeichnis /
ist
und in $file
der String "datei"
steht? "//datei"
wäre die
Folge. Oder falls $dir
schon einen abschließenden Slash enthält, wie
etwa in "/tmp/"
? Oder falls wir (schauder!) unter Windows arbeiteten,
wo Backslashes als Pfadtrenner fungieren: \tmp\datei
hieße es da zum
Beispiel. Diese ganzen Sonderfälle deckt das Modul File::Spec
ab,
welches eine Methode File::Spec->catfile()
anbietet, welche Pfad-
und Dateinamen plattformübergreifend und intelligent zusammenkleistert:
use File::Spec;
my $dir = "/tmp"; my $file = "test.dat";
my $path = File::Spec->catfile($dir, $file); # => '/tmp/test.dat' (Linux) # => '\tmp\test.dat' (Windows)
Wie alle heute vorgestellten Module liegt auch File::Spec
der
Perl-Distribution ab Release 5.6.0 bereits bei, keine zusätzlichen
Installierungsklimmzüge sind erforderlich. Es ist so bequem, sich nicht
mehr um die Sonderfälle zu kümmern und auch automatisch die für
die aktuelle Plattform (Unix, Windows, VMS, OS2 und Mac) gültigen
Pfadangaben zu bekommen, dass File::Spec
tatsächlich
abhängig macht
-- wer es einmal in seine Werkzeugkiste packte, gibt es nicht mehr her.
Wer nicht dauernd File::Spec->catfile()
sagen will, sondern
lieber catfile()
importiert, holt statt File::Spec
einfach
File::Spec::Functions
herein. Ausgewählte
File::Spec
-Funktionen wie catfile()
importiert es ungefragt:
use File::Spec::Functions;
print catfile("./tmp/", "./test.log"); # => tmp/./test.log print "\n";
Hier werden die Grenzen von catfile()
klar: Man sieht, dass
catfile()
zwar das erste ./
eliminiert, das zweite
(vor test.log
)
aber beibehält: tmp/./test.log
ist das Ergebnis. Um aus einer
Pfadangabe doppelte Slashes und überflüssige ./
-Angaben zu entfernen,
daraus also einen kanonischen Pfad zu machen, dazu dient die Funktion
canonpath()
:
use File::Spec::Functions; print canonpath("./tmp//./test.dat"); # => "tmp/test.dat" print "\n";
Ein Beispiel: Um einen Blick auf die Implementierung von
File::Spec
zu werfen, müssen wir herausfinden, wohin es installiert
wurde. Irgendwo in die Perl-Bibliotheken, zweifellos. Da Perl das Modul
laden kann, muss unterhalb einer der Pfade im Perl-Include-Pfad @INC
die Datei File/Spec.pm
stehen. Listing wpm.pl
nimmt den Namen
eines Moduls auf der Kommandozeile entgegen, baut daraus den Namen der
gesuchten *.pm
-Datei und spult anschließend durch alle Pfade in
@INC
, um mit dem Dateitester -e
herauszufinden, wo das Modul sich
tatsächlich befindet. Im Erfolgsfall bricht es mit last
sofort ab,
denn sinnigerweise sollte es nur eine Installation geben:
$ wpm.pl File::Spec /usr/lib/perl5/5.6.0/File/Spec.pm
Zeile 7 spaltet den in $ARGV[0]
übergebenen Modulnamen an den
::
-Trennern in Einzelteile, die catfile()
anschließend
wieder mit Pfadtrennern zusammenfügt. Zeile 11 hängt noch den
gerade untersuchten Pfad aus @INC
davor, auch wieder mit
catfile()
.
01 #!/usr/bin/perl -w 02 03 use File::Spec::Functions; 04 # Ohne Modulnamen aufgerufen? 05 die "usage: $0 Module" unless defined $ARGV[0]; 06 07 $subpath = catfile(split "::", $ARGV[0]) . ".pm"; 08 09 foreach $path (@INC) { 10 # Pfad + Modulverzeichnis + Modul + pm 11 $fullpath = catfile($path, $subpath); 12 13 if(-e $fullpath) { 14 print "$fullpath\n"; # Gefunden! 15 last; 16 } 17 }
Ein notorisch schwieriges Problem ist es, absolute in relative
Pfadangaben zu transformieren. Steht man gerade in /home/mschilli
und sagt tmp/abc.tgz
, schließt die rel2abs()
-Funktion
messerscharf auf das richtige absolute Verzeichnis:
use File::Spec::Functions qw(rel2abs);
print rel2abs("tmp/abc.tgz"); => /home/mschilli/tmp/abc.tgz print "\n";
Hinter den Kulissen ermittelt rel2abs()
mit der cwd()
-Funktion (aus
dem Modul Cwd
) zunächst das aktuelle Verzeichnis, das sich im obigen
Fall als /home/mschilli
herausstellt und nimmt dann die
Pfadtransformation vor. Zu beachten ist, dass File::Spec::Functions
die Funktion rel2abs()
nicht standardmäßig exportiert, sondern dass
das aufrufende Programm mit einer an die use
-Anweisung angehängten
Liste explizit danach fragen muss.
Zwei weitere nützliche Funktionen aus File::Spec::Functions
seien
ebenfalls erwähnt: curdir()
zeigt an, wie der Spezialeintrag für das
aktuelle Verzeichnis heißt ("."
unter Unix/Windows) und updir()
gibt an,
wie der Link zum darüberliegenden Verzeichnis lautet (".."
unter Unix, Windows). Die Lesefunktion für Verzeichnisse, readdir()
, liefert
diese Sondereinträge
ja immer mit. Um letztere auszublenden, hilft der Code in Listing
readdir.pl
, der in die nächste Runde springt, falls einer der
Spezialeinträge auftaucht.
01 #!/usr/bin/perl -w 02 03 use File::Spec::Functions; 04 05 $dir = "/tmp"; 06 07 opendir(DIR, $dir) or die("Cannot open $dir"); 08 09 foreach $dir (readdir DIR) { 10 next if $dir eq updir(); 11 next if $dir eq curdir(); 12 13 print "Verzeichnis gefunden: $dir\n"; 14 } 15 16 closedir(DIR);
Eine schnellere Möglichkeit unter Unix, wo klar ist, dass die Kandidaten
"."
und ".."
heißen, ist freilich, sie direkt mit einem regulären
Ausdruck wie
next if $dir =~ /^\.{1,2}\z/;
anzusprechen. Der reguläre Ausdruck sucht nach dem Zeilenanfang
(^
), einem oder zwei Punkten (als \.
ausmaskiert,
weil der Punkt als Metazeichen im Regex sonst eine Sonderbedeutung hat)
und dem Stringende \z
.
Warum \z
und nicht das sonst übliche $
als Zeilenendemarkierung? $
passt auf das Ende der ersten Zeile des Strings, \z
ist das
definitive Stringende. Filenamen unter Unix dürfen auch mehrere Zeilen
lang sein und eine Datei mit einem Namen wie etwa "..\ndatei"
soll
bei der Auflistung nicht unter den Tisch fallen.
Um eine Datei vom zu ihr führenden Pfad zu trennen, exportiert das Modul
File::Basename
die Funktion basename()
, die zu einer Pfadangabe
wie zum Beispiel /tmp/01/test.dat
den Dateinamen test.dat
extrahiert,
während die Funktion dirname()
(ebenfalls aus File::Basename
) sich
auf den Pfad konzentriert:
use File::Basename;
print basename("/tmp/test.dat"); # => test.dat print "\n";
print dirname("/tmp/test.dat"); # => /tmp print "\n";
Mit mkdir()
bietet Perl ja die Möglichkeit, neue Verzeichnisse
anzulegen, allerdings nur direkt und nicht in beliebiger Tiefe:
mkdir("/tmp/01/test")
kann nur funktionieren, falls /tmp/01
schon
existiert. Das Modul File::Path
exportiert mkpath()
und rmpath()
,
die beliebig tiefe Verzeichnisse erzeugen und auch wieder abräumen:
use File::Path;
mkpath("/tmp/01/02/03/04/05") or die "mkpath failed"; rmtree("/tmp/01") or die "rmtree failed";
mkpath()
ist ein mkdir()
mit Tiefenwirkung, rmtree()
entspricht
dem rm -rf
der Shell und löscht im Beispiel alle von mkpath()
vorher erzeugten Verzeichisse.
Eine Datei in eine andere zu kopieren geht nur durch Einlesen der
ersten und anschließendes Ausschreiben in die Zweite.
Das Modul File::Copy
vereinfacht dies drastisch:
use File::Copy; # Backup von test.dat copy("test.dat","test.dat.old") or die "copy failed";
Die exportierte Funktion
copy()
stellt auch sicher, dass die Dateiattribute wie Zugriffsrechte
so kopiert werden, wie die systemeigene Kopierfunktion (unter Unix
cp
) es tun würde. Reines Umbenennen einer Datei erledigt die
move()
-Funktion, die ebenfalls von File::Copy
exportiert wird, obwohl
das nicht auf den ersten Blick einleuchtet. move()
funktioniert
im Gegensatz zum sonst in Perl verwendeten rename()
selbst unter extremen Umständen, wie zum Beispiel
wenn Quell- und Zieldatei auf unterschiedlichen Dateisystemen liegen --
genau wie das Unix-Kommando mv
. In
diesem Fall artet das Umbenennen in Kopieren-und-Löschen aus:
use File::Copy;
move("/tmp/test.dat", "/mnt/disk2/test.dat") or die "move failed";
File::Find
Wie die Unix-Funktion find
durchstöbert die find()
-Funktion des
File::Find
-Moduls Verzeichnisse und deren Abkömmlinge rekursiv und
führt während des Abstiegs beliebig definierbare Kniffe aus. Die
find()
-Funktion nimmt dabei eine Referenz auf eine benutzerdefinierte
Funktion entgegen, gefolgt von einem oder mehreren Verzeichnissen, in
die sie anschließend depth-first vordringt. Für jeden gefundenen
Eintrag (egal ob Verzeichnis, symbolischer Link oder Datei) wird die
benutzerdefinierte Funktion angesprungen und dort $_
auf den Namen
des Eintrags gesetzt. Außerdem befindet sich die Funktion zum Zeitpunkt
ihres Aufrufs schon im entsprechenden Verzeichnis, so dass man zum
Beispiel ganz einfach mit Filetestern wie
-f $_
bzw. -d $_
prüfen kann, ob es sich
bei dem Eintrag um eine Datei oder ein Verzeichnis handelt. Um zum
Beispiel herauszufinden, welche Unterverzeichnisse und
Unter-Unterverzeichnisse sich unterhalb des /tmp
-Verzeichnisses
befinden, hilft Listing finddir.pl
.
01 #!/usr/bin/perl -w 02 03 use File::Spec::Functions; 04 use File::Find; 05 06 find( \&wanted, '/tmp'); 07 08 sub wanted { 09 return unless -d $_; 10 return if $_ eq curdir(); 11 12 print "$File::Find::name\n"; 13 }
finddir.pl
lässt perl
sofort aus der Ansprungfunktion wanted
aussteigen,
falls der gesuchte Eintrag kein Verzeichnis (Operator -d
) ist.
File::Find
unterdrückt zwar die Einträge "."
und ".."
in den
gefundenen Unterverzeichnissen, liefert aber für das
Startverzeichnis einmal "."
, wohl um mit dem Unix-Kommando
find
gleichzuziehen, das ebenfalls das Startverzeichnis meldet.
Die Zeile return if $_ eq curdir();
unterdrückt in finddir.pl
diese Ausgabe.
Falls sich ein Verzeichnis findet,
gibt der print
-Befehl den vollständigen Pfad des Eintrags
aus, den File::Find
praktischerweise im Skalar $File::Find::name
bereitstellt
(also zum Beispiel /tmp/test.dat
). Die andere unter Umständen
nützliche Variable $File::Find::path
liefert den dirname()
des Eintrags.
File::Find
folgt normalerweise aus Sicherheitsgründen
keinen symbolischen Links. Das lässt sich aber umkonfigurieren. Die
exportierte Funktion find()
nimmt statt einer Referenz auf eine
Ansprungsfunktion als erstem Parameter auch eine Referenz auf einen Hash
entgegen, in dem verschiedene Konfigurationswerte stehen. Der Eintrag
wanted
des Hashs zeigt dann auf die Ansprungsfunktion, und falls unter
dem Schlüssel follow
ein wahrer Wert steht, folgt File::Find
auch
symbolischen Links. Es stellt auch bei zyklischen Verweisen sicher, dass
jeder Eintrag im Dateisystem nur einmal durchlaufen wird:
find({ wanted => \&wanted, follow => 1 }, $start_dir);
Hier heißt die Ansprungsfunktion wanted()
und das Startverzeichnis für
die Suche steht in $start_dir
.
Mit File::Find
lassen sich die wildesten Dinge zaubern -- wie etwa
die letzten 3 veränderten Dateien auf der Festplatte zu finden oder alle
Dateien zu löschen,
die größer sind als 10 Megabyte und einem bestimmten Benutzer
gehören. wanted()
darf auch jede
vorbeizischende Datei kurz öffnen und nach bestimmten Textmustern
durchkämmen -- und so die Festplatte nach Dateien mit bestimmtem Inhalt
durchforsten. Lasst eurer Fantasie freien Lauf!
Dieser Text entstammt einem Fortgeschrittenen-Abschnitt des neuen Buches Perl Lernen, das voraussichtlich im Frühjahr 2000 bei Addison-Wesley erscheinen wird.
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |