Finder: Dateisuche
Sie müssen Dateien finden, die einer bestimmten Maske entsprechen? Dabei hilft Ihnen der Finder. Er ist ein vielseitiges und schnelles Werkzeug zum Durchsuchen von Verzeichnisstrukturen.
Installation:
composer require nette/utils
Die Beispiele setzen voraus, dass dieser Klassen-Alias angelegt wurde:
use Nette\Utils\Finder;
Verwendung
Sehen wir uns zuerst an, wie Sie mit Nette\Utils\Finder die Namen der Dateien mit den Endungen
.txt und .md im aktuellen Verzeichnis auflisten:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
Standardmäßig wird im aktuellen Verzeichnis gesucht, das lässt sich aber mit den Methoden in() oder from() ändern. Die Variable $file ist eine Instanz der Klasse FileInfo, $name ist ein String mit dem Pfad zur Datei.
Der Pfad wird so zurückgegeben, wie Sie ihn geschrieben haben, samt der Trennzeichen der Plattform; unter Windows kann das
Ergebnis daher / und \ mischen. Rufen Sie FileSystem::unixSlashes() auf, wenn Sie eine
einheitliche Form brauchen.
Was soll gesucht werden?
Neben der Methode findFiles() gibt es findDirectories(), die nur Verzeichnisse sucht, und
find(), die beides sucht. Diese Methoden sind statisch, lassen sich also ohne das Erzeugen einer Instanz aufrufen.
Das Argument mit der Maske ist optional; wird es weggelassen, passt alles.
foreach (Finder::find() as $file) {
echo $file; // jetzt werden alle Dateien und Verzeichnisse aufgelistet
}
Mit den Methoden files() und directories() geben Sie an, wonach zusätzlich gesucht werden soll. Die
Methoden lassen sich wiederholt aufrufen, und als Argument lässt sich auch ein Array von Masken übergeben:
Finder::findDirectories('vendor') // alle Verzeichnisse
->files(['*.php', '*.phpt']); // plus alle PHP-Dateien
Eine Alternative zu den statischen Methoden ist das Erzeugen einer Instanz mit new Finder (ein so erzeugtes Objekt
sucht zunächst nach nichts) und die Angabe des Gesuchten über files() und directories():
(new Finder)
->directories() // alle Verzeichnisse
->files('*.php'); // plus alle PHP-Dateien
In der Maske können Sie Wildcards wie *, **, ? und
[...] verwenden. Sie können sogar Verzeichnisse angeben, so findet zum Beispiel src/*.php alle
PHP-Dateien im Verzeichnis src. Symlinks werden ebenfalls als Verzeichnisse oder Dateien behandelt.
Wo soll gesucht werden?
Standardmäßig wird im aktuellen Verzeichnis gesucht. Das ändern Sie mit den Methoden in() und
from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // sucht direkt in src/ und tests/
->from('vendor'); // sucht auch in den Unterverzeichnissen von vendor/
Die beiden Methoden unterscheiden sich in der Tiefe: in() sucht nur innerhalb des angegebenen Verzeichnisses,
während from() auch in dessen Unterverzeichnisse hinabsteigt (rekursiv). Um das aktuelle Verzeichnis rekursiv zu
durchsuchen, verwenden Sie from('.').
Über die Rekursion entscheidet allerdings nicht from() allein – auch die Wildcard ** in der Maske
treibt sie an, findFiles('**/*.php')->in('src') sucht also ebenfalls rekursiv. Anders gesagt ist
from('src') bloß eine Abkürzung für in('src') mit einer rekursiven Maske. Siehe Wildcards.
Diese Methoden lassen sich mehrfach aufrufen, oder Sie übergeben mehrere Pfade als Array; die Dateien werden dann in allen
angegebenen Verzeichnissen gesucht. Existiert eines dieser Verzeichnisse nicht, wird eine Nette\InvalidStateException
geworfen.
Relative Pfade beziehen sich auf das aktuelle Verzeichnis, absolute Pfade lassen sich aber ebenso verwenden:
Finder::findFiles('*.php')
->in('/var/www/html');
Im Pfad können Sie die Wildcards *, ** und ? verwenden, nicht aber
[...], das dort wörtlich genommen wird. Das verhindert unbeabsichtigtes Verhalten, wenn Sie etwa
in(__DIR__) durchsuchen und der Pfad zufällig die Zeichen [] enthält. So sucht zum Beispiel
src/*/*.php alle PHP-Dateien in den Verzeichnissen der zweiten Ebene unterhalb von src.
Beim rekursiven Durchsuchen von Dateien und Verzeichnissen (in die Tiefe) wird zuerst das übergeordnete Verzeichnis
zurückgegeben und danach die darin enthaltenen Dateien. Diese Reihenfolge lässt sich mit childFirst() umkehren.
Wildcards
Eine Maske kann mehrere Sonderzeichen enthalten:
*– beliebig viele Zeichen außer dem Trennzeichen/(bleibt also auf einer Verzeichnisebene)**– beliebig viele Zeichen einschließlich/(überspannt also Verzeichnisebenen, siehe unten)?– genau ein Zeichen außer/[a-z]– ein Zeichen aus dem Bereich oder der Menge in den Klammern[!a-z]– ein Zeichen, das nicht in den Klammern steht
Der entscheidende und leicht zu übersehende Punkt: ** passt auf null oder mehr Verzeichnisebenen. Es
bedeutet nicht “mindestens ein Unterverzeichnis”. Deshalb passt src/**/*.php genauso auf eine Datei direkt
in src wie auf eine, die mehrere Ebenen tiefer liegt. Betrachten Sie diesen Baum:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
Die folgende Tabelle zeigt, worauf die einzelnen Masken passen, für Dateien wie für Verzeichnisse:
| Maske | Passt auf |
|---|---|
src/*.php |
nur src/app.php (direkt in src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (alle
Ebenen, auch direkt in src) |
src/* |
die direkten Kinder von src: app.php, Model, Control |
src/** |
alles unterhalb von src, Dateien und Verzeichnisse (Kurzform für src/**/*) |
src/*/ |
die direkten Unterverzeichnisse von src: Model, Control |
src/**/ |
alle Unterverzeichnisse in beliebiger Tiefe: Model,
Model/Repository, Control |
Zwei Abkürzungen lohnt es sich zu merken:
- Ein
**, auf das nicht unmittelbar ein/folgt, verhält sich wie**/plus*.src/**ist also eine Kurzform fürsrc/**/*und**.phpeine für**/*.php. - Ein abschließender Schrägstrich beschränkt die Maske auf Verzeichnisse.
find('log/')gibt also Verzeichnisse namenslogzurück, aber nie eine Datei dieses Namens. (findFiles()lehnt einen abschließenden Schrägstrich ab, denn die Suche nach einer Datei “Verzeichnis” ergibt keinen Sinn.)
Weitere Beispiele für die Verwendung:
img/?.png– Dateien mit einem einbuchstabigen Namen wie0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– Logdateien im FormatYYYY-MM-DDdocs/**/*.md– alle Dateien mit der Endung.mdindocsund allen seinen Unterverzeichnissen
Ausschluss
Mit der Methode exclude() entfernen Sie Dateien und Verzeichnisse aus den Ergebnissen. Das Argument ist eine
Maske, auf die das Element nicht passen darf. Hier suchen wir *.txt-Dateien außer denen, die den Buchstaben
X im Namen enthalten:
Finder::findFiles('*.txt')
->exclude('*X*');
Die Maske für den Ausschluss verwendet genau dieselbe Grammatik wie die Suchmasken – dieselben Wildcards, die Verankerung mit ./ und die Abkürzung **. Ihr abschließender
Teil entscheidet über den Umfang des Ausschlusses:
| Maske | Schließt aus |
|---|---|
temp |
jede Datei und jedes Verzeichnis namens temp, in beliebiger Tiefe |
temp/ |
nur ein Verzeichnis temp (samt Inhalt); eine Datei namens temp bleibt erhalten |
temp/* |
den Inhalt von temp, behält aber das Verzeichnis temp selbst |
temp/** |
dasselbe wie temp/* |
Ein ausgeschlossenes Verzeichnis wird beim Durchlaufen gar nicht erst betreten, das Ausschließen ganzer Teilbäume beschleunigt die Suche also zusätzlich. So überspringen Sie bestimmte Unterverzeichnisse:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Filterung
Der Finder bietet mehrere Methoden, um die Ergebnisse zu filtern, also einzuschränken. Sie lassen sich kombinieren und wiederholt aufrufen.
Mit size() filtern wir nach der Dateigröße. So finden wir Dateien mit einer Größe im Bereich von 100 bis
200 Bytes:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
Die Methode date() filtert nach dem Datum der letzten Änderung der Datei. Die Werte können absolute
Datumsangaben sein oder relativ zum aktuellen Datum samt Uhrzeit. So finden wir zum Beispiel Dateien, die in den letzten zwei
Wochen geändert wurden:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Beide Methoden verstehen die Operatoren >, >=, <, <=,
=, !=, <>.
Der Finder erlaubt es außerdem, die Ergebnisse mit eigenen Callbacks zu filtern. Der Callback bekommt als Parameter ein Objekt
Nette\Utils\FileInfo und muss true zurückgeben, damit die Datei in den Ergebnissen landet.
Beispiel: Suche nach PHP-Dateien, die den String 'Nette' enthalten (ohne Rücksicht auf die
Groß-/Kleinschreibung):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Tiefenfilterung
Beim rekursiven Suchen können Sie mit der Methode limitDepth() die maximale Tiefe des Durchlaufs festlegen.
limitDepth(1) durchläuft nur die erste Ebene der Unterverzeichnisse, limitDepth(0) schaltet den Abstieg
in die Tiefe ganz ab, und der Wert –1 hebt die Begrenzung der Tiefe auf.
Der Finder erlaubt es, mit eigenen Callbacks zu entscheiden, welche Verzeichnisse beim Durchlauf betreten werden. Der Callback
bekommt ein Objekt Nette\Utils\FileInfo, das das Verzeichnis repräsentiert, und muss true zurückgeben,
damit es betreten wird:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Nicht lesbare Verzeichnisse
Standardmäßig überspringt der Finder Verzeichnisse, die er nicht lesen kann (etwa wegen fehlender Rechte). Wenn Sie
stattdessen möchten, dass er in solchen Fällen eine Exception wirft, rufen Sie
ignoreUnreadableDirs(false) auf.
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Sortierung
Der Finder bietet auch mehrere Methoden zum Sortieren der Ergebnisse.
Die Methode sortByName() sortiert die Ergebnisse nach dem Dateinamen. Die Sortierung ist natürlich, sie behandelt
also Zahlen in Namen korrekt und gibt zum Beispiel foo1.txt vor foo10.txt zurück.
Der Finder erlaubt das Sortieren auch mit einem eigenen Callback. Er bekommt als Parameter zwei Objekte
Nette\Utils\FileInfo und muss das Ergebnis des Vergleichs mit dem Operator <=> zurückgeben (also
-1, 0 oder 1). So sortieren wir die Dateien zum Beispiel nach ihrer Größe:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Mehrere verschiedene Suchen
Wenn Sie mehrere Mengen von Dateien an verschiedenen Orten oder nach verschiedenen Kriterien finden müssen, verwenden Sie die
Methode append(). Sie gibt ein neues Finder-Objekt zurück, sodass Sie die Methodenaufrufe für die
angehängte Suche verketten können:
($finder = new Finder) // den ersten Finder in der Variablen $finder speichern!
->files('*.php') // in src/ nach *.php-Dateien suchen
->from('src')
->append()
->files('*.md') // in docs/ nach *.md-Dateien suchen
->from('docs')
->append()
->files('*.json'); // im aktuellen Verzeichnis nach *.json-Dateien suchen
Alternativ lässt sich die Methode append() verwenden, um eine bestimmte Datei (oder ein Array von Dateien)
hinzuzufügen. In diesem Fall gibt sie dasselbe Finder-Objekt zurück:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo ist eine Klasse, die eine in den Suchergebnissen gefundene Datei oder ein Verzeichnis repräsentiert. Sie erweitert die Klasse SplFileInfo und liefert Informationen wie die Dateigröße, das Datum der letzten Änderung, den Namen, den Pfad und so weiter.
Zusätzlich bietet sie Methoden, die den relativen Pfad zurückgeben, was beim rekursiven Durchlaufen nützlich ist:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
Außerdem stehen Methoden zum Lesen und Schreiben des Dateiinhalts zur Verfügung:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Rückgabe der Ergebnisse als Array
Wie in den Beispielen zu sehen, implementiert der Finder das Interface IteratorAggregate, Sie können die
Ergebnisse also mit foreach durchlaufen. Er ist so gebaut, dass die Ergebnisse erst während des Durchlaufs geladen
werden: Wenn Sie also sehr viele Dateien haben, wartet er nicht darauf, dass zuerst alle eingelesen werden.
Sie können die Ergebnisse mit der Methode collect() auch als Array von Nette\Utils\FileInfo-Objekten
abholen. Das Array ist numerisch indiziert, nicht assoziativ.
$array = Finder::findFiles('*.php')->collect();