Finder: cercare i file

Avete bisogno di trovare i file che corrispondono a una certa maschera? Finder può aiutarvi. È uno strumento versatile e veloce per esplorare le strutture di directory.

Installazione:

composer require nette/utils

Gli esempi presuppongono che sia stato creato questo alias di classe:

use Nette\Utils\Finder;

Uso

Vediamo per prima cosa come usare Nette\Utils\Finder per elencare i nomi dei file con estensione .txt e .md nella directory corrente:

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

La directory di ricerca predefinita è quella corrente, ma potete cambiarla con i metodi in() o from(). La variabile $file è un'istanza della classe FileInfo, mentre $name è una stringa che contiene il percorso del file.

Il percorso viene restituito così come lo avete scritto, conservando i separatori della piattaforma; su Windows il risultato può quindi mescolare / e \. Chiamate FileSystem::unixSlashes() se vi serve una forma uniforme.

Cosa cercare?

Oltre al metodo findFiles() esistono findDirectories(), che cerca solo le directory, e find(), che cerca entrambi. Questi metodi sono statici, quindi si possono chiamare senza creare un'istanza. L'argomento con la maschera è facoltativo; se lo omettete, viene trovato tutto.

foreach (Finder::find() as $file) {
	echo $file; // ora vengono elencati tutti i file e tutte le directory
}

Con i metodi files() e directories() potete indicare altri elementi da cercare. I metodi si possono chiamare più volte e come argomento si può passare anche un array di maschere:

Finder::findDirectories('vendor') // tutte le directory
	->files(['*.php', '*.phpt']); // più tutti i file PHP

Un'alternativa ai metodi statici è creare un'istanza con new Finder (un oggetto appena creato così all'inizio non cerca nulla) e indicare cosa cercare con files() e directories():

(new Finder)
	->directories()      // tutte le directory
	->files('*.php');    // più tutti i file PHP

Nella maschera potete usare i caratteri jolly come *, **, ? e [...]. Potete indicare anche delle directory: per esempio src/*.php trova tutti i file PHP nella directory src. Anche i link simbolici vengono trattati come directory o file.

Dove cercare?

La directory di ricerca predefinita è quella corrente. La cambiate con i metodi in() e from():

Finder::findFiles('*.php')
	->in(['src', 'tests']) // cerca direttamente in src/ e tests/
	->from('vendor');      // cerca anche nelle sottodirectory di vendor/

I due metodi differiscono per profondità: in() cerca solo dentro la directory indicata, mentre from() scende anche nelle sue sottodirectory (ricorsivamente). Per cercare ricorsivamente nella directory corrente usate from('.').

La ricorsione non è però decisa solo da from(): la guida anche il carattere jolly ** nella maschera, quindi anche findFiles('**/*.php')->in('src') cerca ricorsivamente. In altre parole, from('src') è solo una scorciatoia per in('src') con una maschera ricorsiva. Vedi Caratteri jolly.

Questi metodi si possono chiamare più volte, oppure potete passare più percorsi come array; i file verranno allora cercati in tutte le directory indicate. Se una delle directory non esiste, viene sollevata Nette\InvalidStateException.

I percorsi relativi sono relativi alla directory corrente, ma si possono usare anche percorsi assoluti:

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

Nel percorso potete usare i caratteri jolly *, ** e ?, ma non [...], che lì viene preso alla lettera. Questo evita comportamenti indesiderati quando, per esempio, cercate con in(__DIR__) e il percorso contiene per caso i caratteri []. Per esempio src/*/*.php cerca tutti i file PHP nelle directory di secondo livello sotto src.

Cercando file e directory in modo ricorsivo (in profondità), viene restituita prima la directory superiore e poi i file che contiene. Questo ordine si può invertire con childFirst().

Caratteri jolly

Una maschera può contenere diversi caratteri speciali:

  • * – un numero qualsiasi di caratteri, tranne il separatore / (resta all'interno di un solo livello di directory)
  • ** – un numero qualsiasi di caratteri, / compreso (attraversa i livelli di directory, vedi sotto)
  • ? – esattamente un carattere, tranne /
  • [a-z] – un carattere dell'intervallo o dell'insieme indicato tra parentesi
  • [!a-z] – un carattere non presente tra parentesi

Il punto essenziale, e facile da trascurare: ** corrisponde a zero o più livelli di directory. Non significa “almeno una sottodirectory”. Perciò src/**/*.php trova un file collocato direttamente in src esattamente come uno sepolto a diversi livelli di profondità. Consideriamo questo albero:

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

La tabella seguente mostra cosa trovano le singole maschere, sia per i file sia per le directory:

Maschera Trova
src/*.php solo src/app.php (direttamente in src)
src/**/*.php src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (tutti i livelli, compreso direttamente in src)
src/* i figli diretti di src: app.php, ModelControl
src/** tutto ciò che si trova sotto src, file e directory (forma abbreviata di src/**/*)
src/*/ le sottodirectory dirette di src: ModelControl
src/**/ tutte le sottodirectory a qualsiasi profondità: Model, Model/RepositoryControl

