Finder: ファイルの検索

あるマスクに合うファイルを見つけたいですか。Finder が助けてくれます。ディレクトリ構造をたどるための、多用途で高速な道具です。

インストール:

composer require nette/utils

以下の例では、次のクラスの別名が作られているものとします。

use Nette\Utils\Finder;

使い方

まずは、Nette\Utils\Finder を使って現在のディレクトリにある拡張子 .txt.md のファイル名を並べる方法を見てみましょう。

foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
	echo $file;
}

既定の検索ディレクトリは現在のディレクトリですが、in() や from()メソッドで変えられます。$file 変数は FileInfoクラスのインスタンス、$name はファイルへのパスを保持する文字列です。

パスは書いたとおりの形で返され、プラットフォームの区切り文字が保たれます。ですから Windows では結果に /\ が混ざることがあります。形をそろえたい場合は FileSystem::unixSlashes() を呼んでください。

何を検索するか

findFiles() メソッドのほかに、ディレクトリだけを探す findDirectories() と、その両方を探す find() があります。これらは静的メソッドなので、インスタンスを作らずに呼べます。マスクの引数は省略でき、省略するとすべてが一致します。

foreach (Finder::find() as $file) {
	echo $file; // これですべてのファイルとディレクトリが並びます
}

files()directories() メソッドを使うと、検索対象を追加できます。これらのメソッドは繰り返し呼べますし、引数にマスクの配列を渡すこともできます。

Finder::findDirectories('vendor') // すべてのディレクトリ
	->files(['*.php', '*.phpt']); // に加えてすべての PHP ファイル

静的メソッドの代わりに、new Finder でインスタンスを作り(こうして作ったオブジェクトは最初は何も検索しません)、files()directories() で何を探すかを指定することもできます。

(new Finder)
	->directories()      // すべてのディレクトリ
	->files('*.php');    // に加えてすべての PHP ファイル

マスクには ***?[...] といったワイルドカードが使えます。ディレクトリを指定することもでき、たとえば src/*.phpsrc ディレクトリのすべての PHP ファイルを見つけます。シンボリックリンクもディレクトリやファイルとして扱われます。

どこを検索するか

既定の検索ディレクトリは現在のディレクトリです。in()from() メソッドで変えられます。

Finder::findFiles('*.php')
	->in(['src', 'tests']) // src/ と tests/ の直下を検索します
	->from('vendor');      // vendor/ のサブディレクトリも検索します

この 2 つのメソッドは深さが違います。in() は指定したディレクトリの中だけを検索し、from() はそのサブディレクトリにも(再帰的に)下りていきます。現在のディレクトリを再帰的に検索するには from('.') を使います。

ただし再帰は from() だけで決まるわけではありません。マスクの ** ワイルドカードもそれを決めるので、findFiles('**/*.php')->in('src') も再帰的に検索します。言い換えれば、from('src') は再帰的なマスクを伴う in('src') の近道にすぎません。ワイルドカードをご覧ください。

これらのメソッドは何度でも呼べますし、複数のパスを配列で渡すこともできます。その場合、指定したすべてのディレクトリでファイルが検索されます。いずれかのディレクトリが存在しない場合は Nette\InvalidStateException が投げられます。

相対パスは現在のディレクトリからの相対ですが、絶対パスも使えます。

Finder::findFiles('*.php')
	->in('/var/www/html');

