Perl-Code mit globstar

Globstar für Perl

Einen Doppelstern in Dateinamens-Muster wie in assets/**/*.css zu benutzen, ist für Web-Entwicklerinnen im Node.js-Ökosystem gang und gäbe. Er wird auch von gitignore bei der Auswertung von Ausnahme-Mustern verwendet. Die neue Perl-Bibliothek File-Globstar stellt die gleiche Funktionalität für Perl zur Verfügung.

Das Modul stellt fünf verwandte Funktionalitäten bereit, die Funktionen globstar(), translatestar(), fnmatchstar() und quotestar(), sowie die Klasse File::Globstar::ListMatch mit der Include- oder Exclude-Musterlisten à la gitignore realisiert werden können.

Funktionen und Klassen

globstar(MUSTER)

Diese Funktion verhält sich ungefähr wie die reguläre Perl-Funktion glob(), jedoch mit Unterstützung für das Doppelstern-Feature. Damit ist jetzt Code wie der folgende möglich:

my @files = globstar 'lib/**/*.p[lm]';

Die Variable @files enthält nunmehr die Namen aller Perl-Quelldateien im Verzeichnis lib und allen seinen Unterverzeichnissen.

Intern expandiert die Funktion lediglich alle Vorkommen von ** und überlässt den Rest der Arbeit der regulären Funktion glob().

Weitere Informationen finden sich in der Manpage für File::Globstar.

fnmatchstar(MUSTER, ZEICHENKETTE[, OPTIONEN])

Während globstar() das Dateisystem konsultiert, um ein Muster in eine Liste von Dateien zu expandieren, vergleicht die Funktion fnmatchstar() in abstrakter Weise eine Zeichenkette gegen ein Muster. Sie liefert true oder false, je nachdem, ob der String passt oder nicht. Einerseits handelt es sich um eine Erweiterung der Standard-C-Funktion fnmatch(3), andererseits um eine Untermenge, weil sie weniger Optionen unterstützt.

Die Manpage File::Globstar gibt weitere Informationen.

translatestar(MUSTER, OPTIONEN)

Die Funktion translatestar() wird intern von fnmatchstar() benutzt. Sie übersetzt ein Globstar-Muster in einen regulären Ausdruck im Perl-Format.

Alle schmutzigen Details finden sich wieder in der Manual-Page File::Globstar.

File::Globstar::ListMatch

File::Globstar::ListMatch implementiert den Algorithmus, den gitignore verwendet, in Perl. Eingabe ist eine Liste von --- eventuell negierten --- Globstar-Mustern, die in eine Liste von regulären Ausdrücken übersetzt wird. Die Methode match() erwartet als Argument einen String und vergleicht ihn mit jedem einzelnen Muster und liefert das Gesamtergebnis des Vergleichs als true oder false zurück.

File::Globstar::ListMatch hat zwei mögliche Arbeits-Modi, den Inklusions- und Exklusions-Modus. Der Exklusions-Modus ist kompatibel zu gitignore und unterscheidet sich vom Inklusions-Modus, durch ein einziges Feature: Im Exklusions-Modus können keine Dateien oder Verzeichnisse wieder *ein*geschlossen werden, die ein Elternverzeichnis haben, dass durch eine vorherige Regel *aus*geschlossen wurde. Dieses Feature kann eine signifikante Performanz-Verbesserung bewirken, ist allerdings auf den ersten Blick etwas verwirrend

Sekunde! Gerade noch hieß es, dass lediglich Zeichenketten verglichen werden, und das Dateisystem außen vor bleibt, oder? Richtig! Aber die normale Aufgabe der Klasse besteht im Vergleichen von Dateinamen. Das Dateisystem ist aber dennoch lediglich die Metapher für die Funktionalität.

Was bringt aber die merkwürdige Regel mit den Elternverzeichnissen? Tatsächlich macht sie den Vergleich sogar langsamer. Wie kann daraus eine Performanzsteigerung erwachsen?

Die Motivation für File::Globstar::ListMatch war der Static-Site-Generator Qgoda. Qgoda funktioniert ähnlich wie Jekyll. Das Program untersucht sämtliche Dateien und Unterverzeichnisse im Quellverzeichnis und kopiert diese in das Ausgabeverzeichnis _site, wobei die Datei durch einen Markdown-Prozessor in HTML konvertiert, und eventuell auch umbenannt wird.

