Globbing en Bash: comodines y extglob
Aprende cómo Bash expande comodines antes de ejecutar comandos y evita errores con nullglob, dotglob, globstar y la negación de extglob.
Qué hace el globbing de Bash antes de ejecutar el comando
El globbing de Bash consiste en que el shell convierte un patrón como *.log en una lista ordenada de nombres de archivo reales antes de iniciar el comando. rm *.log nunca entrega un patrón a rm. Bash lee el directorio, conserva los nombres que coinciden, los ordena y después ejecuta rm con cada nombre como un argumento independiente. El programa rm no puede saber que antes había un patrón.
Casi todo lo sorprendente del globbing se debe a ese hecho. El shell realiza la coincidencia con los archivos que existen en ese momento, y el comando sólo recibe la lista final. El nombre formal de este proceso es expansión de nombres de ruta. Globbing es el término habitual, y ambos significan lo mismo.
Cree un árbol pequeño para practicar
Todos los ejemplos siguientes usan los mismos archivos. Créelos en un directorio temporal para mantenerlos alejados de cualquier dato importante y poder eliminarlos 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 un directorio nuevo y devuelve su ruta; $(...) guarda esa ruta en la variable lab. Si esta sintaxis es nueva para usted, cómo la sustitución de comandos captura la salida de un comando lo explica en detalle.
Revise lo que creó y compruebe el shell que utiliza, porque una nota al final depende de la versión:
ls -a
bash --versionTodo esto funciona en bash 4.0 y posteriores. La nota sobre globskipdots que aparece cerca del final se aplica a bash 5.2 y posteriores, que es la versión incluida en las versiones actuales de Ubuntu y Debian a fecha de agosto de 2026.
El comando de vista previa utilizado en todo el tutorial es printf '%s\n' PATTERN. Muestra una coincidencia por línea y no modifica ningún archivo, por lo que es seguro ejecutarlo con cualquier patrón del que no esté seguro.
Los tres comodines: asterisco, signo de interrogación y corchetes
*coincide con cualquier secuencia de caracteres, incluida una secuencia vacía.?coincide exactamente con un carácter.[...]coincide exactamente con un carácter del conjunto incluido entre corchetes.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *Compare la segunda línea con la lista de archivos que creó. ? representa un carácter y nada más, por lo que un sufijo de dos dígitos no coincide con ese patrón. Amplíelo a app.log.[0-9]* y ejecútelo de nuevo para ver la diferencia.
Se aplican dos reglas a los tres comodines. Ningún comodín coincide con /, por lo que un patrón permanece dentro de un solo nivel de directorio. Además, un . inicial debe escribirse literalmente, por lo que un patrón que comienza con * omite todos los nombres ocultos. Por eso rm * no incluye sus archivos ocultos.
Los resultados de glob aparecen ordenados según el orden de cotejo de la configuración regional actual, que realiza una ordenación textual carácter por carácter. Por tanto, un sufijo de 10 se ordena antes que un sufijo de 2, porque la comparación no interpreta ninguno de los dos como un número. Ejecute printf '%s\n' app.log.* y después ls -v app.log.* y compare ambos: ls -v ordena los números incluidos según su valor, y sort -V hace lo mismo en una tubería.
Clases de caracteres y rangos
Dentro de los corchetes puede escribir una lista, un rango o una negación.
[ch]coincide con un carácter, ya seacoh.[a-f]coincide con un carácter del rango de a a f.[!0-9]coincide con un carácter que no sea un dígito.[^0-9]significa lo mismo.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*Los rangos son la parte que puede comportarse de forma distinta entre máquinas. Un rango se resuelve según el orden de colación de la configuración regional actual, no según ASCII puro. Por eso, en algunos sistemas, [a-z] también coincide con letras mayúsculas. Bash tiene una opción globasciiranges que fuerza el orden ASCII puro. Las versiones recientes suelen activarla de forma predeterminada, así que compruebe la suya con shopt globasciiranges en lugar de darlo por supuesto. Un script que deba coincidir de la misma forma en todos los sistemas puede fijar el orden estableciendo LC_ALL=C en una línea propia, cerca del principio. Esto devuelve todos los rangos al orden de bytes puro.
Esa asignación debe ser un comando independiente. Escrita como prefijo, LC_ALL=C ls [a-z]* establece la configuración regional para ls y para ninguna otra cosa. Además, el shell ya ha expandido el patrón con su propia configuración regional antes de que ls se inicie. Esta es otra aplicación de la regla de la primera sección.
La expansión de llaves no es expansión de comodines
Las llaves parecen un comodín, pero no funcionan como uno. La expansión de llaves se ejecuta mucho antes que la expansión de rutas y nunca consulta el disco.
echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}Ninguno de esos archivos existe, pero la expansión se realiza de todos modos. Esa es la diferencia: un comodín consulta el sistema de archivos, mientras que una llave sólo genera texto. Por eso las llaves son la herramienta adecuada para crear elementos, no para buscarlos.
mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csvcp app.log{,.bak} se expande en dos palabras, app.log y app.log.bak, porque el elemento vacío situado antes de la coma reproduce el texto original. {01..10} conserva los ceros iniciales porque un cero inicial en cualquiera de los extremos hace que bash rellene todos los números generados hasta alcanzar la misma longitud.
El orden de expansión también explica un error frecuente. Bash expande primero las llaves, después las variables y, por último, realiza la expansión de rutas.
n=5
echo {1..$n}La expansión de llaves ya ha terminado cuando $n se convierte en un número, por lo que nunca se genera ningún intervalo. Use seq 1 "$n" o un bucle con sintaxis de C escrito como for ((i=1; i<=n; i++)).
Qué ocurre cuando un patrón no coincide con nada
Este es el primer modo de fallo y tarde o temprano le ocurre a todo el mundo. De forma predeterminada, bash deja completamente intacto un patrón sin coincidencias. La palabra llega al comando exactamente como se escribió, por lo que el comando recibe los caracteres *, ., t, m y p e intenta tratarlos como un nombre de archivo.
printf '%s\n' *.tmpEsto no causa problemas con printf. Sí los causa con un comando que crea elementos. touch *.tmp en un directorio sin archivos .tmp crea un archivo cuyo nombre literal es *.tmp. Para eliminarlo después hay que usar comillas, de modo que el shell no vuelva a expandirlo: rm -- '*.tmp'.
Dos opciones del shell cambian esta regla. Pruebe cada una con un patrón que no coincida con nada y, después, desactívela.
shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob
shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglobnullglob hace que un patrón sin coincidencias se expanda a nada, por lo que el comando se ejecuta con menos argumentos de los escritos. Esto es lo que se necesita en un bucle. Con nullglob desactivado, for f in *.tmp ejecuta su cuerpo una vez con f, que contiene el patrón literal. Esto es un error en casi todos los scripts que lo hacen. Con nullglob activado, el cuerpo se ejecuta cero veces.
nullglob también tiene un riesgo, porque un comando al que se han eliminado todos los argumentos sigue ejecutándose. ls *.nope se convierte en un ls sin argumentos, que lista todo el directorio. grep -l needle *.nope se convierte en grep -l needle, que no recibe ningún archivo para leer, por lo que espera la entrada estándar y parece haberse bloqueado. Active nullglob alrededor del bucle que lo necesita y vuelva a desactivarlo después.
failglob adopta el enfoque contrario: un patrón sin coincidencias es un error, bash lo informa y el comando no se ejecuta. Es una buena configuración para un shell interactivo, porque impide que un patrón escrito incorrectamente se entregue a rm como un nombre literal.
Archivos ocultos: dotglob
shopt -s dotglob
printf '%s\n' *
shopt -u dotglobEjecute ese bloque y compare el listado con el printf '%s\n' * simple de antes. dotglob elimina la regla del punto inicial, por lo que * también coincide con los nombres ocultos. Las entradas . y .. siempre quedan excluidas con dotglob.
Sin dotglob, .* es la forma habitual de acceder a los archivos ocultos, pero antes era peligroso. En bash anterior a 5.2, .* también coincidía con . y .., por lo que un comando recursivo como chmod -R 755 .* entraba directamente en el directorio padre. Bash 5.2 añadió la opción globskipdots, activada de forma predeterminada, que mantiene . y .. fuera de todas las expansiones. Ejecute shopt globskipdots antes de depender de ella, porque un sistema antiguo no la tendrá.
globstar: coincidencias en todos los niveles del árbol
shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstarCon globstar establecido, un ** que forma un componente de ruta completo coincide con archivos y directorios a cualquier profundidad, incluidos cero niveles hacia abajo. Por tanto, **/*.log incluye el directorio actual, además de logs/ y todo lo que haya bajo archive/. Un **/ con la barra final coincide sólo con directorios, lo que permite ver rápidamente la estructura de un árbol.
Sin globstar establecido, ** se comporta como un * normal y permanece en un solo directorio. Ejecute el mismo patrón con la opción desactivada y compare los resultados. Si un patrón recursivo copiado de otro lugar devuelve muchos menos resultados de los esperados, esta suele ser la causa. globstar está desactivado de forma predeterminada en todas las versiones de bash.
Hay un límite importante: ** no sigue los enlaces simbólicos a directorios. Para recorrer un árbol construido mediante enlaces simbólicos, necesita find -L.
nocaseglob, para nombres que no coinciden en mayúsculas y minúsculas
shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglobnocaseglob hace que la coincidencia de nombres de archivo ignore las mayúsculas y minúsculas, por lo que un sufijo .LOG en mayúsculas coincide con un patrón .log en minúsculas. Compare este listado con el de la primera sección. Desactive la opción inmediatamente después, porque, si permanece activa, cambia la coincidencia de todas las órdenes posteriores de esa shell y resulta difícil depurar ese comportamiento una hora más tarde.
nocaseglob sólo se aplica a la expansión de nombres de archivo. La opción relacionada para la coincidencia de patrones dentro de una instrucción case o una prueba de corchetes dobles es nocasematch, y ambas se configuran por separado.
extglob: patrones que permiten decir «no esto»
Los patrones extendidos están desactivados de forma predeterminada. Actívelos en una línea independiente:
shopt -s extglobHay cinco formas. Todas reciben una lista de patrones separados por |:
?(list)coincide con cero o una aparición.*(list)coincide con cero o más apariciones.+(list)coincide con una o más apariciones.@(list)coincide exactamente con una de las alternativas.!(list)coincide con cualquier elemento que no coincida con ninguna alternativa.
Un patrón extendido debe interpretarlo un shell que ya tenga activada la opción. Por eso, el bloque siguiente se incluye en un script pequeño iniciado con bash -O extglob. La opción -O establece una opción de shopt antes de que el nuevo shell lea una sola línea del archivo. Así, todos los patrones se analizan correctamente. El último párrafo de esta sección explica por qué el orden es 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]) significa uno o más dígitos, por lo que cubre un sufijo numerado de cualquier longitud. Esta es la solución al límite de ? mencionado antes. Conviene ejecutar ambos patrones seguidos para comprobarlo.
La forma de negación responde a «todo excepto» y es la razón principal para activar extglob. Hay dos aspectos que suelen sorprender.
También coincide con directorios, no sólo con archivos. Por eso, ls !(*.log) muestra el contenido de cualquier directorio incluido en el resultado, en lugar del nombre del directorio. Use ls -d o la vista previa printf para ver los nombres directamente.
También respeta la regla de los puntos, como cualquier otro patrón glob. Por eso, !(*.log) incluye todos los nombres visibles que no terminan en .log. Para incluir nombres ocultos, también debe establecer dotglob.
El ! de !(...) es una negación de glob. No tiene relación con la expansión del historial y el carácter bang, que es una función independiente y se ejecuta en una etapa distinta.
Hay otra trampa, y esta ocurre durante el análisis sintáctico. Bash analiza un comando completo antes de ejecutar cualquiera de sus partes. Por eso, activar extglob y usar un patrón extendido en la misma línea falla: el patrón se analiza mientras la opción sigue desactivada. Mantenga shopt -s extglob en una línea independiente, antes de los patrones y cerca del principio del script. El cuerpo de una función se analiza cuando se define la función. Por tanto, la opción debe estar activada antes de la definición, no antes de la llamada. Iniciar el shell con bash -O extglob, como hace el bloque anterior, establece el orden antes de que comience cualquier análisis.
El shell expande el patrón, no el comando
Este es el segundo modo de fallo y explica toda una familia de errores.
grep -l needle *.log
find . -name '*.log'
find . -name *.logLa primera línea deja que bash realice la coincidencia y grep recibe una lista de nombres de archivo. La segunda pone el patrón entre comillas, por lo que find recibe los cinco caracteres *.log y realiza su propia coincidencia en todos los niveles inferiores al punto inicial. La tercera línea contiene el error: bash expande primero el patrón, pero sólo en el directorio actual, por lo que find recibe la instrucción de buscar un nombre concreto. En un directorio con una sola coincidencia, busca silenciosamente algo distinto de lo previsto. Con dos o más coincidencias, find muestra un error de uso, porque los nombres adicionales ocupan el lugar donde espera una expresión.
La regla es sencilla. Si el patrón está destinado al comando, escríbalo entre comillas. Si está destinado al shell, déjelo sin comillas. La misma distinción se aplica a los patrones de --exclude en tar y rsync, y a los filtros --include de grep.
Los globs tampoco son expresiones regulares, aunque comparten algunos caracteres. En un glob, * representa cualquier secuencia de caracteres. En una expresión regular, * representa cero o más repeticiones de lo que aparezca antes, por lo que grep '*.log' solicita algo muy distinto de lo que parece.
Las variables siguen el mismo orden de operaciones. La expansión de glob se ejecuta después de la expansión de variables, por lo que una variable sin comillas que contenga un patrón se expande contra el directorio.
pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"Sin comillas, bash aplica el glob al resultado de la variable. Entre comillas, el patrón permanece literal. Hay una consecuencia relacionada que debe tener en cuenta: las palabras producidas por el glob nunca se vuelven a dividir por los espacios. Por tanto, for f in *.log trata un nombre de archivo que contiene un espacio como un solo nombre, mientras que for f in $(ls *.log) divide ese nombre en dos palabras. Recorra el glob directamente.
Previsualice un patrón destructivo antes de ejecutarlo
No permita que rm sea el primer comando con el que pruebe un patrón nuevo. Ejecute el patrón con un comando inocuo, lea la lista y después cambie sólo el comando al principio de la línea.
target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $targetEscribir el patrón una vez en target evita que las tres líneas se separen, y $target se deja sin comillas de forma intencionada, porque así bash puede expandirlo. printf '%s\n' muestra un nombre por línea y no modifica nada. ls -ld muestra cada entrada en sí, en lugar de listar el contenido de los directorios, y la columna de permisos indica qué entradas son directorios. Esto es lo que debe comprobar antes de cualquier eliminación recursiva. Interpretación de los bits de permisos drwxr-xr-x explica esa columna.
Recupere la línea anterior con la flecha arriba y edite sólo el comando para que el patrón siga siendo idéntico. Volver a escribir un patrón de memoria es la forma en que aparece el error.
Coloque -- antes del patrón. Marca el final de las opciones, por lo que un nombre de archivo que comienza por - se trata como un nombre y no como una opción.
Si quiere desactivar el patrón glob durante una parte del script, set -f lo desactiva y set +f vuelve a activarlo.
Este hábito es importante porque rm no tiene deshacer. Recuperación de archivos eliminados con rm -rf es lenta y normalmente incompleta, por lo que una previsualización cuesta mucho menos que la alternativa.
Limpie el directorio de práctica cuando termine. Muestre la ruta y léala primero, porque rm -rf apuntando a la variable equivocada es exactamente el accidente que trata esta sección.
cd ~
echo "$lab"
rm -rf -- "$lab"FAQ
¿Por qué find . -name *.log encuentra los archivos equivocados?
Porque bash expande *.log antes de que find llegue a iniciarse. El shell compara el patrón con el directorio actual y pasa el nombre de archivo resultante a find, que después busca ese nombre en todos los niveles de profundidad. Si coinciden dos o más archivos, find recibe argumentos adicionales donde espera una expresión y muestra un error de uso. Escriba el patrón entre comillas como find . -name '*.log' para que find realice la comparación directamente, con sus propias reglas y su propia recursividad.
¿Qué significa que mi patrón aparezca como texto literal?
Significa que no hubo coincidencias. De forma predeterminada, bash deja sin modificar un patrón sin coincidencias y pasa los caracteres originales al comando, que después los interpreta como un nombre de archivo. Configure shopt -s failglob para convertir un patrón sin coincidencias en un error que detenga el comando, o shopt -s nullglob para que se expanda a nada. Con nullglob, compruebe que el comando siga teniendo sentido sin ningún argumento, porque ls *.nope se convierte en un ls independiente y muestra todo el directorio.
¿Cómo coincido con todos los archivos excepto con un patrón en bash?
Active los patrones extendidos con shopt -s extglob en una línea independiente y use después la forma de negación. !(*.log) coincide con los nombres que no terminan en .log, y !(*.log|*.md) excluye ambos. Los archivos ocultos no aparecen en el resultado a menos que también se establezca dotglob, y los directorios se incluyen. Por tanto, previsualice el patrón con printf '%s\n' !(*.log) antes de pasarlo a rm.
¿Por qué * omite los archivos ocultos?
Un punto inicial debe coincidir de forma literal, por lo que * nunca coincide con un nombre que empiece por uno. Use .* para incluir los nombres ocultos, o establezca shopt -s dotglob para incluirlos en todos los patrones normales. Las entradas . y .. siempre se excluyen con dotglob, y bash 5.2 y posteriores también las mantienen fuera de .* mediante la opción globskipdots, que está activada de forma predeterminada.
¿Necesito globstar para que ** funcione?
Sí. Sin shopt -s globstar, bash trata ** como un * normal, que coincide dentro de un solo directorio y se detiene allí. Con globstar activado, un ** que forme un componente de ruta completo coincide a cualquier nivel de profundidad. Por tanto, **/*.log llega al directorio actual y a todos sus subdirectorios, mientras que **/ por sí solo coincide únicamente con directorios. Tenga en cuenta que ** no sigue enlaces simbólicos a directorios. Use find -L para un árbol construido con enlaces simbólicos.