Módulo 2: Trabajar con archivos y texto
5. Encontrar archivos con find
Descripción
Al terminar esta lección vas a poder construir cualquier consulta de find: decirle dónde buscar, bajo qué condiciones —nombre, tipo, tamaño, fecha de modificación, profundidad— y qué hacer con lo que encuentre, incluyendo borrar en lote sin destruir lo que no debías.
Esto no es un ejercicio de sintaxis. Te conectas por SSH a un servidor de producción después de un despliegue que salió mal y necesitas saber qué archivo de configuración se modificó en las últimas horas. O el disco se está llenando y tienes que encontrar, entre miles de archivos, cuáles pesan más de un gigabyte. En ese servidor no hay explorador de archivos con clic derecho y "buscar". Hay una terminal y find, y punto.
Conexión con el módulo: la lección anterior te enseñó a leer un archivo una vez que ya sabes cuál es. Esta cierra el hueco de antes: encontrar cuál es, antes de poder leerlo. La siguiente lección busca dentro del contenido de un archivo con grep; esta busca por su metadata —nombre, tipo, tamaño, fecha— sin abrir nada.
find como una frase: dónde, qué condición, qué hacer
Piensa en cómo le darías instrucciones a alguien que no conoce tu casa: "en la cocina, busca los frascos vacíos, y tíralos a la basura". Esa instrucción tiene tres partes: un lugar, una condición que describe qué cuenta como "encontrado", y una acción sobre lo que cumple la condición. find se lee exactamente igual:
find <dónde> <condiciones> <acción>
Técnicamente, find recorre de forma recursiva el árbol de directorios a partir del punto de partida que le des, y evalúa sobre cada entrada (archivo, directorio, enlace) una expresión formada por una o más condiciones. Si no le pides ninguna acción, la acción por defecto es imprimir la ruta completa. Todo lo que vas a aprender en esta lección son formas distintas de llenar esas tres partes.
Ejemplo trabajado
Vas a construir un árbol de proyecto de juguete y correr consultas reales contra él. Usa mkdir y touch (los conoces de la lección 2, incluida la expansión de llaves):
mkdir -p project/{src,tests,config,logs,node_modules/lodash}
touch project/config/{app.yaml,database.yaml,secrets.env}
touch project/src/{index.js,utils.js,index.js.bak}
touch project/tests/index.test.js
touch project/node_modules/lodash/index.js
head -c 2097152 /dev/urandom > project/logs/access.log
La última línea no es find: es head -c 2097152 leyendo 2,097,152 bytes (2 mebibytes) de datos aleatorios para simular un archivo de log con peso real (los que crea touch pesan 0 bytes). El número va sin sufijo de unidad a propósito —el soporte de sufijos como 2M en head -c varía entre sistemas, mientras que un conteo de bytes plano funciona igual en cualquiera. El árbol que acabas de crear queda así:
project/
├── config/
│ ├── app.yaml
│ ├── database.yaml
│ └── secrets.env
├── logs/
│ └── access.log (2 MiB)
├── node_modules/
│ └── lodash/
│ └── index.js
├── src/
│ ├── index.js
│ ├── index.js.bak
│ └── utils.js
└── tests/
└── index.test.js
Filtrar por nombre con -name y -iname. -name compara el patrón contra el nombre base de cada entrada —sin la ruta de directorios— usando comodines de shell (*, ?), y por eso el patrón siempre va entre comillas: si no lo proteges, es tu propio shell el que expande el comodín antes de que find lo reciba, no find.
find project -name "*.yaml"
Qué esperar:
project/config/app.yaml
project/config/database.yaml
(El orden exacto de las líneas puede variar según tu sistema de archivos; lo que importa es qué rutas aparecen, no en qué orden.)
-iname es igual, pero ignora mayúsculas y minúsculas en todo el patrón. Útil cuando heredas un árbol con convenciones inconsistentes:
find project -iname "*.ENV"
Qué esperar:
project/config/secrets.env
Filtrar por tipo con -type. -type f selecciona solo archivos regulares, -type d solo directorios, -type l solo enlaces simbólicos. Combinado con -maxdepth, que limita cuántos niveles de directorios baja find a partir del punto de partida:
find project -maxdepth 1 -type d
Qué esperar:
project
project/config
project/logs
project/node_modules
project/src
project/tests
(De nuevo: el orden real depende de tu sistema de archivos, no de find. Lo que debe coincidir son las seis rutas, no el orden en que aparecen.)
-maxdepth 1 incluye el propio punto de partida (nivel 0) más un nivel hacia abajo. Sin ese límite, find seguiría bajando por node_modules/lodash y por cualquier subcarpeta que tenga tu proyecto real, generando cientos de líneas de ruido que no pediste.
Filtrar por tamaño con -size. La unidad va pegada al número: c para bytes, k para kibibytes, M para mebibytes, G para gibibytes. El prefijo + significa "mayor que", - significa "menor que"; sin prefijo es el tamaño exacto.
find project -type f -size +1M
Qué esperar:
project/logs/access.log
Solo el log de 2 MiB cumple "mayor a 1 mebibyte". El resto de los archivos, creados con touch, pesan 0 bytes.
Filtrar por fecha: -mtime y -mmin
-mtime mide en días desde la última modificación, -mmin mide en minutos —la misma idea, distinta escala. +n significa "hace más de n unidades", -n significa "hace menos de n unidades". Un detalle que sorprende la primera vez: find redondea a periodos completos de 24 horas para -mtime (a minutos completos para -mmin), así que -mtime +1 en realidad exige al menos dos días completos, no "más de un día" en sentido literal.
Como acabas de crear el árbol de la sección anterior, todo en él tiene menos de una hora de modificado. Puedes comprobarlo:
find project -type f -mmin -60
Qué esperar:
project/config/app.yaml
project/config/database.yaml
project/config/secrets.env
project/logs/access.log
project/node_modules/lodash/index.js
project/src/index.js
project/src/index.js.bak
project/src/utils.js
project/tests/index.test.js
Los nueve archivos aparecen porque los nueve se modificaron hace menos de 60 minutos. En un escenario real, la escala natural para "qué tocó el último despliegue" suele ser días, no minutos:
find /var/www/app -type f -mtime -1 # modificado en las últimas 24 horas
find /var/log -type f -mtime +30 # sin tocar hace más de un mes: candidato a archivar
Combinar condiciones: -and, -or y !
Cuando escribes dos condiciones seguidas sin nada entre ellas, find las une con un -and implícito: la entrada tiene que cumplir ambas. Ya lo hiciste arriba con -type f -size +1M.
Para "o" necesitas -o (o -or), y casi siempre agrupar con paréntesis escapados —escapados porque el shell interpreta ( y ) como sintaxis de subshell si no los proteges:
find project -type f \( -name "*.log" -o -name "*.bak" \)
Qué esperar:
project/logs/access.log
project/src/index.js.bak
Los paréntesis no son decoración: -and liga más fuerte que -or, así que sin ellos find project -type f -name "*.log" -o -name "*.bak" se interpretaría como (-type f -a -name "*.log") -o (-name "*.bak") —la segunda rama no tiene -type f, y encontraría también un directorio que por casualidad se llamara *.bak.
Para negar, ! (o -not, el equivalente que no es parte del estándar POSIX pero que casi todo find moderno acepta):
find project -type f ! -name "*.log"
Qué esperar:
project/config/app.yaml
project/config/database.yaml
project/config/secrets.env
project/node_modules/lodash/index.js
project/src/index.js
project/src/index.js.bak
project/src/utils.js
project/tests/index.test.js
Los ocho archivos de project, excepto el log.
Actuar sobre los resultados: -exec y -delete
Hasta aquí, cada comando solo imprimió rutas. -exec te deja ejecutar cualquier programa sobre cada resultado. {} es el marcador que find reemplaza por la ruta de cada entrada encontrada, y \; cierra la instrucción —escapado porque ; también es sintaxis del shell:
find project -type f -name "*.js" -exec wc -l {} \;
Qué esperar:
0 project/src/index.js
0 project/src/utils.js
0 project/node_modules/lodash/index.js
Cero líneas en los tres, porque touch los creó vacíos. Existe una variante, -exec comando {} +, que en vez de lanzar un proceso nuevo por cada archivo agrupa varios nombres en una sola invocación —igual que hace xargs, la herramienta que vas a conocer en el módulo 3 cuando conectes comandos entre sí.
El criterio obligatorio antes de borrar: primero listas, verificas la lista, y solo entonces actúas. -delete no es un comando aparte que se agrega después de una búsqueda ya confirmada: es otra condición dentro de la misma expresión, y find evalúa esa expresión de izquierda a derecha. Eso significa que el orden en el que escribes las condiciones no es cosmético.
Paso 1, listas con exactamente las mismas condiciones que vas a usar para borrar:
find project -type f -name "*.bak"
Qué esperar:
project/src/index.js.bak
Paso 2, verificas visualmente que esa lista —y nada más que esa lista— es lo que quieres eliminar.
Paso 3, solo entonces agregas -delete al final, sin tocar ninguna otra parte de la expresión:
find project -type f -name "*.bak" -delete
No hay salida: sin salida es éxito. Confirmas volviendo a correr el comando de solo-listar del paso 1 y comprobando que ya no devuelve nada.
-delete además evita el costo de lanzar un proceso rm por cada archivo, que es lo que harías con -exec rm {} \;. Pero la ganancia de velocidad es secundaria: lo que importa es que sigue siendo parte de la misma expresión evaluada en orden, y de eso trata el primer error común de abajo.
Alternativas modernas: fd
fd es una reescritura en Rust pensada para el uso diario: sintaxis más corta (fd patron en vez de find -iname '*patron*'), coloreada, insensible a mayúsculas por defecto, y respeta .gitignore sin que se lo pidas. Para buscar en tu propio proyecto, después de instalarla, es más agradable que find.
Esta guía enseña find primero por una razón que no tiene que ver con preferencia: find viene preinstalado en cualquier máquina Unix a la que te vayas a conectar —un servidor de producción, un contenedor recién levantado, la VM de un compañero. fd casi nunca está ahí, y no siempre puedes instalar un paquete nuevo en un sistema ajeno. Aprende find para sobrevivir en cualquier terminal; aprende fd después para tu propia máquina, si quieres comodidad.
Errores comunes
1. Creer que -delete filtra primero y borra después (conceptual). find no separa "encontrar" de "actuar" en dos fases: evalúa la expresión completa, condición por condición, de izquierda a derecha, para cada entrada. Si escribes -delete antes que el filtro —find project -delete -name "*.bak"—, no hay ninguna condición previa que lo detenga: -delete se ejecuta para cada entrada desde el primer momento, y borra el árbol entero. -name "*.bak" nunca llega a filtrar nada, porque la entrada ya fue borrada antes de llegar a esa parte de la expresión. Cómo detectarlo: la lista que verificaste en el paso de solo-listar y el comando final con -delete deben tener exactamente las mismas condiciones en el mismo orden relativo. Cómo corregirlo: -delete siempre al final de la expresión, nunca al principio.
2. Olvidar las comillas en el patrón de -name. Si escribes find project -name *.yaml sin comillas y hay algún archivo .yaml en tu directorio actual, es el shell —no find— quien expande *.yaml antes de que el comando lo reciba. find termina buscando un nombre de archivo literal, exacto, en vez de un patrón. Cómo detectarlo: el mismo comando da resultados distintos según desde qué carpeta lo corras, o falla con "No such file or directory" citando un nombre de archivo real. Cómo corregirlo: comillas simples siempre, -name "*.yaml".
3. Combinar -o sin agrupar con paréntesis, asumiendo que un filtro anterior aplica a ambas ramas (conceptual). -and (el que se implica al poner dos condiciones seguidas) liga más fuerte que -or. Si escribes find project -type f -name "*.log" -o -name "*.bak" sin paréntesis, no obtienes "archivos que sean .log o .bak": obtienes "(archivo Y termina en .log) O (termina en .bak, sea archivo, directorio o lo que sea)". Cómo detectarlo: el resultado incluye entradas que no son archivos regulares aunque pusiste -type f. Cómo corregirlo: agrupa la parte que quieres compartida entre ambas ramas con paréntesis escapados: -type f \( -name "*.log" -o -name "*.bak" \).
Ejercicios
1. Filtrar dentro de una subcarpeta. Sobre el árbol project que creaste, escribe un único comando find que liste solo los archivos regulares dentro de project/config (sin bajar a ninguna otra carpeta del proyecto).
Ver solución
find project/config -type f
Por qué funciona: al pasar project/config como punto de partida, find nunca sale de esa subcarpeta —no hace falta -maxdepth porque no hay nada más abajo para explorar—, y -type f descarta cualquier directorio que hubiera en el camino.
2. Encontrar los archivos "vacíos". Todos los archivos que creaste con touch pesan 0 bytes; solo access.log pesa 2 MiB. Escribe un comando que liste, dentro de todo project, los archivos regulares con menos de 1 kilobyte.
Ver solución
find project -type f -size -1k
Por qué funciona: -size -1k significa "menor a 1 kibibyte" (1024 bytes). Los ocho archivos de touch (0 bytes) cumplen la condición; access.log (2 MiB) queda afuera por goleada.
3. Combinar tipo y alternancia con paréntesis. Dentro de project/config, escribe un único comando que encuentre los archivos .yaml o .env, asegurándote de que la condición -type f aplique a ambas ramas.
Ver solución
find project/config -type f \( -name "*.yaml" -o -name "*.env" \)
Por qué funciona: los paréntesis escapados agrupan las dos alternativas de nombre antes de que -and (implícito entre -type f y el grupo) las combine, así que -type f termina aplicando al resultado de la alternancia completa, no solo a la primera rama.
4. Depurar un comando peligroso. Un compañero te pasa este comando para limpiar respaldos viejos y te pide revisarlo antes de correrlo:
find project -delete -name "*.js.bak"
Explica qué pasaría si lo ejecutas tal cual, y reescríbelo siguiendo el criterio de listar, verificar y solo entonces borrar.
Ver solución
Qué pasaría: -delete aparece antes que cualquier filtro, así que se evalúa primero para cada entrada del árbol —incluidos project mismo y todas sus subcarpetas— y borra todo, sin que -name "*.js.bak" llegue jamás a filtrar nada.
Versión segura, en dos pasos:
find project -type f -name "*.js.bak" # 1. listas y verificas visualmente
find project -type f -name "*.js.bak" -delete # 2. solo entonces, con las mismas condiciones
Por qué funciona: al listar primero con exactamente las condiciones que planeas usar para borrar, confirmas que la lista contiene solo lo que quieres eliminar antes de que sea irreversible. -delete va al final, nunca al principio.
Resumen y siguiente paso
Ya puedes ubicar cualquier archivo de tu máquina por nombre, tipo, tamaño, fecha o profundidad, combinar esas condiciones con -and, -or y !, y actuar sobre los resultados con -exec o -delete siguiendo un criterio de seguridad explícito: listar, verificar, y solo entonces actuar.
Antes de avanzar deberías poder:
- Escribir un
findcon al menos dos condiciones combinadas (por ejemplo-type f -size +1M, o una alternancia agrupada con-o). - Explicar por qué
-deletetiene que ir al final de la expresión y no al principio. - Ejecutar el protocolo completo —listar, verificar, borrar— sobre un conjunto real de archivos sin sorpresas.
Lo que te falta ahora es la otra mitad del problema: find te da la ruta del archivo correcto, pero no te dice qué hay dentro de él. Eso es exactamente lo que resuelve grep, la siguiente lección: encontrar una línea de texto en cualquier lugar de un proyecto entero.
Recursos
- find(1) — Linux manual page — referencia completa de opciones de
find, la que se usó para verificar cada flag de esta lección. - GNU Findutils Manual — Invoking find — documentación oficial de la sintaxis general del comando y sus opciones globales como
-maxdepth. - GNU Findutils Manual — find Expressions — el detalle oficial de
-and,-or,-noty su precedencia, la base del error común #3. - GNU Findutils Manual — Deleting files — la advertencia oficial sobre el orden de
-deletedentro de la expresión. - fd — A simple, fast and user-friendly alternative to find — repositorio oficial de la alternativa moderna mencionada en esta lección.