パスには ***? のワイルドカードが使えますが、[...]使えず、そこでは文字どおりに扱われます。これは、たとえば in(__DIR__) で検索したときにパスに [] の文字が含まれていた場合の思わぬ振る舞いを防ぎます。たとえば src/*/*.phpsrc の下の 2 階層目のディレクトリにあるすべての PHP ファイルを検索します。

ファイルとディレクトリを再帰的に(深さ優先で)検索するとき、まず親ディレクトリが返され、続いてその中のファイルが返されます。この順序は childFirst() で逆にできます。

ワイルドカード

マスクにはいくつかの特別な文字を含められます。

  • * – 任意の数の文字。ただし区切りの / は除きます(ひとつのディレクトリ階層にとどまります)
  • ** – 任意の数の文字。/含みます(ディレクトリの階層をまたぎます。後述)
  • ? – ちょうど 1 文字。ただし / は除きます
  • [a-z] – かっこの中の範囲や集合のうち 1 文字
  • [!a-z] – かっこの中にない 1 文字

決定的で見落としやすい点があります。**ゼロ個以上のディレクトリ階層に一致します。「少なくともひとつのサブディレクトリ」という意味ではありません。ですから src/**/*.php は、src の直下に置かれたファイルにも、何階層も奥に埋もれたファイルにも同じように一致します。次の木を考えてみましょう。

src/
├── app.php
├── Model/
│   ├── User.php
│   └── Repository/
│       └── UserRepository.php
└── Control/
    └── SignForm.php

次の表は、それぞれのマスクがファイルとディレクトリの両方について何に一致するかを示します。

マスク 一致するもの
src/*.php src/app.php だけ(src の直下)
src/**/*.php src/app.phpsrc/Model/User.phpsrc/Model/Repository/UserRepository.phpすべての階層。src の直下も含みます
src/* src の直接の子: app.phpModelControl
src/** src の下のすべて。ファイルもディレクトリも(src/**/* の短縮形)
src/*/ src の直接のサブディレクトリ: ModelControl
src/**/ 深さを問わないすべてのサブディレクトリ: ModelModel/RepositoryControl

覚えておく価値のある近道が 2 つあります。

  • 直後に / が続かない ** は、**/* を足したものとして振る舞います。ですから src/**src/**/* の、**.php**/*.php の短縮形です。
  • 末尾のスラッシュは、マスクをディレクトリだけに限定します。ですから find('log/')log という名前のディレクトリを返しますが、その名前のファイルは決して返しません。(findFiles() は末尾のスラッシュを拒否します。「ディレクトリ」というファイルを探すのは意味をなさないからです。)

さらに使用例を挙げます。

  • img/?.png – 0.png1.pngx.png のように 1 文字の名前のファイル
  • logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log – YYYY-MM-DD 形式のログファイル
  • docs/**/*.md – docs とそのすべてのサブディレクトリにある拡張子 .md のすべてのファイル

除外

結果からファイルやディレクトリを落とすには exclude() メソッドを使います。引数は、その項目が一致してはいけないマスクです。ここでは名前に X の文字を含むものを除いて *.txt のファイルを検索します。

Finder::findFiles('*.txt')
	->exclude('*X*');

除外のマスクは検索のマスクとまったく同じ文法を使います。同じワイルドカード./ による起点の指定、** の短縮形です。その末尾の部分が除外の範囲を決めます。

