Cuando escribes rm *.log, hay una trampa conceptual que atrapa a mucha gente: rm nunca ve el asterisco. Lo que recibe son los nombres de archivo ya resueltos, separados como argumentos individuales. Eso es el globbing: el proceso por el cual la shell interpreta los patrones de comodines y los sustituye por los nombres de archivo reales antes de ejecutar cualquier cosa.
El mecanismo es sencillo pero sus consecuencias no lo son. Cuando pulsas Enter, la shell (bash, en nuestro caso) recorre el comando antes de ejecutarlo, detecta los caracteres especiales *, ? y [...], los compara contra el sistema de archivos real y construye la lista de coincidencias. Esa lista reemplaza al patrón en la línea de comandos. Solo entonces arranca el proceso hijo. El programa que invocas —rm, cp, ls, cualquiera— recibe en su argv los nombres ya expandidos, igual que si los hubieras escrito tú a mano.
Esto explica por qué echo * muestra los archivos del directorio actual: echo es uno de los comandos más tontos que existen, simplemente imprime sus argumentos. Si recibieras el argumento * como texto literal, imprimiría un asterisco. Pero como la shell ya hizo la expansión, echo recibe la lista completa de nombres y los imprime todos. El protagonista no es echo, es la shell.
Los tres patrones básicos tienen semánticas precisas:
*casa con cualquier secuencia de caracteres, incluyendo la cadena vacía, pero nunca cruza una barra/. Por eso*.logno encuentralogs/app.log.?casa con exactamente un carácter, cualquiera.archivo?.txtencuentraarchivo1.txtpero noarchivo10.txt.[abc]casa con uno de los caracteres dentro de los corchetes.[a-z]es un rango;[0-9]otro.[!a-z]niega el rango.
Hay un caso especial que sorprende: ¿qué pasa si el patrón no encuentra ningún archivo? En bash, por defecto, el patrón se pasa literalmente al comando sin modificar. Si no existe ningún .log en el directorio y ejecutas rm *.log, rm recibe la cadena literal *.log e intenta borrar un archivo que se llama exactamente así, fallando con “No such file or directory”. No es un error de la shell; es que la shell cumplió su parte y pasó lo que tenía. Este comportamiento se puede cambiar con la opción nullglob de bash, que hace que un patrón sin coincidencias desaparezca por completo de la línea de comandos, sin pasar nada.
#!/usr/bin/env bash # Preparamos un directorio de trabajo temporal para ver todo en acción mkdir -p /tmp/demo_glob && cd /tmp/demo_glob # Creamos archivos de prueba con nombres variados touch app.log error.log debug.log config.conf datos1.csv datos2.csv archivo_a.txt # ── 1. echo * — la shell expande, echo solo imprime lo que recibe ────────── echo * # La shell sustituye * por todos los archivos del directorio actual. # echo nunca sabe que hubo un asterisco. # ── 2. * no cruza directorios ───────────────────────────────────────────── mkdir -p subdirectorio touch subdirectorio/oculto.log echo *.log # Solo muestra app.log error.log debug.log — NO subdirectorio/oculto.log # ── 3. ? — exactamente un carácter ─────────────────────────────────────── echo datos?.csv # Coincide: datos1.csv datos2.csv # No coincide: datos10.csv (son dos caracteres después de "datos") # ── 4. [abc] y rangos ───────────────────────────────────────────────────── echo datos[12].csv # Equivalente al anterior pero más explícito: solo 1 o 2, no 3 echo archivo_[a-z].txt # Coincide: archivo_a.txt # Con [A-Z] no coincidiría porque la 'a' es minúscula # ── 5. El comportamiento por defecto cuando no hay coincidencias ────────── echo *.xml # No existe ningún .xml → bash pasa el literal "*.xml" a echo # Salida: *.xml (el asterisco aparece tal cual) # ── 6. nullglob: que el patrón desaparezca si no hay coincidencias ──────── shopt -s nullglob echo "Con nullglob activo:" echo *.xml # Ahora no imprime nada — el patrón se eliminó antes de llamar a echo shopt -u nullglob # Desactivamos para no afectar al resto de la sesión # ── 7. La diferencia práctica con rm ───────────────────────────────────── # Sin nullglob y sin .xml: rm *.xml intentaría borrar un archivo llamado "*.xml" # Con nullglob activo: rm *.xml no haría nada (rm recibiría cero argumentos) # Con archivos reales: rm *.log borraría app.log error.log debug.log # Limpieza cd /tmp && rm -rf /tmp/demo_glob
El detalle más importante del paso 1 es lo que NO aparece en la salida: el asterisco. Eso confirma que bash hizo la expansión antes de ejecutar echo. Si quieres pasarle un asterisco literal a un programa, tienes que escaparlo: echo \* o echo '*'.
En el paso 2, fíjate en que *.log no devuelve subdirectorio/oculto.log aunque existe en el disco. El * es codicioso con los nombres pero respetuoso con los separadores de directorio. Para bajar un nivel necesitarías subdirectorio/*.log o **/*.log con globstar activado —eso ya es otro tema.
Los pasos 5 y 6 son los más críticos para entender errores en producción. Cuando un script falla con “No such file or directory” sobre un patrón que esperabas que no coincidiera con nada, casi siempre es esto: la shell pasó el literal porque no había archivos. shopt -s nullglob al principio de un script que maneje archivos opcionales es una decisión defensiva muy razonable. La contrapartida es que con nullglob activo un rm *.log sin archivos no da ningún error —lo que puede ocultar problemas si necesitabas que esos archivos existieran.
Los rangos como [a-z] tienen una complicación silenciosa: su comportamiento depende del locale del sistema. Si LC_COLLATE no es C o POSIX, el rango [a-z] puede incluir letras mayúsculas según el orden de cotejo del idioma configurado. En un sistema Debian estándar en producción, lo más predecible es usar LC_ALL=C cuando necesites rangos de caracteres ASCII exactos dentro de scripts.
N° 23