Standardmäßig ignoriert die Software sämtliche Dateien und Verzeichnisse auf der Wurzelebene, wenn der Name mit einem Unterstrich _ beginnt. Des weiteren werden auch sämtliche versteckten Datein ignoriert. Als Konvention für versteckte Dateien wird angenommen, dass der Name mit einem Punkt (.) beginnt. Die standardmäßigen Ausnamehmuster sehen also folgendermaßen aus:

/_*
.*

Der führende Slash (Querstrich) verankert das Muster im Wurzelverzeichnis.

Vorherige Muster können mit einem führenden Ausrufezeichen wieder aufgehoben werden:

/_*
.*
!.htaccess

Es werden weiterhin sämtliche versteckten Dateien ausgeschlossen, es sei denn, sie heißen .htaccess.

Was (nur im Exklusions-Modus) allerdings nicht funktioniert, ist diese Regel:

/_*
.*
!_assets/fonts

Diese Negation zu unterstützen, hätte einen sehr negativen Einfluss auf die Performanz. Das lässt sich durch einen genaueren Blick auf die Logik, mit der Qgoda (oder vermutlich auch git) Dateien sammelt, verstehen. Das Quellverzeichnis wird dazu mit File::Find rekursiv durchsucht, und für jede gefunde Datei und jedes gefunden Verzeichnis wird ein Vergleich mit der Ausschlussliste durchgeführt. Passt der Name der Datei, wird sie ignoriert. Handelt es sich um ein Verzeichnis, wird die Suche für diesen Teilbaum abgebrochen. Die Dateien unterhalb eines ausgeschlossenen Verzeichnisses sind daher gar nicht bekannt.

Im letzten Beispiel wurde _assets auf Grund der ersten Regel /_* ignoriert, wobei dies später teilweise durch die Zeile !_assets/fonts wieder aufgehoben werden sollte. Das Verzeichnis _assets wurde aber vollständig verworfen, und so wird das Unterverzeichnis _assets/fonts von der Suche nie besucht. Die entsprechende Regel wird daher sinnlos.

Dies ließe sich natürlich leicht verhinden, indem immer der vollständige Verzeichnisbaum eingelesen würde, und die Entscheidung individuell für alle Dateien und Unterverzeichnisse getroffen würde. Allerdings enthält zum Beispiel das Verzeichnis /node_modules, das in vielen Web-Projekten anzutreffen ist, typischerweise Zehn- oder gar Hunderttausende Dateien. Solche Verzeichnisse gar nicht erst zu öffnen, macht daher einen großen Unterschied.

Aber stellt die nutzlose Regel nicht lediglich ein kosmetisches Problem dar? Nicht ganz. Qgoda arbeitet normalerweise im Überwachungs-Modus (watch mode). Die Software überwacht dabei das Wurzelverzeichnisse auf Modifikationen und generiert die Web-Site bei Bedarf neu.

Wird vom Betriebssystem die Änderung einer Datei gemeldet, muss die Software daher eine Was-Wäre-Wenn-Prüfung vornehmen, und und feststellen, ob die Datei beim initialen Einlesen des Wurzelverzeichnis eingeschlossen oder ignoriert worden wäre. Zum Beispiel sollte die Datei _assets/fonts/funnyface.ttf gemäß den obigen Regeln nicht verworfen werden, weil die !_assets/fonts die vorherige Regel /_* aufhebt. Andererseits wurde diese Datei bei der initialen Suche mit File::Find ignoriert, weil die Rekursion bereits bei _assets abgebrochen wurde.

Das stellt in der Tat mehr als ein kosmetisches Problem dar. Wurde die Datei initial, bei der Suche, die das Dateisystem untersucht, ignoriert, muss sie auch bei der folgenden abstrakten Prüfung, die lediglich den Dateinamen betrachtet, ignoriert werden.

Damit sollte auch klar sein, weshalb File::Globstar::ListMatch oder fnmatchstar() nie prüfen, ob eine Datei tatsächlich existiert. Dies wäre sinnlos, denn die vom Betriebssystem berichtete Änderung kann auch darin bestanden haben, dass die fragliche Datei gelöscht wurde. Das Löschen einer Datei kann aber eine Neu-Generierung der Web-Site durch Qgoda erfordern (oder ein Git-Checkout dirty machen). Wenn allerdings klar ist, dass alle Dateisystems-Operationen bei gelöschten Dateien ohnehin fehlschlagen, kann man sie sich auch direkt schenken.

Es gibt übrigens eine Möglichkeit, das Verzeichnis _assets/fonts selektiv in Qgoda einzuschließen. Die Lösung sieht allerdings etwas esoterisch aus:

