Finder: vyhledávání souborů
Potřebujete najít soubory vyhovující určité masce? Finder vám v tom pomůže. Je to všestranný a rychlý nástroj pro procházení adresářové struktury.
Instalace:
composer require nette/utils
Příklady předpokládají vytvořený alias:
use Nette\Utils\Finder;
Použití
Nejprve si ukážeme, jak můžete pomocí Nette\Utils\Finder vypsat jména souborů s příponami
.txt a .md v aktuálním adresáři:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
Výchozí adresář pro hledání je aktuální adresář, ale můžete ho změnit pomocí metod in() nebo from(). Proměnná $file je instancí třídy FileInfo, zatímco $name je řetězec s cestou k souboru.
Cesta se vrací tak, jak jste ji zapsali, se zachovanými oddělovači platformy; na Windows tak výsledek může míchat
/ a \. Pokud potřebujete jednotný tvar, zavolejte FileSystem::unixSlashes().
Co se má hledat?
Kromě metody findFiles() existuje i findDirectories(), která hledá jen adresáře, a
find(), která hledá obojí. Tyto metody jsou statické, takže je lze volat bez vytvoření instance. Parametr
s maskou je volitelný, pokud ho neuvedete, vyhledá se vše.
foreach (Finder::find() as $file) {
echo $file; // nyní se vypíší všechny soubory i adresáře
}
Pomocí metod files() a directories() můžete doplňovat co dalšího se má vyhledat. Metody lze
volat opakovaně a jako parametr lze uvést i pole masek:
Finder::findDirectories('vendor') // všechny adresáře
->files(['*.php', '*.phpt']); // plus všechny PHP soubory
Alternativou statických metod je vytvoření instance pomocí new Finder (takto vytvořený čerstvý objekt
nevyhledává nic) a uvedení co hledat pomocí files() a directories():
(new Finder)
->directories() // všechny adresáře
->files('*.php'); // plus všechny PHP soubory
V masce můžete používat zástupné znaky jako *, **,
? a [...]. Dokonce můžete specifikovat i adresáře, například src/*.php najde
všechny PHP soubory v adresáři src. Symlinky jsou také považovány za adresáře nebo soubory.
Kde se má hledat?
Výchozí adresář pro hledání je aktuální adresář. Změníte ho pomocí metod in() a
from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // hledá přímo v src/ a tests/
->from('vendor'); // hledá i v podadresářích vendor/
Metody se liší hloubkou: in() hledá pouze v daném adresáři, zatímco from() sestupuje i do
jeho podadresářů (rekurzivně). Pokud chcete prohledat aktuální adresář rekurzivně, použijte from('.').
Rekurzi ovšem neurčuje jen from() – řídí ji i zástupný znak ** v masce, takže
findFiles('**/*.php')->in('src') také hledá rekurzivně. Jinak řečeno, from('src') je jen zkratka
za in('src') s rekurzivní maskou. Viz Zástupné znaky.
Tyto metody lze volat vícekrát nebo jim předat více cest jakožto pole, soubory se pak budou hledat ve všech
adresářích. Pokud některý z adresářů neexistuje, vyhodí se výjimka Nette\InvalidStateException.
Relativní cesty jsou relativní vůči aktuálnímu adresáři, lze ale uvést i cesty absolutní:
Finder::findFiles('*.php')
->in('/var/www/html');
V cestě lze použít zástupné znaky *, ** a ?, ale ne [...],
které se tam berou doslova. Tím se předchází nežádoucímu chování, když třeba hledáte in(__DIR__) a
v cestě se náhodou vyskytnou znaky []. Například src/*/*.php hledá všechny PHP soubory
v adresářích druhé úrovně v adresáři src.
Při vyhledávání souborů i adresáře do hloubky se vrací nejprve rodičovský adresář a teprve poté soubory v něm
obsažené, což lze obrátit pomocí childFirst().
Zástupné znaky
Maska může obsahovat několik speciálních znaků:
*– libovolný počet znaků, kromě oddělovače/(zůstává v rámci jedné úrovně adresářů)**– libovolný počet znaků, včetně/(překlenuje úrovně adresářů, viz níže)?– právě jeden znak, kromě/[a-z]– jeden znak z rozsahu nebo výčtu v hranatých závorkách[!a-z]– jeden znak, který v závorkách není
Zásadní a snadno přehlédnutelný bod: ** matchuje nula a více úrovní adresářů. Neznamená
„aspoň jeden podadresář". Proto src/**/*.php odpovídá souboru ležícímu přímo v src stejně
dobře jako souboru zanořenému o několik úrovní hlouběji. Uvažujme tento strom:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
Následující tabulka ukazuje, co jednotlivé masky najdou, a to pro soubory i adresáře:
| Maska | Najde |
|---|---|
src/*.php |
jen src/app.php (přímo v src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (všechny
úrovně, včetně přímo v src) |
src/* |
přímé potomky src: app.php, Model, Control |
src/** |
vše pod src, soubory i adresáře (zkratka za src/**/*) |
src/*/ |
přímé podadresáře src: Model, Control |
src/**/ |
všechny podadresáře v jakékoliv hloubce: Model,
Model/Repository, Control |
Za zapamatování stojí dvě zkratky:
**, za kterým bezprostředně nenásleduje/, se chová jako**/plus*. Takžesrc/**je zkratka zasrc/**/*a**.phpza**/*.php.- Koncové lomítko omezí masku jen na adresáře. Takže
find('log/')vrátí adresáře jménemlog, ale nikdy stejnojmenný soubor. (findFiles()koncové lomítko odmítne, protože hledat soubor-„adresář" nedává smysl.)
Další příklady použití:
img/?.png– soubory s jednopísmenným názvem jako0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– logy ve formátuYYYY-MM-DDdocs/**/*.md– všechny soubory s příponou.mdv adresářidocsa všech jeho podadresářích
Vyloučení
Pomocí metody exclude() lze vyloučit soubory a adresáře z výsledků. Parametrem je maska, které položka
nesmí vyhovovat. Zde hledáme soubory *.txt kromě těch, co obsahují v názvu písmeno X:
Finder::findFiles('*.txt')
->exclude('*X*');
Maska pro vyloučení používá úplně stejnou gramatiku jako masky pro hledání – stejné zástupné znaky, kotvení přes ./ i zkratku **. Její koncová část
určuje rozsah vyloučení:
| Maska | Vyloučí |
|---|---|
temp |
jakýkoliv soubor nebo adresář jménem temp, v jakékoliv hloubce |
temp/ |
jen adresář temp (a jeho obsah); soubor jménem temp zůstane |
temp/* |
obsah temp, ale samotný adresář temp ponechá |
temp/** |
totéž jako temp/* |
Vyloučený adresář se při procházení ani neotevře, takže vyloučení celých podstromů hledání i zrychluje. Takto přeskočíte konkrétní podadresáře:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Filtrování
Finder nabízí několik metod pro filtrování výsledků (tj. jejich redukci). Můžete je kombinovat a volat opakovaně.
Pomocí size() filtrujeme podle velikosti souboru. Takto najdeme soubory s velikostí v rozmezí 100 až
200 bytů:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
Metoda date() filtruje podle data poslední změny souboru. Hodnoty mohou být absolutní nebo relativní
k aktuálnímu datu a času, například takto najdeme soubory změněné v posledních dvou týdnech:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Obě funkce rozumí operátorům >, >=, <, <=, =,
!=, <>.
Finder umožňuje také filtrovat výsledky pomocí vlastních funkcí. Funkce dostane jako parametr objekt
Nette\Utils\FileInfo a musí vrátit true, aby byl soubor zahrnut do výsledků.
Příklad: hledání souborů PHP, které obsahují řetězec Nette (bez ohledu na velikost písmen):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Filtrování do hloubky
Při rekurzivním vyhledávání můžete nastavit maximální hloubku procházení pomocí metody limitDepth().
Pokud nastavíte limitDepth(1), prochází se pouze první podadresáře, limitDepth(0) vypne
procházení do hloubky a hodnota –1 ruší limit.
Finder umožňuje pomocí vlastních funkcí rozhodovat, do kterého adresáře se má při procházení vstoupit. Funkce
dostane jako parametr objekt Nette\Utils\FileInfo a musí vrátit true, aby se do adresáře
vstoupilo:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Nečitelné adresáře
Ve výchozím nastavení Finder přeskočí adresáře, které nemůže přečíst (například kvůli nedostatečným
oprávněním). Pokud chcete, aby v takovém případě místo toho vyhodil výjimku, zavolejte
ignoreUnreadableDirs(false).
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Řazení
Finder nabízí také několik funkcí pro řazení výsledků.
Metoda sortByName() řadí výsledky podle názvů souborů. Řazení je naturální, tedy správně si poradí
s čísly v názvech a vrací např. foo1.txt před foo10.txt.
Finder umožňuje také řadit pomocí vlastní funkce. Ta dostane jako parametr dva objekty Nette\Utils\FileInfo
a musí vrátit výsledek porovnání operátorem <=>, tedy -1, 0 nebo
1. Například takto seřadíme soubory podle velikosti:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Více různých hledání
Pokud potřebujete najít více různých souborů v různých lokacích nebo splňujících jiná kritéria, použijte metodu
append(). Vrací nový objekt Finder, takže je možné řetězit volání metod:
($finder = new Finder) // do proměnné $finder si uložíme první Finder!
->files('*.php') // v src/ hledáme soubory *.php
->from('src')
->append()
->files('*.md') // v docs/ hledáme soubory *.md
->from('docs')
->append()
->files('*.json'); // v aktuální složce hledáme soubory *.json
Alternativně lze použít metodu append() pro přidání konkrétního souboru (nebo pole souborů). Pak vrací
ten samý objekt Finder:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo je třída reprezentující soubor nebo adresář ve výsledcích hledání. Jde o rozšíření třídy SplFileInfo, která poskytuje informace, jako je velikost souboru, datum poslední změny, jméno, cesta, atd.
Navíc poskytuje metody pro vrácení relativní cesty, což je užitečné při procházení do hloubky:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
Dále máte k dispozici metody pro přečtení a zápis obsahu souboru:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Vrácení výsledků jako pole
Jak bylo vidět v příkladech, Finder implementuje rozhraní IteratorAggregate, takže můžete použít
foreach pro procházení výsledků. Je naprogramovaný tak, že výsledky jsou načítány pouze v průběhu
procházení, takže pokud máte velké množství souborů, nečeká se, než se všechny přečtou.
Výsledky si můžete nechat také vrátit jako pole objektů Nette\Utils\FileInfo, a to metodou
collect(). Pole není asociativní, ale numerické.
$array = Finder::findFiles('*.php')->collect();