Globbing Bash : jokers, nullglob, dotglob et extglob
Comprenez comment Bash développe les jokers avant la commande et évitez les pièges de nullglob, dotglob, globstar et de la négation avec extglob.
Ce que fait le globbing de Bash avant l’exécution de votre commande
Le globbing de Bash consiste pour le shell à transformer un motif tel que *.log en une liste triée de noms de fichiers réels, avant le démarrage de la commande. rm *.log ne transmet jamais de motif à rm. Bash lit le répertoire, conserve les noms correspondants, les trie, puis exécute rm en transmettant chaque nom comme argument distinct. Le programme rm ne peut pas savoir qu’un motif a été utilisé.
Presque tout ce qui peut surprendre avec le globbing découle de ce fait. La correspondance est effectuée par le shell, à partir des fichiers présents à cet instant, et la commande ne reçoit que la liste finale. Le terme formel est pathname expansion. Globbing est le terme courant ; les deux désignent la même chose.
Créer une petite arborescence pour s’exercer
Tous les exemples ci-dessous utilisent les mêmes fichiers. Créez-les dans un répertoire temporaire afin qu’aucun fichier important ne se trouve à proximité et que le nettoyage se fasse avec une seule commande.
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 crée un répertoire vide et renvoie son chemin, tandis que $(...) enregistre ce chemin dans la variable lab. Si cette syntaxe vous est inconnue, l’explication de la capture de la sortie d’une commande par une substitution de commande la présente en détail.
Examinez ce que vous avez créé et vérifiez votre shell, car une remarque à la fin dépend de sa version :
ls -a
bash --versionTout ceci fonctionne avec bash 4.0 et les versions ultérieures. La remarque globskipdots vers la fin s’applique à bash 5.2 et aux versions ultérieures, qui sont fournies par les versions actuelles d’Ubuntu et de Debian en août 2026.
La commande de prévisualisation utilisée tout au long de ce tutoriel est printf '%s\n' PATTERN. Elle affiche une correspondance par ligne et ne modifie rien sur le disque. Vous pouvez donc l’exécuter sans risque avec un motif dont vous n’êtes pas sûr.
Les trois caractères génériques : astérisque, point d’interrogation et crochets
*correspond à toute suite de caractères, y compris une suite vide.?correspond à exactement un caractère.[...]correspond à exactement un caractère choisi dans l’ensemble indiqué entre crochets.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *Comparez la deuxième ligne avec la liste de fichiers que vous avez créée. ? représente un seul caractère, et rien de plus. Un suffixe de deux chiffres ne correspond donc pas à ce motif. Élargissez-le en app.log.[0-9]*, puis exécutez à nouveau la commande pour voir la différence.
Deux règles s’appliquent aux trois caractères génériques. Aucun caractère générique ne correspond à un /. Un motif reste donc limité à un seul niveau de répertoire. De plus, un . en tête doit être saisi littéralement. Un motif qui commence par * ignore donc tous les noms masqués. C’est pourquoi rm * laisse vos dotfiles inchangés.
Les résultats d’un glob sont triés selon l’ordre de classement de la locale courante. Il s’agit d’un tri textuel caractère par caractère. Un suffixe 10 est donc placé avant un suffixe 2, car la comparaison ne considère jamais l’un ou l’autre comme un nombre. Exécutez printf '%s\n' app.log.*, puis ls -v app.log.*, et comparez les deux résultats : ls -v trie les nombres incorporés selon leur valeur, et sort -V effectue la même opération dans un pipeline.
Classes et plages de caractères
Entre crochets, vous pouvez écrire une liste, une plage ou une négation.
[ch]correspond à un caractère, soitc, soith.[a-f]correspond à un caractère compris entre a et f.[!0-9]correspond à un caractère qui n’est pas un chiffre.[^0-9]signifie la même chose.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*Les plages sont le point qui varie selon les machines. Une plage est résolue selon l’ordre de classement de la locale active, et non selon le simple ordre ASCII. Sur certains systèmes, [a-z] correspond donc aussi aux lettres majuscules. Bash dispose d’une option globasciiranges qui impose l’ordre ASCII, et les versions récentes l’activent par défaut. Vérifiez le comportement de votre version avec shopt globasciiranges au lieu de partir du principe qu’il est identique partout. Un script qui doit produire le même résultat partout peut imposer lui-même cet ordre en définissant LC_ALL=C sur sa propre ligne, près du début du script. Toutes les plages reviennent ainsi à l’ordre des octets.
Cette affectation doit constituer une commande à part entière. Écrite comme préfixe, LC_ALL=C ls [a-z]* définit la locale pour ls uniquement. Le shell a déjà développé le pattern dans sa propre locale avant même le démarrage de ls. C’est la même règle que celle présentée dans la première section.
L’expansion des accolades n’est pas le globbing
Les accolades ressemblent à un caractère générique, mais leur comportement est complètement différent. L’expansion des accolades s’exécute bien avant le globbing et n’examine jamais le disque.
echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}Aucun de ces fichiers n’existe, mais l’expansion a quand même lieu. C’est toute la différence : un glob pose une question au système de fichiers, tandis qu’une accolade génère uniquement du texte. C’est aussi pourquoi les accolades conviennent pour créer des éléments plutôt que pour les rechercher.
mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csvcp app.log{,.bak} est développé en deux mots, app.log et app.log.bak, car l’élément vide avant la virgule reproduit le texte d’origine. {01..10} conserve ses zéros initiaux, car un zéro initial sur l’une ou l’autre des bornes indique à bash de compléter tous les nombres générés à la même largeur.
L’ordre des expansions explique aussi un problème fréquent. Bash développe d’abord les accolades, puis les variables, et effectue l’expansion des chemins en dernier.
n=5
echo {1..$n}L’expansion des accolades est déjà terminée lorsque $n devient un nombre ; aucune plage n’est donc jamais créée. Utilisez seq 1 "$n" ou une boucle de style C écrite sous la forme for ((i=1; i<=n; i++)).
Ce qui se passe lorsqu’un motif ne correspond à rien
Il s’agit du premier mode d’échec, et il finit par arriver à tout le monde. Par défaut, bash laisse complètement inchangé un motif qui ne correspond à rien. Le mot est transmis à la commande exactement comme vous l’avez saisi. La commande reçoit donc les caractères *, ., t, m et p et tente de les traiter comme un nom de fichier.
printf '%s\n' *.tmpC’est sans danger avec printf. Ce ne l’est pas avec une commande qui crée des éléments. touch *.tmp exécuté dans un répertoire qui ne contient aucun fichier .tmp crée un fichier dont le nom est littéralement *.tmp. Pour le supprimer ensuite, vous devez utiliser des guillemets afin que le shell ne le développe pas de nouveau : rm -- '*.tmp'.
Deux options du shell modifient ce comportement. Testez chacune d’elles avec un motif qui ne correspond à rien, puis désactivez-la.
shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob
shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglobnullglob fait disparaître complètement les motifs sans correspondance. La commande s’exécute donc avec moins d’arguments que ceux que vous avez écrits. C’est ce qu’il faut utiliser autour d’une boucle. Avec nullglob désactivé, for f in *.tmp exécute son corps une fois, avec f contenant le motif littéral. Dans presque tous les scripts concernés, il s’agit d’un bug. Avec nullglob activé, le corps s’exécute zéro fois.
nullglob comporte son propre risque, car une commande privée de tous ses arguments s’exécute quand même. ls *.nope devient un ls seul, qui liste tout le répertoire. grep -l needle *.nope devient grep -l needle, sans fichier à lire. La commande attend alors l’entrée standard et donne l’impression d’être bloquée. Activez nullglob autour de la boucle qui en a besoin, puis désactivez-la de nouveau après la boucle.
failglob adopte l’approche inverse : un motif sans correspondance est considéré comme une erreur. bash l’indique et la commande ne s’exécute pas. C’est un bon réglage pour un shell interactif, car il empêche de transmettre à rm un motif mal saisi comme nom littéral.
Fichiers masqués : dotglob
shopt -s dotglob
printf '%s\n' *
shopt -u dotglobExécutez ce bloc et comparez la liste avec le printf '%s\n' * simple présenté plus haut. dotglob supprime la règle du point initial, donc * correspond également aux noms masqués. Les entrées . et .. restent toujours exclues avec dotglob.
Sans dotglob, .* est la méthode habituelle pour accéder aux fichiers masqués, mais elle était autrefois dangereuse. Avec bash antérieur à 5.2, .* correspondait également à . et ... Une commande récursive telle que chmod -R 755 .* remontait donc directement dans le répertoire parent. Bash 5.2 a ajouté l’option globskipdots, activée par défaut, qui exclut . et .. de toutes les expansions. Exécutez shopt globskipdots avant de vous appuyer dessus, car une machine plus ancienne ne la prendra pas en charge.
globstar : parcourir toute l’arborescence
shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstarLorsque globstar est défini, un ** qui constitue un composant de chemin complet correspond aux fichiers et aux répertoires à n’importe quel niveau, y compris zéro niveau de profondeur. Ainsi, **/*.log couvre le répertoire courant, ainsi que logs/ et tout ce qui se trouve sous archive/. Un **/ suivi d’une barre oblique finale ne correspond qu’aux répertoires. C’est donc un moyen rapide de visualiser la structure d’une arborescence.
Lorsque globstar n’est pas défini, ** se comporte comme un * ordinaire et reste dans un seul répertoire. Exécutez le même motif avec cette option désactivée, puis comparez les résultats. Lorsqu’un motif récursif copié ailleurs renvoie beaucoup moins de résultats que prévu, c’est généralement la cause. globstar est désactivé par défaut dans toutes les versions de bash.
Une limite est à connaître : ** ne suit pas les liens symboliques vers des répertoires. Pour parcourir une arborescence reliée par des liens symboliques, utilisez plutôt find -L.
nocaseglob, pour les noms dont la casse diffère
shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglobnocaseglob ignore la casse lors de la correspondance des noms de fichiers. Ainsi, un suffixe .LOG en majuscules correspond à un motif .log en minuscules. Comparez cette liste à celle de la première section. Désactivez immédiatement l’option, car si elle reste active, elle modifie la correspondance pour toutes les commandes suivantes dans ce shell. Cela complique le diagnostic une heure plus tard.
nocaseglob s’applique uniquement à l’expansion des noms de fichiers. L’option correspondante pour la correspondance des motifs dans une instruction case ou un test entre doubles crochets est nocasematch. Les deux options se configurent séparément.
extglob : les patterns qui permettent d’exclure
Les patterns étendus sont désactivés par défaut. Activez-les sur une ligne dédiée :
shopt -s extglobIl existe cinq formes. Chacune prend une liste de patterns séparés par | :
?(list)correspond à zéro ou une occurrence.*(list)correspond à zéro ou plusieurs occurrences.+(list)correspond à une ou plusieurs occurrences.@(list)correspond exactement à l’une des alternatives.!(list)correspond à tout ce qui ne correspond à aucune alternative.
Un pattern étendu doit être interprété par un shell dans lequel l’option est déjà activée. Le script ci-dessous est donc placé dans un petit script lancé avec bash -O extglob. L’option -O définit une option shopt avant que le nouveau shell ne lise une seule ligne du fichier. Tous les patterns sont ainsi correctement analysés. Le dernier paragraphe de cette section explique pourquoi l’ordre est important.
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]) signifie « un ou plusieurs chiffres ». Il couvre donc un suffixe numéroté de longueur quelconque. C’est la correction de la limite ? présentée plus haut. Exécutez les deux patterns l’un après l’autre pour le constater.
La forme de négation répond au besoin « tout sauf ». C’est la raison principale d’activer extglob. Deux points surprennent souvent les utilisateurs.
Cette forme correspond aussi aux répertoires. ls !(*.log) affiche donc le contenu de chaque répertoire trouvé au lieu du nom du répertoire. Utilisez ls -d ou l’aperçu printf pour afficher les noms eux-mêmes.
Elle suit également la règle concernant les points, comme tous les autres globs. !(*.log) couvre donc tous les noms visibles qui ne se terminent pas par .log. Les noms masqués nécessitent aussi l’activation de dotglob.
Le ! dans !(...) correspond à la négation d’un glob. Il n’a aucun rapport avec l’expansion de l’historique et le caractère bang, qui est une fonctionnalité distincte exécutée à une autre étape.
Il reste un piège, qui intervient au moment de l’analyse syntaxique. Bash analyse une commande complète avant d’en exécuter la moindre partie. Activer extglob et utiliser un pattern étendu sur la même ligne échoue donc : le pattern est analysé alors que l’option est encore désactivée. Placez shopt -s extglob sur sa propre ligne, avant les patterns, et près du début du script. Le corps d’une fonction est analysé lors de la définition de la fonction. L’option doit donc être activée avant la définition, et non avant l’appel. Démarrer le shell avec bash -O extglob, comme dans le script ci-dessus, règle l’ordre avant toute analyse syntaxique.
Le shell développe le motif, pas la commande
C’est le deuxième mode d’échec. Il explique toute une famille de bugs.
grep -l needle *.log
find . -name '*.log'
find . -name *.logLa première ligne laisse bash effectuer la correspondance, puis grep reçoit une liste de noms de fichiers. La deuxième protège le motif entre guillemets. find reçoit donc les cinq caractères *.log et effectue lui-même la correspondance à tous les niveaux sous le point de départ. La troisième ligne est le bug : bash développe d’abord le motif, uniquement par rapport au répertoire courant. find reçoit donc l’instruction de rechercher un nom précis. Dans un répertoire contenant une seule correspondance, la commande recherche silencieusement le mauvais élément. Avec au moins deux correspondances, find signale une erreur de syntaxe, car les noms supplémentaires arrivent à l’endroit où il attend une expression.
La règle est simple. Si le motif est destiné à la commande, protégez-le entre guillemets. S’il est destiné au shell, laissez-le sans guillemets. La même distinction s’applique aux motifs --exclude dans tar et rsync, ainsi qu’aux filtres --include dans grep.
Les globs ne sont pas non plus des expressions régulières, même s’ils utilisent certains caractères communs. Dans un glob, * représente une suite quelconque de caractères. Dans une expression régulière, * représente zéro occurrence ou davantage de l’élément qui le précède. Ainsi, grep '*.log' recherche quelque chose de très différent de ce que sa forme semble indiquer.
Les variables suivent le même ordre des opérations. Le globbing intervient après le développement des variables. Une variable non protégée entre guillemets qui contient un motif est donc développée par rapport au répertoire.
pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"Sans guillemets, bash applique le globbing au résultat de la variable. Entre guillemets, le motif reste littéral. Une conséquence importante mérite d’être retenue : les mots produits par le globbing ne sont jamais découpés de nouveau sur les espaces. for f in *.log traite donc un nom de fichier contenant une espace comme un seul nom, tandis que for f in $(ls *.log) le découpe en deux mots. Parcourez directement le glob.
Prévisualiser un motif destructeur avant de l’exécuter
Ne faites jamais de rm la première commande avec laquelle vous testez un nouveau motif. Exécutez le motif avec une commande sans risque, lisez la liste, puis modifiez uniquement la commande au début de la ligne.
target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $targetÉcrire le motif une seule fois dans target évite que les trois lignes divergent. $target reste volontairement entre guillemets, car c’est ce qui permet à bash de le développer. printf '%s\n' affiche un nom par ligne et ne modifie rien. ls -ld affiche chaque entrée elle-même au lieu de lister le contenu des répertoires. La colonne du mode indique quelles entrées sont des répertoires, ce qu’il faut vérifier avant toute suppression récursive. Lire les bits d’autorisation drwxr-xr-x explique cette colonne.
Rappelez la ligne précédente avec la flèche vers le haut et modifiez uniquement la commande afin de conserver le même motif. Retaper un motif de mémoire est précisément ce qui favorise les erreurs.
Placez -- avant le motif. Cette option marque la fin des options. Un nom de fichier qui commence par - est alors traité comme un nom et non comme une option.
Si vous voulez désactiver le globbing pendant une partie d’un script, set -f le désactive et set +f le réactive.
Cette habitude est importante, car rm ne permet aucune annulation. Récupérer des fichiers supprimés avec rm -rf est une opération longue et généralement incomplète. Une prévisualisation coûte donc bien moins cher que les conséquences d’une erreur.
Nettoyez le répertoire de test lorsque vous avez terminé. Affichez le chemin et lisez-le d’abord, car rm -rf utilisé avec la mauvaise variable provoquerait exactement l’accident décrit dans cette section.
cd ~
echo "$lab"
rm -rf -- "$lab"FAQ
Pourquoi find . -name *.log trouve-t-il les mauvais fichiers ?
Parce que bash développe *.log avant même le lancement de find. Le shell compare le motif au contenu du répertoire courant, puis transmet le nom de fichier obtenu à find, qui recherche ensuite ce nom à tous les niveaux. Si plusieurs fichiers correspondent, find reçoit des arguments supplémentaires à l’emplacement où il attend une expression et affiche une erreur d’utilisation. Placez le motif entre guillemets, comme dans find . -name '*.log', afin que find effectue lui-même la correspondance, avec ses propres règles et sa propre récursion.
Que signifie l’affichage de mon motif comme texte littéral ?
Cela signifie qu’aucune correspondance n’a été trouvée. Par défaut, bash conserve les motifs sans correspondance et transmet les caractères bruts à la commande, qui les traite comme un nom de fichier. Définissez shopt -s failglob pour transformer un motif sans correspondance en erreur et arrêter la commande, ou shopt -s nullglob pour le remplacer par une chaîne vide. Avec nullglob, vérifiez que la commande reste cohérente sans aucun argument, car ls *.nope devient un ls seul et liste alors tout le contenu du répertoire.
Comment sélectionner tous les fichiers sauf ceux correspondant à un motif dans bash ?
Activez les motifs étendus avec shopt -s extglob sur une ligne séparée, puis utilisez la forme de négation. !(*.log) sélectionne les noms qui ne se terminent pas par .log, et !(*.log|*.md) exclut les deux. Les fichiers cachés restent exclus du résultat, sauf si dotglob est également défini. Les répertoires sont inclus : prévisualisez donc le motif avec printf '%s\n' !(*.log) avant de le transmettre à rm.
Pourquoi * ignore-t-il les fichiers cachés ?
Un point initial doit être associé explicitement. * ne correspond donc jamais à un nom qui commence par un point. Utilisez .* pour inclure les noms cachés, ou définissez shopt -s dotglob pour les inclure dans tous les motifs ordinaires. Les entrées . et .. restent toujours exclues avec dotglob. Bash 5.2 et les versions ultérieures les excluent également de .* via l’option globskipdots, activée par défaut.
globstar est-il nécessaire pour que ** fonctionne ?
Oui. Sans shopt -s globstar, bash traite ** comme un * ordinaire, qui recherche dans un seul répertoire et s’arrête à ce niveau. Lorsque globstar est activé, un ** qui constitue un composant de chemin complet peut correspondre à n’importe quelle profondeur. **/*.log atteint donc le répertoire courant et tous ses sous-répertoires, tandis que **/ utilisé seul sélectionne uniquement les répertoires. Notez que ** ne suit pas les liens symboliques vers des répertoires. Utilisez donc find -L pour parcourir une arborescence composée de liens symboliques.