Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

Pohodlné procházení filesystémem

Vymyslet dobré API je někdy neskutečný porod. Vedle toho skutečný porod je procházka růžovou ordinací. Snad dva roky jsem neustále překopával třídu na procházení adresářů. A stále nebyl spokojen. Přitom taková blbost. Existuje totiž spousta variant toho, co a jak hledat, které soubory vracet, které adresáře vracet a které procházet rekurzivně, nebo naopak kterým se vyhnout. Také jsem potřeboval řešit specifické situace, kupříkladu když během procházení adresářovou strukturou teprve zjišťuji dodatečná pravidla. Otázkou bylo, jak to navrhnout univerzálně a pokud možno srozumitelně.

Výsledkem snažení je třída Nette\Finder, jejíž API není dokonalé, ale je zatím asi to nejpoužitelnější, k jakému jsem se dopracoval. Můžete si ji stáhnout na GitHubu.

Pár příkladů použití:

// nerekurzivní hledání souborů *.txt v adresáři $dir
foreach (Finder::findFiles('*.txt')->in($dir) as $key => $file) {
    echo $key; // $key je řetězec s názvem souboru včetně cesty
    echo $file; // $file je objektem SplFileInfo
}

// rekurzivní hledání souborů *.txt
foreach (Finder::findFiles('*.txt')->from($dir) as $file) {
    echo $file;
}

// hledání podle více masek a dokonce z více adresářů v rámci jedné iterace
foreach (Finder::findFiles('*.txt', '*.php')->in($dir1, $dir2) as $file) {
}

// rekurzivní hledání souborů *.txt obsahujících číslici v názvu
foreach (Finder::findFiles('*[0-9]*.txt')
    ->from($dir) as $file) {
}

// rekurzivní hledání souborů *.txt kromě těch, co obsahují v názvu X
// pozn.: exclude se tu vztahuje na findFiles()
foreach (Finder::findFiles('*.txt')->exclude('*X*')
    ->from($dir) as $file) {
}

// rekurzivní hledání souborů *.txt umístěných v adresáři
// začínajícím na "te" ale nikoliv "temp"
foreach (Finder::findFiles('te*/*.txt')->exclude('temp*/*')
    ->from($dir) as $file) {
}

Omezit hloubku procházení lze metodou limitDepth().

Kromě souborů lze hledat i adresáře přes Finder::findDirectories('subdir*') nebo obojí Finder::find('file.txt'). V takovém případě se maska vztahuje na soubory, nikoliv adresáře.

Adresáře, kterým se chceme zcela vyhnout, uvedeme za klauzulí „from“:

// tady se exclude vztahuje na klauzuli "from"
foreach (Finder::findFiles('*.php')
    ->from($dir)->exclude('temp', '.git') as $file) {
}

Nejen maskou lze výsledky filtrovat:

// prochází soubory v rozmezí 100B až 200B
foreach (Finder::findFiles('*.php')->size('>=', 100)->size('<=', 200)
    ->from($dir) as $file) {
}

// prochází soubory změněné v posledních dvou týdnech
foreach (Finder::findFiles('*.php')->date('>', '- 2 weeks')
    ->from($dir) as $file) {
}

// prochází soubory PHP s počtem řádku větším než 1000 filtrujeme callbackem
$finder = Finder::findFiles('*.php')->filter(function($file) {
    return count(file($file->getPathname())) > 1000;
})->from($dir);

V Nette lze jít dál a třídu Nette\Finder skrze extension methods dále rozšiřovat a poté můžete třeba:

// hledat obrázky s rozměry většími než 50px x 50px
foreach (Finder::findFiles('*')->dimensions('>50', '>50')
    ->from($dir) as $file) {
}

Třída funguje na Windows i Linuxu a je napsána co nejoptimálněji, měla by tudíž fungovat velmi rychle a neprochází zbytečně adresáře, které nemá. Enjoy!

