Globbing Bash: wildcard, nullglob, dotglob ed extglob
Scopri come Bash espande i wildcard nei nomi di file prima del comando e come usare nullglob, dotglob, globstar ed extglob, inclusa la negazione.
Cosa fa il globbing di Bash prima dell'esecuzione del comando
Il globbing di Bash consiste nel fatto che la shell trasforma un pattern come *.log in un elenco ordinato di nomi di file reali, prima dell'avvio del comando. rm *.log non passa mai a rm un pattern. Bash legge la directory, mantiene i nomi che corrispondono al pattern, li ordina e quindi esegue rm passando ogni nome come argomento separato. Il programma rm non può sapere che è stato usato un pattern.
Quasi tutti i comportamenti sorprendenti del globbing derivano da questo fatto. La corrispondenza viene calcolata dalla shell in base ai file presenti in quel momento, mentre il comando riceve soltanto l'elenco finale. Il nome formale di questa operazione è espansione dei nomi di percorso. Globbing è il termine di uso comune; i due termini indicano la stessa operazione.
Creare un piccolo albero di directory per fare pratica
Tutti gli esempi seguenti operano sugli stessi file. Creali in una directory temporanea, così non avrai vicino nulla di importante e potrai eliminare tutto con un solo comando.
lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.logmktemp -d crea una nuova directory e ne restituisce il percorso, mentre $(...) salva quel percorso nella variabile lab. Se questa sintassi è nuova per te, come la sostituzione dei comandi acquisisce l'output di un comando la spiega in dettaglio.
Controlla ciò che hai creato e verifica la shell, perché una nota finale dipende dalla versione:
ls -a
bash --versionTutto questo funziona con bash 4.0 e versioni successive. La nota globskipdots verso la fine si applica a bash 5.2 e versioni successive, la versione inclusa nelle release correnti di Ubuntu e Debian ad agosto 2026.
Il comando di anteprima usato in tutto il tutorial è printf '%s\n' PATTERN. Stampa una corrispondenza per riga e non modifica nulla sul disco, quindi è sicuro eseguirlo con qualsiasi pattern su cui hai dubbi.
I tre wildcard: asterisco, punto interrogativo e parentesi quadre
*corrisponde a qualsiasi sequenza di caratteri, anche vuota.?corrisponde esattamente a un carattere.[...]corrisponde esattamente a un carattere scelto dall’insieme tra parentesi quadre.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *Confronta la seconda riga con l’elenco dei file che hai creato. ? rappresenta un solo carattere, quindi un suffisso di due cifre non rientra in questo pattern. Estendilo a app.log.[0-9]* ed eseguilo di nuovo per vedere la differenza.
Per tutti e tre i wildcard valgono due regole. Un wildcard non corrisponde mai a un /, quindi un pattern rimane all’interno di un singolo livello di directory. Inoltre, un . iniziale deve essere digitato letteralmente, quindi un pattern che inizia con * ignora tutti i nomi nascosti. Per questo rm * non considera i dotfile.
I risultati dei glob vengono restituiti in ordine, usando l’ordine di collazione della locale corrente, cioè un ordinamento testuale carattere per carattere. Un suffisso 10 viene quindi ordinato prima di un suffisso 2, perché il confronto non interpreta nessuno dei due come un numero. Esegui printf '%s\n' app.log.* e poi ls -v app.log.* e confronta i risultati: ls -v ordina i numeri incorporati in base al loro valore, mentre sort -V svolge la stessa funzione in una pipeline.
Classi e intervalli di caratteri
Tra parentesi quadre puoi scrivere un elenco, un intervallo o una negazione.
[ch]corrisponde a un carattere,coppureh.[a-f]corrisponde a un carattere nell'intervallo da a a f.[!0-9]corrisponde a un carattere che non è una cifra.[^0-9]ha lo stesso significato.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*Gli intervalli sono l'elemento che può comportarsi diversamente da un sistema all'altro. Un intervallo viene risolto usando l'ordine di collazione della locale corrente, non il semplice ordine ASCII. Di conseguenza, su alcuni sistemi [a-z] corrisponde anche alle lettere maiuscole. Bash dispone dell'opzione globasciiranges, che forza l'ordine ASCII semplice. Nelle build recenti questa opzione è attiva per impostazione predefinita. Verifica quindi il comportamento con shopt globasciiranges invece di darlo per scontato. Uno script che deve eseguire la corrispondenza nello stesso modo ovunque può fissare esplicitamente l'ordine impostando LC_ALL=C su una riga dedicata, vicino all'inizio del file. In questo modo ogni intervallo torna a usare l'ordine dei byte semplice.
Quella assegnazione deve essere un comando autonomo. Scritta come prefisso, LC_ALL=C ls [a-z]* imposta la locale soltanto per ls. La shell ha già espanso il pattern usando la propria locale prima che ls venga avviato. È la stessa regola della prima sezione che si ripresenta in questo caso.
L’espansione con parentesi graffe non è il globbing
Le parentesi graffe sembrano un carattere jolly, ma si comportano in modo completamente diverso. L’espansione con parentesi graffe viene eseguita molto prima del globbing e non controlla mai il filesystem.
echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}Nessuno di questi file esiste, ma l’espansione viene comunque eseguita. Questa è la differenza fondamentale: un glob interroga il filesystem, mentre una parentesi graffa genera soltanto testo. Per questo le parentesi graffe sono lo strumento corretto per creare elementi, non per cercarli.
mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csvcp app.log{,.bak} si espande in due parole, app.log e app.log.bak, perché l’elemento vuoto prima della virgola riproduce il testo originale. {01..10} mantiene gli zeri iniziali, perché uno zero iniziale in uno dei due estremi fa sì che bash utilizzi la stessa larghezza per tutti i numeri generati.
L’ordine delle espansioni spiega anche un errore frequente. Bash espande prima le parentesi graffe, poi le variabili e infine esegue l’espansione dei nomi di percorso.
n=5
echo {1..$n}L’espansione con parentesi graffe è già terminata quando $n diventa un numero, quindi non viene mai generato alcun intervallo. Usa seq 1 "$n" oppure un ciclo in stile C scritto come for ((i=1; i<=n; i++)).
Cosa succede quando un pattern non trova corrispondenze
Questo è il primo caso di errore e prima o poi capita a tutti. Per impostazione predefinita, bash lascia invariato un pattern senza corrispondenze. La parola arriva al comando esattamente come è stata digitata, quindi il comando riceve i caratteri *, ., t, m e p e prova a trattarli come un nome file.
printf '%s\n' *.tmpCon printf questo comportamento non è problematico. Non lo è invece con un comando che crea file. touch *.tmp in una directory senza file .tmp crea un file il cui nome letterale è *.tmp. Per eliminarlo in seguito è necessario usare le virgolette, così la shell non lo espande nuovamente: rm -- '*.tmp'.
Due opzioni della shell modificano questa regola. Prova ciascuna opzione con un pattern senza corrispondenze, quindi disattivala.
shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob
shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglobnullglob fa sì che un pattern senza corrispondenze venga espanso nel nulla. Di conseguenza, il comando viene eseguito con meno argomenti di quelli scritti. Questo è il comportamento desiderato quando si usa un ciclo. Con nullglob disattivato, for f in *.tmp esegue il corpo una volta, con f contenente il pattern letterale. Nella maggior parte degli script che adottano questo schema, si tratta di un bug. Con nullglob attivato, il corpo viene eseguito zero volte.
nullglob comporta un rischio specifico, perché un comando a cui sono stati rimossi tutti gli argomenti viene comunque eseguito. ls *.nope diventa un semplice ls, che elenca l'intera directory. grep -l needle *.nope diventa grep -l needle, che non ha alcun file da leggere, quindi attende l'input standard e sembra bloccato. Attiva nullglob intorno al ciclo che ne ha bisogno e disattivala subito dopo.
failglob segue la strategia opposta: un pattern senza corrispondenze viene considerato un errore, bash segnala il problema e il comando non viene eseguito. Questa è una buona impostazione per una shell interattiva, perché impedisce che un pattern digitato in modo errato venga passato a rm come nome letterale.
File nascosti: dotglob
shopt -s dotglob
printf '%s\n' *
shopt -u dotglobEsegui quel blocco e confronta l'elenco con il semplice printf '%s\n' * precedente. dotglob rimuove la regola del punto iniziale, quindi * include anche i nomi nascosti. Le voci . e .. restano sempre escluse con dotglob.
Senza dotglob, .* è il modo consueto per raggiungere i file nascosti, ma in passato era pericoloso. In bash precedente alla versione 5.2, .* includeva anche . e ... Di conseguenza, un comando ricorsivo come chmod -R 755 .* entrava direttamente nella directory padre. Bash 5.2 ha aggiunto l'opzione globskipdots, abilitata per impostazione predefinita, che esclude . e .. da ogni espansione. Esegui shopt globskipdots prima di farci affidamento, perché su un sistema meno recente questa opzione potrebbe non essere disponibile.
globstar: corrispondenze a qualsiasi profondità nell'albero
shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstarCon globstar impostato, un ** che costituisce un componente completo del percorso corrisponde a file e directory a qualsiasi profondità, anche a zero livelli, quindi **/*.log include la directory corrente, logs/ e tutto ciò che si trova sotto archive/. Un **/ con la barra finale corrisponde solo alle directory e consente di visualizzare rapidamente la struttura di un albero.
Senza globstar impostato, ** si comporta come un normale * e resta all'interno di una sola directory. Esegui lo stesso pattern con l'opzione disattivata e confronta i risultati: se un pattern ricorsivo copiato da un'altra parte restituisce molti meno risultati del previsto, questa è di solito la causa. globstar è disattivato per impostazione predefinita in ogni bash.
È importante conoscere un limite: ** non segue i link simbolici alle directory. Per un albero composto tramite link simbolici, usa invece find -L.
nocaseglob, per i nomi che differiscono solo per maiuscole e minuscole
shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglobnocaseglob fa sì che la corrispondenza dei nomi file ignori le maiuscole e le minuscole. Di conseguenza, un suffisso .LOG in maiuscolo corrisponde a un modello .log in minuscolo. Confronta questo elenco con quello della prima sezione. Disattiva subito l'opzione, perché, se resta attiva, modifica la corrispondenza per ogni comando successivo eseguito in quella shell. Questo rende difficile individuare il problema anche solo un'ora dopo.
nocaseglob si applica solo all'espansione dei nomi file. L'opzione correlata per la corrispondenza dei modelli all'interno di un'istruzione case o di un test con doppie parentesi quadre è nocasematch. Le due opzioni vengono impostate separatamente.
extglob: i pattern che possono indicare «tutto tranne questo»
I pattern estesi sono disattivati per impostazione predefinita. Attivali su una riga separata:
shopt -s extglobEsistono cinque forme, ciascuna con un elenco di pattern separati da |:
?(list)corrisponde a zero o una occorrenza.*(list)corrisponde a zero o più occorrenze.+(list)corrisponde a una o più occorrenze.@(list)corrisponde esattamente a una delle alternative.!(list)corrisponde a tutto ciò che non corrisponde ad alcuna alternativa.
Un pattern esteso deve essere interpretato da una shell in cui l'opzione è già attiva. Per questo motivo, il blocco seguente viene inserito in uno script avviato con bash -O extglob. Il flag -O imposta un'opzione di shopt prima che la nuova shell legga una sola riga del file. In questo modo, tutti i pattern vengono analizzati correttamente. L'ultimo paragrafo di questa sezione spiega perché l'ordine è importante.
cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh+([0-9]) indica una o più cifre, quindi corrisponde a un suffisso numerico di qualsiasi lunghezza. Questa è la soluzione al limite ? descritto in precedenza. Conviene eseguire entrambi i pattern consecutivamente per osservare la differenza.
La forma di negazione consente di indicare «tutto tranne». È il motivo principale per cui si abilita extglob. Due aspetti possono creare confusione.
Corrisponde sia alle directory sia ai file. Di conseguenza, ls !(*.log) inserisce nel risultato il contenuto di ogni directory corrispondente, invece del nome della directory. Usa ls -d oppure l'anteprima printf per visualizzare i nomi delle directory.
Inoltre, segue la regola dei punti come ogni altro glob. Perciò !(*.log) corrisponde a ogni nome visibile che non termina con .log. Per includere i nomi nascosti è necessario impostare anche dotglob.
Il ! in !(...) è la negazione dei glob. Non ha alcun rapporto con l'espansione della cronologia e il carattere bang, che è una funzione separata ed eseguita in una fase diversa.
Esiste un'altra insidia, che si verifica durante l'analisi sintattica. Bash analizza un comando completo prima di eseguirne qualsiasi parte. Per questo motivo, abilitare extglob e usare un pattern esteso sulla stessa riga non funziona: il pattern viene analizzato mentre l'opzione è ancora disattivata. Mantieni shopt -s extglob su una riga separata, prima dei pattern e vicino all'inizio dello script. Il corpo di una funzione viene analizzato quando la funzione viene definita. L'opzione deve quindi essere attiva prima della definizione, non prima della chiamata. Avviare la shell con bash -O extglob, come nello script precedente, stabilisce l'ordine prima che inizi qualsiasi analisi sintattica.
La shell espande il pattern, non il comando
Questo è il secondo caso di errore e spiega un'intera famiglia di bug.
grep -l needle *.log
find . -name '*.log'
find . -name *.logLa prima riga lascia che sia bash a eseguire la ricerca dei nomi e grep riceve un elenco di nomi file. La seconda racchiude il pattern tra virgolette, quindi find riceve i cinque caratteri *.log ed esegue la propria ricerca a qualsiasi profondità sotto il punto di partenza. La terza riga contiene il bug: bash espande prima il pattern, ma soltanto nella directory corrente, quindi find riceve l'istruzione di cercare un nome specifico. In una directory con una sola corrispondenza, esegue silenziosamente la ricerca dell'elemento sbagliato. Con due o più corrispondenze, find segnala un errore di sintassi, perché i nomi aggiuntivi vengono inseriti nel punto in cui si aspetta un'espressione.
La regola è semplice. Se il pattern è destinato al comando, racchiudetelo tra virgolette. Se è destinato alla shell, lasciatelo senza virgolette. La stessa distinzione vale per i pattern --exclude in tar e rsync e per i filtri --include in grep.
I glob non sono espressioni regolari, anche se usano alcuni caratteri in comune. In un glob, * indica una sequenza qualsiasi di caratteri. In un'espressione regolare, * indica zero o più occorrenze dell'elemento precedente, quindi grep '*.log' cerca qualcosa di molto diverso da ciò che sembra.
Le variabili seguono lo stesso ordine delle operazioni. L'espansione dei glob viene eseguita dopo l'espansione delle variabili, quindi una variabile senza virgolette che contiene un pattern viene espansa in base al contenuto della directory.
pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"Senza virgolette, bash esegue il glob sul risultato della variabile. Tra virgolette, il pattern resta letterale. Ne deriva una conseguenza importante: le parole prodotte dal glob non vengono suddivise nuovamente in base agli spazi, quindi for f in *.log gestisce come un unico nome un file che contiene uno spazio, mentre for f in $(ls *.log) divide quel nome in due parole. Iterare direttamente sul glob.
Visualizzare in anteprima un pattern distruttivo prima di eseguirlo
Non usare mai rm come primo comando per provare un nuovo pattern. Esegui il pattern con un comando innocuo, leggi l’elenco, quindi modifica soltanto il comando all’inizio della riga.
target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $targetScrivere una volta il pattern in target evita che le tre righe divergano. $target resta senza virgolette intenzionalmente, perché questo consente a bash di espanderlo. printf '%s\n' stampa un nome per riga e non modifica nulla. ls -ld mostra ogni voce invece di elencare il contenuto delle directory. La colonna dei permessi indica quali voci sono directory: è questo il dato da controllare prima di qualsiasi eliminazione ricorsiva. Leggere i bit dei permessi drwxr-xr-x descrive questa colonna.
Richiama la riga precedente con la freccia su e modifica soltanto il comando, in modo che il pattern resti identico. È quando si riscrive un pattern a memoria che si insinua l’errore.
Inserisci -- prima del pattern. Indica la fine delle opzioni, quindi un nome di file che inizia con - viene trattato come un nome e non come un flag.
Se vuoi disabilitare il globbing per una parte dello script, set -f lo disabilita e set +f lo riabilita.
Questa abitudine è importante perché rm non dispone di una funzione di annullamento. Recuperare i file eliminati con rm -rf è lento e in genere incompleto, quindi un’anteprima costa molto meno dell’alternativa.
Pulisci la directory di esercitazione al termine. Stampa il percorso e leggilo prima, perché rm -rf associato alla variabile sbagliata è esattamente l’incidente descritto in questa sezione.
cd ~
echo "$lab"
rm -rf -- "$lab"FAQ
Perché find . -name *.log trova i file sbagliati?
Perché bash espande *.log prima che find venga eseguito. La shell confronta il pattern con la directory corrente e passa il nome del file risultante a find, che poi cerca quel nome a ogni livello di profondità. Se corrispondono 2 o più file, find riceve argomenti aggiuntivi dove si aspetta un'espressione e restituisce un errore di sintassi. Racchiudi il pattern tra virgolette come find . -name '*.log', in modo che sia find a eseguire direttamente il confronto, usando le proprie regole e la propria ricorsione.
Cosa significa quando il pattern compare come testo letterale?
Significa che non è stata trovata alcuna corrispondenza. Per impostazione predefinita, bash lascia invariato un pattern senza corrispondenze e passa i caratteri originali al comando, che li interpreta come un nome di file. Imposta shopt -s failglob per trasformare un pattern senza corrispondenze in un errore che interrompe il comando, oppure shopt -s nullglob per farlo espandere a una stringa vuota. Con nullglob, verifica che il comando sia ancora valido senza alcun argomento, perché ls *.nope diventa un semplice ls e mostra l'intero contenuto della directory.
Come posso selezionare in bash tutti i file tranne quelli corrispondenti a un pattern?
Abilita i pattern estesi con shopt -s extglob su una riga separata, quindi usa la forma di negazione. !(*.log) corrisponde ai nomi che non terminano con .log, mentre !(*.log|*.md) esclude entrambi. I file nascosti restano esclusi dal risultato, a meno che non sia impostato anche dotglob, e vengono incluse le directory; quindi visualizza in anteprima il pattern con printf '%s\n' !(*.log) prima di passarlo a rm.
Perché * ignora i file nascosti?
Il punto iniziale deve essere cercato in modo letterale, quindi * non corrisponde mai a un nome che inizia con un punto. Usa .* per includere i nomi nascosti oppure imposta shopt -s dotglob per includerli in ogni pattern ordinario. Le voci . e .. sono sempre escluse quando è attivo dotglob; inoltre, in bash 5.2 e versioni successive, restano escluse da .* tramite l'opzione globskipdots, attiva per impostazione predefinita.
È necessario globstar perché ** funzioni?
Sì. Senza shopt -s globstar, bash tratta ** come un normale *, che cerca le corrispondenze all'interno di una sola directory e si ferma a quel livello. Con globstar attivo, un ** che costituisce un intero componente del percorso può corrispondere a qualsiasi livello di profondità; **/*.log raggiunge quindi la directory corrente e tutte le relative sottodirectory, mentre **/ usato da solo corrisponde soltanto alle directory. Tieni presente che ** non segue i collegamenti simbolici alle directory; per un albero composto da collegamenti simbolici usa find -L.