マスク 除外されるもの
temp 深さを問わず temp という名前のファイルまたはディレクトリ
temp/ temp というディレクトリ(とその中身)だけ。temp という名前のファイルは残ります
temp/* temp の中身。ただし temp ディレクトリ自体は残ります
temp/** temp/* と同じ

除外されたディレクトリは走査中に入ることさえないので、部分木ごと除外すると検索も速くなります。特定のサブディレクトリを飛ばすにはこうします。

Finder::findFiles('*.php')
	->from($dir)
	->exclude('temp', '.git');

絞り込み

Finder には結果を絞り込む(つまり減らす)メソッドがいくつかあります。組み合わせられますし、繰り返し呼べます。

size() を使うとファイルサイズで絞り込めます。こうして 100 から 200 バイトの範囲のファイルを見つけます。

Finder::findFiles('*.php')
	->size('>=', 100)
	->size('<=', 200);

date() メソッドはファイルの最終更新日で絞り込みます。値には絶対的な日付も、現在の日時からの相対も使えます。たとえば次は過去 2 週間以内に変更されたファイルを見つけます。

Finder::findFiles('*.php')
	->date('>', '-2 weeks')
	->from($dir)

どちらのメソッドも演算子 >>=<<==!=<> を理解します。

Finder では独自のコールバックで結果を絞り込むこともできます。コールバックはパラメータとして Nette\Utils\FileInfo オブジェクトを受け取り、そのファイルを結果に含めるなら true を返さなければなりません。

例: 文字列 'Nette' を含む PHP ファイルを探します(大文字小文字は区別しません)。

Finder::findFiles('*.php')
	->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));

深さによる絞り込み

再帰的に検索するとき、limitDepth() メソッドで走査の最大の深さを設定できます。limitDepth(1) はサブディレクトリの 1 階層目だけを辿り、limitDepth(0) は深さ方向の走査を完全に無効にし、-1 は深さの制限をなくします。

Finder では、走査中にどのディレクトリに入るかを独自のコールバックで決められます。コールバックはそのディレクトリを表す Nette\Utils\FileInfo オブジェクトを受け取り、入るなら true を返さなければなりません。

Finder::findFiles('*.php')
	->descentFilter(fn($file) => $file->getBasename() !== 'temp');

読めないディレクトリ

既定では、Finder は読めないディレクトリ(権限が足りない場合など)を飛ばします。そうした場合に例外を投げてほしいなら、ignoreUnreadableDirs(false) を呼んでください。

Finder::findFiles('*.php')
	->from($dir)
	->ignoreUnreadableDirs(false);

並べ替え

Finder には結果を並べ替えるメソッドもいくつかあります。

sortByName() メソッドはファイル名で結果を並べ替えます。並べ替えは自然順で、名前の中の数字を正しく扱い、たとえば foo10.txt より先に foo1.txt を返します。

Finder では独自のコールバックで並べ替えることもできます。コールバックはパラメータとして 2 つの Nette\Utils\FileInfo オブジェクトを受け取り、<=> 演算子による比較の結果(つまり -101)を返さなければなりません。たとえば次はファイルをサイズで並べ替えます。

$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());

複数の異なる検索

複数の場所や異なる条件でファイルの集合を見つける必要があるなら、append() メソッドを使います。新しい Finder オブジェクトを返すので、付け足した検索についてメソッドを連ねて呼べます。

($finder = new Finder) // 最初の Finder を $finder 変数に入れておきます!
	->files('*.php')   // src/ で *.php ファイルを検索
	->from('src')
	->append()
	->files('*.md')    // docs/ で *.md ファイルを検索
	->from('docs')
	->append()
	->files('*.json'); // 現在のフォルダで *.json ファイルを検索

あるいは append() メソッドで特定のファイル(やファイルの配列)を追加することもできます。この場合は同じ Finder オブジェクトを返します。

$finder = Finder::findFiles('*.txt')
	->append(__FILE__);

FileInfo

Nette\Utils\FileInfo は、検索結果として見つかったファイルやディレクトリを表すクラスです。SplFileInfo クラスを継承し、ファイルサイズ、最終更新日、名前、パスなどの情報を提供します。

さらに、再帰的な走査で便利な相対パスを返すメソッドもあります。

foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
	$absoluteFilePath = $file->getRealPath();
	$relativeFilePath = $file->getRelativePathname();
}

加えて、ファイルの内容を読み書きするメソッドも使えます。

foreach ($finder as $file) {
    $contents = $file->read();
    // ...
    $file->write($contents);
}

結果を配列として受け取る

例で見たとおり、Finder は IteratorAggregate インターフェースを実装しているので、foreach で結果を辿れます。結果は反復中にはじめて読み込まれる設計なので、ファイルが大量にあっても、あらかじめ全部が読まれるのを待つことはありません。

collect() メソッドを使うと、結果を Nette\Utils\FileInfo オブジェクトの配列として受け取れます。この配列は連想配列ではなく、数値のインデックスを持ちます。

$array = Finder::findFiles('*.php')->collect();
バージョン: 4.x