La expansión de llaves (brace expansion) y el globbing se parecen superficialmente pero operan en momentos distintos y con lógicas completamente diferentes. El globbing busca archivos que ya existen en el sistema de ficheros y sustituye el patrón por los nombres que coinciden. La expansión de llaves, en cambio, genera cadenas de texto puras antes de que la shell mire al disco: file{1,2,3}.txt produce tres tokens (file1.txt, file2.txt, file3.txt) aunque ninguno de esos archivos exista todavía. Esto la hace ideal para crear estructuras, no para buscarlas.
El orden de expansión en bash importa: primero se procesan las llaves, luego los tildes, luego los parámetros, y solo al final el globbing. Cuando escribes mkdir proyecto/{src,tests,docs}, la shell ya ha generado las tres rutas completas antes de invocar mkdir, que las recibe como tres argumentos separados. No hay magia: es simple sustitución léxica.
** como patrón recursivo solo está disponible si activas shopt -s globstar. Sin esa opción, ** se comporta igual que * en bash, lo que produce resultados silenciosamente incorrectos, no un error. Actívala al principio del script o en tu .bashrc si la usas de forma interactiva.
La trampa clásica del espacio merece atención especial. for f in $(ls) usa sustitución de comandos, y el resultado de ls es una cadena de texto que la shell divide usando $IFS (por defecto: espacio, tabulador, salto de línea). Un archivo llamado informe final.pdf se convierte en dos tokens: informe y final.pdf. El globbing evita este problema porque nunca pasa por $IFS: la shell sustituye el patrón directamente por una lista de argumentos, preservando los espacios en los nombres. Si ignoras esto en un script de producción que maneja uploads de usuarios, tarde o temprano procesarás la mitad de un nombre de archivo.
shopt -s nullglob resuelve otro caso problemático: cuando un patrón no coincide con nada, bash por defecto lo deja literalmente en la línea de comandos. Un bucle for f in /ruta/vacía/*.log sin nullglob itera una vez con f igual a la cadena literal /ruta/vacía/*.log, lo que rompe cualquier lógica que asuma que $f es un archivo real.
#!/usr/bin/env bash
# Requiere bash 4+ (Debian Bookworm trae bash 5.2)
shopt -s globstar nullglob
# ── Expansión de llaves: genera cadenas, no busca archivos ──────────────
# Crea estructura de directorios en un solo comando
mkdir -p proyecto/{src,tests,docs}/{main,utils}
# Rangos: genera archivo de plantilla para cada mes del año
touch informes/informe_{01..12}_2024.txt
# La coma y el rango se pueden combinar
echo proyecto/{src,tests}/main_{a..c}.py
# → proyecto/src/main_a.py proyecto/src/main_b.py ... (solo imprime, no crea)
# ── globstar: recursivo de verdad ───────────────────────────────────────
# Encuentra todos los .py bajo el directorio actual, a cualquier profundidad
for fichero in proyecto/**/*.py; do
echo "Encontrado: $fichero"
done
# ── nullglob en acción ──────────────────────────────────────────────────
# Sin nullglob, si /tmp/logs/ está vacío, $f sería la cadena literal "*.log"
for f in /tmp/logs/*.log; do
# Con nullglob activo, este bucle simplemente no se ejecuta si no hay .log
gzip "$f"
done
# ── La trampa del espacio: versión rota vs. versión correcta ────────────
# MAL: $(ls) parte por espacios; "informe final.pdf" se convierte en dos tokens
for f in $(ls /tmp/uploads/); do
echo "Procesando: $f" # puede fallar o procesar nombres incompletos
done
# BIEN: globbing preserva nombres con espacios como un solo argumento
for f in /tmp/uploads/*; do
echo "Procesando: $f" # "informe final.pdf" llega íntegro
done
# BIEN para árboles más complejos o cuando necesitas filtrar por tipo:
# -print0 y read -d '' manejan espacios, saltos de línea y caracteres raros
while IFS= read -r -d '' f; do
echo "Procesando: $f"
done < <(find /tmp/uploads/ -maxdepth 1 -type f -print0)
El shopt -s globstar nullglob al principio del script activa ambas opciones para todo lo que sigue; ponerlas juntas en la misma línea es perfectamente válido. Si el script se ejecuta con sh en lugar de bash, shopt no existe y el script falla de inmediato, razón por la que el shebang especifica bash explícitamente.
El mkdir -p proyecto/{src,tests,docs}/{main,utils} demuestra que la expansión de llaves es combinatoria: dos grupos de llaves generan el producto cartesiano, en este caso seis rutas. La shell las expande antes de llamar a mkdir, que recibe seis argumentos independientes.
El bucle con **/*.py funciona porque globstar hace que ** cruce límites de directorio. Sin nullglob, si no hay ningún .py en el árbol, fichero tomaría el valor proyecto/**/*.py literalmente y el cuerpo del bucle se ejecutaría una vez con una ruta inexistente.
El bloque de find con -print0 y read -d '' es la opción más robusta cuando los nombres pueden contener cualquier cosa: -print0 separa resultados con el byte nulo en lugar de un salto de línea, e IFS= evita que read elimine espacios iniciales y finales. Es más verboso que el globbing, pero necesario cuando trabajas con rutas arbitrarias de usuarios o con árboles de directorios muy grandes donde el globbing podría superar el límite de argumentos del sistema (getconf ARG_MAX en Debian Bookworm devuelve típicamente 2 097 152 bytes).
N° 24