Komentáře

  1. v6ak http://v6ak.profitux.cz/ #1

    Zajímavé. Měl bych k tomu dvě poznámky:

    • Proč se jmenuje Nette\Finder a ne třeba Nette\Files\Finder nebo Nette\FileFinder?
    • in a from: nevíím, jak moc je na první pohled zřejmý rozdíl (samozřejmě na těchto příkladech rozdíl zřejmý je), někdo to může považovat za alias.

    Ale jinak to API vypadá celkem použitelně.

    před 6 lety | reagoval [3] David Grudl
  2. Jan Tichý http://www.phpguru.cz #2

    avatar

    Takhle na první nástřel a bez hlubšího přemýšlení bych to udělal obráceně. Nikoliv tak, že zadám masku a tu pak filtruju konkrétním adresářem. Ale že zvolím adresář a v tom pak provádím různé věci, například filtrování určitou maskou:

    $finder = new Finder($dir);
    foreach ($finder->find('*.txt') as $key => $file) {
        // tady si z daného adresáře vypíšeme textové soubory
    }
    foreach ($finder->dimensions('>50', '>50') as $key => $file) {
        // tady si z daného adresáře vypíšeme dané obrázky
    }

    Navíc pokud by se našel nějaký podadresář, tak by se mohl hned vrátit nikoliv jako SplFileInfo, alébrž opět jako instance Finder (ať už by se to pak místo Finder nazvalo jakkoliv, třeba Dir ;)), což by pak opět umožňovalo nad takovým podadresářem se ptát dalšími podmínkami, třeba nějak takhle:

    $finder = new Finder($dir);
    foreach ($finder->type(Finder::DIRECTORY) as $dir) {
        if ($dir->size < 100) {
            foreach ($dir->dimensions('>50', '>50') as $key => $file) {
                // tady si ze všech adresářů s velikostí menší než 100
                // vypíšeme obrázky větší než 50x50
            }
        }
    }
  3. David Grudl http://davidgrudl.com #3

    avatar

    #1 v6aku, protože to umí hledat i adresáře a FileFinder::findDirectories() by bylo divné

    #2 Jane Tichý, váhal jsem nad pořadím adresáře a masky a nakonec si řekl, že půjdu cestou SQL, tedy aby to znělo jako věta. Takže je to podle vzoru SELECT * FROM dir.

    Vrácet podadresáře jako Finder nevidím jako dobrý nápad. Celou věc to zbytečně komplikuje a nenapadá mě, co by to přineslo navíc oproti současnému pojetí.

    před 6 lety | reagoval [5] The Zero [6] v6ak
  4. Roman #4

    Poměrně podobné rozhraní má perlový modul File::Find::Rule (http://search.cpan.org/perldoc?…)

    Možná by se mohlo hodit pro inspiraci.

    před 6 lety
  5. The Zero http://www.thezero.info/ #5

    avatar

    #3 Davide Grudle, Já souhlasím s #2 Jan Tichý, že je to lepší. Navrch mám za to, že cestě SQL odpovídá právě jeho řešení: SELECT * FROM dir WHERE filename LIKE '%.txt'

    před 6 lety | reagoval [8] David Grudl
  6. v6ak http://v6ak.profitux.cz/ #6

    #3 Davide Grudle, Dobře, tak FS.

    Co se týče #2 Jan Tichý, jsem pro. Jinak zrovna pořadí sloupců a tabulky se mi v SQL zdá nešťastné. Ostatně, je to jeden z důvodů, proč s DibiFluent nemohu, … Mám tu větu dokončit? Obávám se OT diskuze.

    před 6 lety | reagoval [7] blizzboz [8] David Grudl
  7. blizzboz #7

    avatar

    #6 v6aku, hej presne napr. v LINQ je to poradie opačné (from in select) a mne to príde prirodzenejšie ako v klasickom SQL

    před 6 lety | reagoval [8] David Grudl
  8. David Grudl http://davidgrudl.com #8

    avatar
    před 6 lety
  9. pes #9

    ahoj, můžu mít dotaz k téhle bezva věci (myšleno finder)?
    Nedaří se mi seřadit výsledek, který mi finder vrátí … například něco jako : NFinder::findDirectories(‚*‘)->in($filePath)->order(„desc“)
    Řazení obejdu sice při zpracování výsledku, ale mám takový pocit, že to tam určitě je někde vyřešené a já to nemůžu najít :(
    díky za případné info

    před 6 lety
  10. David Grudl http://davidgrudl.com #10

    avatar

    Řazení tam skutečně není. Dalo by se udělat třeba takto:

    $dirs = iterator_to_array(NFinder::findDirectories(‚*‘)->in($filePath));
    ksort($dirs);
    před 6 lety | reagoval [11] pes [12] Jan Tvrdík [13] David Grudl
  11. pes #11

    #10 Davide Grudle, Super ;) dobrý „trik“ .. díky

    před 6 lety
  12. Jan Tvrdík #12

    avatar

    #10 Davide Grudle, Nestálo by za to, tam přidat pro řazení nativní podporu?

    před 6 lety | reagoval [13] David Grudl
  13. David Grudl http://davidgrudl.com #13

    avatar

    #12 Jane Tvrdíku, neznám jiné řešení než #10 David Grudl, což je pomalé a tudíž bych nechtěl, aby to bylo snadno dosažitelné jedním příkazem.

    před 6 lety
  14. Filip Procházka http://hosiplan.kdyby.org #14

    avatar

    Ahoj, při použití samotného

    foreach (Finder::findFiles('*.php')->size('>=', 100)->size('<=', 200) as $file) { ...

    Vyskočí výjimka, asi by bylo dobré ty příklady opravit :) některé jedince to mate viz https://github.com/…e/issues/277#…

    před 5 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.