Vale la pena ricordare due scorciatoie:

  • Un ** non seguito immediatamente da / si comporta come **/ più *. Quindi src/** è una forma abbreviata di src/**/* e **.php di **/*.php.
  • Una barra finale limita la maschera alle sole directory. Quindi find('log/') restituisce le directory chiamate log, ma mai un file con quel nome. (findFiles() rifiuta la barra finale, perché cercare un file “directory” non ha senso.)

Altri esempi d'uso:

  • img/?.png – file con un nome di una sola lettera, come 0.png, 1.pngx.png
  • logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log – file di log nel formato YYYY-MM-DD
  • docs/**/*.md – tutti i file con estensione .md in docs e in tutte le sue sottodirectory

Esclusione

Usate il metodo exclude() per togliere file e directory dai risultati. L'argomento è una maschera alla quale l'elemento non deve corrispondere. Qui cerchiamo i file *.txt tranne quelli che contengono la lettera X nel nome:

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

La maschera di esclusione usa la stessa identica grammatica delle maschere di ricerca: gli stessi caratteri jolly, l'ancoraggio ./ e la scorciatoia **. La sua parte finale decide l'ampiezza dell'esclusione:

Maschera Esclude
temp qualsiasi file o directory chiamati temp, a qualsiasi profondità
temp/ solo una directory temp (e il suo contenuto); un file chiamato temp viene mantenuto
temp/* il contenuto di temp, ma mantiene la directory temp stessa
temp/** lo stesso di temp/*

Una directory esclusa non viene nemmeno percorsa durante l'attraversamento, quindi escludere interi sottoalberi velocizza anche la ricerca. Ecco come saltare determinate sottodirectory:

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

Filtraggio

Finder offre diversi metodi per filtrare i risultati (cioè per ridurli). Potete combinarli e chiamarli più volte.

Con size() filtriamo per dimensione del file. In questo modo troviamo i file di dimensione compresa tra 100 e 200 byte:

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

Il metodo date() filtra per data dell'ultima modifica del file. I valori possono essere date assolute oppure relative alla data e all'ora correnti. Per esempio, così troviamo i file modificati nelle ultime due settimane:

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

Entrambi i metodi comprendono gli operatori >, >=, <, <=, =, !=, <>.

Finder permette anche di filtrare i risultati con callback personalizzate. La callback riceve come parametro un oggetto Nette\Utils\FileInfo e deve restituire true perché il file venga incluso nei risultati.

Esempio: cercare i file PHP che contengono la stringa 'Nette' (senza distinguere maiuscole e minuscole):

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

Filtrare per profondità

Cercando ricorsivamente potete impostare la profondità massima di attraversamento con il metodo limitDepth(). Impostando limitDepth(1) viene percorso solo il primo livello di sottodirectory, limitDepth(0) disattiva del tutto l'attraversamento in profondità e il valore –1 toglie il limite di profondità.

Finder permette di usare callback personalizzate per decidere in quali directory entrare durante l'attraversamento. La callback riceve un oggetto Nette\Utils\FileInfo che rappresenta la directory e deve restituire true perché vi si entri:

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

Directory non leggibili

Per impostazione predefinita Finder salta le directory che non riesce a leggere (per esempio per permessi insufficienti). Se preferite che in questi casi sollevi un'eccezione, chiamate ignoreUnreadableDirs(false).

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

Ordinamento

Finder offre anche diversi metodi per ordinare i risultati.

Il metodo sortByName() ordina i risultati per nome del file. L'ordinamento è naturale, cioè gestisce correttamente i numeri nei nomi e restituisce per esempio foo1.txt prima di foo10.txt.

Finder permette anche di ordinare con una callback personalizzata. Riceve come parametri due oggetti Nette\Utils\FileInfo e deve restituire il risultato del confronto con l'operatore <=> (cioè -1, 0 o 1). Ecco per esempio come ordiniamo i file per dimensione:

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

Più ricerche diverse

Se avete bisogno di trovare più insiemi di file in posizioni diverse o secondo criteri diversi, usate il metodo append(). Restituisce un nuovo oggetto Finder, che vi permette di concatenare le chiamate ai metodi della ricerca aggiunta:

($finder = new Finder) // salvate il primo Finder nella variabile $finder!
	->files('*.php')   // cerca i file *.php in src/
	->from('src')
	->append()
	->files('*.md')    // in docs/ cerca i file *.md
	->from('docs')
	->append()
	->files('*.json'); // nella cartella corrente cerca i file *.json

In alternativa il metodo append() si può usare per aggiungere un file specifico (o un array di file). In tal caso restituisce lo stesso oggetto Finder:

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

FileInfo

Nette\Utils\FileInfo è una classe che rappresenta un file o una directory trovati nei risultati della ricerca. Estende la classe SplFileInfo e offre informazioni come la dimensione del file, la data dell'ultima modifica, il nome, il percorso e così via.

Offre inoltre metodi per restituire il percorso relativo, cosa utile durante l'attraversamento ricorsivo:

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

Sono disponibili anche metodi per leggere e scrivere il contenuto del file:

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

Restituire i risultati come array

Come si vede negli esempi, Finder implementa l'interfaccia IteratorAggregate, quindi potete usare foreach per scorrere i risultati. È progettato in modo che i risultati vengano caricati solo durante l'iterazione: se avete un gran numero di file, non aspetta quindi che siano stati letti tutti in anticipo.

Potete anche ottenere i risultati come array di oggetti Nette\Utils\FileInfo con il metodo collect(). L'array ha indici numerici, non associativi.

$array = Finder::findFiles('*.php')->collect();
versione: 4.x