/_*
.*
!/_assets
/_assets/*
!/_assets/fonts

Die Regel !/_assets re-inkludiert das komplette Unterverzeichnis _assets. Diese Regel wird durch durch das nächste Muster /_assets/* direkt wieder aufgehoben, wodurch wieder sämtliche Unterverzeichnisse ausgeschlossen werden. Diese Regel wird aber am Ende durch !/_assets/fonts doch wieder selektiv aufgehoben.

Das gleiche Ergebnis ließe sich auch mit weniger führenden Slashes erreichen:

/_*
.*
!/_assets
_assets/*
!_assets/fonts

In den letzten beiden Zeilen wurde der führende Slash entfernt, ohne dass dies irgendeine Änderung bewirkt, denn ...

Slashes

Eine der netten Eigenschaften von .gitignore-Dateien ist die Tatsache, dass die meisten Menschen deren Semantik auf den ersten Blick verstehen, ohne jemals eine Dokumentation dazu gelesen zu haben. Die Bedeutung der Slashes wird jedoch häufig nicht vollständig verstanden, wobei das allerdings selten zu Problemen führt.

Wenn File::Globstar::ListMatch (oder git) ein Muster untersucht, wird zuerst geprüft, ob es mit einem Slash endet. Falls ja, kann dieses Muster nur auf Verzeichnisnamen matchen, nicht auf Namen von anderen Dateien. Der Slash wird dann entfernt und im weiteren Verlauf ignoriert.

Im nächsten Schritt wird betracht, ob das Muster noch mindestens einen weiteren Slash an beliebiger Stelle enthält. Falls ja, wird bei diesem Muster immer der vollständige Pfad verglichen. Dazu gleich mehr.

Als letztes wird ein eventuell vorhandener führender Slash entfernt und ignoriert, nimmt also nicht am Vergleich teil. Einziger Sinn des führenden Slashes ist somit, einen Vergleich mit dem vollständigen Pfadnamen zu erzwingen, bzw. das Muster gegen die Wurzel des Verzeichnisbaums zu verankern.

Die meisten Menschen verstehen das intuitiv. Betrachten wir dennoch ein Beispiel:

js/vendor
*.bak

Wird die Datei js/vendor/awesomelib/index.js mit der Liste verglichen, liefert das Muster js/vendor einen Treffer, und die Datei wird ignoriert.

Die Datei src/js/app/components/menu.js.bak matcht auf das zweite Muster *.bak, was auf den ersten Blick klar und logisch scheint. Weshalb genau? Das erste Muster enthält einen Slash, das zweite dagegen nicht. Für den Vergleich wird daher nicht der volle Pfadname src/js/app/components/menu.js.bak herangezogen, sondern lediglich der relative Name der Datei menu.js.bak, und der passt auf *.bak. Der führende Verzeichnisanteil wird ignoriert.

Eine weitere Konsequenz dieses Features ist, dass jeder Slash, der nicht am Ende des Musters auftaucht, sowohl einen Vergleich mit dem vollständigen Pfadnamen erzwingt, als auch das Muster gegen das Wurzelverzeichnis verankert. js/vendor/awesomelib/index.js passt auf js/vendor aber src/js/vendor/awesome/index.js passt nicht, denn der innere Slash bwirkt, dass der Pfad von Anfang bis Ende passen muss.

Als Daumenregel lässt sich sagen, dass ein Slash in Mustern entweder am Anfang, am Ende oder im Inneren benötigt wird. Kombinationen daraus sind dagegen fast immer unsinnig, wobei allerdings dennoch in aller Regel das gewünschte Ergebnis erreicht wird.

Eine der seltenen Ausnahmen dieser Regel ist zum Beispiel das Muster /_*/. es passt nur auf Verzeichnisse direkt unterhalb des Wurzelverzeichisses, deren Namen mit einem Unterstrich beginnen. Im Gegensatz dazu sind /_assets und /_assets/ praktisch äquivalent. Der subtile Unterschied besteht darin, dass das zweite Muster nur auf /_assets passt, wenn es sich dabei um kein Verzeichnis handelt. In der Praxis weiß man aber selbstverständlich, ob /_assets ein Verzeichnis ist oder nicht. Schließlich sind es die eigenen Datenm, und die kennt man, oder?

Zusammenfassung

Globstar-Muster mit ** sind aus vielen Applikationen nicht mehr wegzudenken. File-Globstar macht sie endlich auch für Perl-Software verfügbar. Wer der Meinung ist, dass es sich dabei um eine sinnvolle Ergänzung handelt, darf gerne einen Stern für das Github-Repository File-Globstar loswerden oder die CPAN-Distribution bewerten.


blog comments powered by Disqus