Cerramos el Módulo 4 diciendo que a informe-diario.sh le falta robustez, y prometimos empezar por aquí: find, xargs, tar y ficheros temporales seguros. No es casualidad que la robustez empiece por los ficheros. Casi todo lo que un script de operaciones hace mal lo hace al elegir sobre qué ficheros actuar —borra de más, se salta un directorio, se atraganta con un nombre que lleva un espacio— o al escribir en un temporal que otro proceso puede pisar. En esta lección aprenderás a seleccionar ficheros con precisión quirúrgica, a actuar sobre ellos sin que un nombre raro rompa nada, y a crear archivos temporales y comprimidos que no se conviertan en un agujero de seguridad.
Contenido
- Por qué el globbing no basta
- Anatomía de
find - Criterios de selección
- Combinar criterios:
-a,-o,!y paréntesis -maxdepthy-mindepth: por qué van antes- Acciones:
-print,-delete,-execy-execdir - Nombres con espacios:
-print0yxargs -0 xargsa fondo- Temporales seguros con
mktemp - Archivar y comprimir:
tar,gzip,zcat,zgrep - Enlaces duros y simbólicos
rsyncen dos líneas
- Por qué el globbing no basta
En 02-05 viste que *.log lo expande el shell antes de que el comando arranque, y que con shopt -s globstar incluso **/*.log recorre subdirectorios. ¿Para qué hace falta find, entonces?
| Necesidad | ¿Globbing? | find |
|---|---|---|
| Ficheros por nombre en un directorio | Sí | Sí |
| Recorrer subdirectorios | Con globstar |
Nativo |
| Filtrar por fecha, tamaño, permisos, dueño | No | Sí |
| Distinguir fichero / directorio / enlace | Con trucos | -type |
| Limitar la profundidad | No | -maxdepth |
| Miles de resultados | Falla: Argument list too long |
Sin límite |
Ese último punto es el que mata scripts en producción: el globbing construye una línea de comandos con todos los nombres, y el núcleo la rechaza a partir de unos dos megabytes. find procesa los ficheros de uno en uno. La regla práctica: globbing para un puñado de ficheros conocidos en un directorio; find para buscar por criterios o recorrer árboles.
- Anatomía de
find
findTres partes, siempre en este orden: dónde buscar, qué seleccionar y qué hacer con lo encontrado. Si omites la acción, find asume -print. Si omites los criterios, muestra todo lo que hay bajo la ruta.
Dos detalles que causan sorpresas desde el primer día:
- El patrón
'*.log'va entrecomillado. Sin comillas, el shell lo expandiría antes de lanzarfind, con los ficheros del directorio actual, yfindrecibiría algo que no esperaba. Es el mismo principio de comillas de 03-06: aquí el patrón es parafind, no para el shell. findrecorre recursivamente desde la ruta indicada.find .puede tardar minutos en un árbol grande.
- Criterios de selección
find /srv/veloz/datos -name 'envios*.csv' # por nombre, sensible a mayúsculas
find /srv/veloz/datos -iname 'ENVIOS*.CSV' # -i: ignora mayúsculas → encuentra envios.csv
find /srv/veloz -path '*/historico/2026/*' # el patrón se aplica a la RUTA completa
find /var/log/veloz -type f # solo ficheros regulares
find /srv/veloz -type d -name 'historico' # solo directorios
find ~/veloz-ops -type l # solo enlaces simbólicosOjo con la diferencia entre -name y -path: -name compara solo el nombre final (el basename de 04-04), mientras que -path compara la ruta entera tal y como find la va generando, así que sus * sí atraviesan barras.
Los criterios de tamaño y tiempo son los que hacen a find insustituible:
| Criterio | Significado | Ejemplo |
|---|---|---|
-size +100M |
mayor de 100 MiB (k, M, G; c = bytes) |
logs desbocados |
-size -1k |
menor de 1 KiB | informes vacíos |
-mtime +30 |
modificado hace más de 30 días | purga de histórico |
-mtime -1 |
modificado en las últimas 24 h | informe de hoy |
-mmin -15 |
modificado en los últimos 15 minutos | vigilancia |
-newer f |
más reciente que el fichero f |
cambios desde el último informe |
-user joan |
propiedad de ese usuario | auditoría |
-perm 600 |
permisos exactamente 600 | comprobar veloz-ops.conf |
-perm -o=w |
escribible por otros (peligro) | auditoría de seguridad |
-empty |
fichero vacío o directorio sin contenido | limpieza |
El signo importa mucho: + es "más de", - es "menos de", y sin signo es exactamente. Un -mtime 30 significa "modificado en el día 30 contando hacia atrás", casi nunca lo que quieres. Y -mtime cuenta en bloques de 24 horas truncando: para ventanas finas, usa -mmin.
find ~/veloz-ops/logs -type f -name 'informe-*.txt' -mtime +30
# /home/joan/veloz-ops/logs/informe-2026-06-15.txt
# /home/joan/veloz-ops/logs/informe-2026-06-14.txtAhí está la purga de informes viejos que necesita el toolkit: todos los informes de más de un mes.
- Combinar criterios:
-a, -o, ! y paréntesis
-a, -o, ! y paréntesisCuando pones dos criterios seguidos, find los une con un Y implícito. Para el resto hay operadores:
find ~/veloz-ops/logs -name '*.txt' -o -name '*.log' # O lógico
find /var/log/veloz -type f ! -name '*.gz' # NO: los no comprimidos
find /srv/veloz -type f \( -name '*.csv' -o -name '*.tsv' \) -mtime +90Los paréntesis se escapan (\\( y \\)) o se entrecomillan ('('), porque para el shell los paréntesis sin escapar significan subshell. Y son imprescindibles en cuanto mezclas -o con otro criterio, porque -a tiene más prioridad que -o: sin paréntesis, ese último ejemplo significaría "los .csv (de cualquier fecha) o los .tsv de más de 90 días".
Este error es especialmente grave combinado con -delete. Comprueba siempre con -print antes de sustituirlo por una acción destructiva.
-maxdepth y -mindepth: por qué van antes
-maxdepth y -mindepth: por qué van antesfind /srv/veloz/datos -maxdepth 1 -name '*.csv' # solo el nivel superior
find /srv/veloz/datos/historico -mindepth 2 -maxdepth 2 -type d # los AAAA/MM-maxdepth 0 es la propia ruta de partida; 1 son sus hijos directos. -mindepth 1 excluye la ruta de partida, útil para vaciar un directorio sin borrarlo.
Estas dos opciones deben ir antes que cualquier otro criterio. Técnicamente no son criterios sino opciones globales: afectan a todo el recorrido, no a un fichero concreto. Si escribes find . -name '*.log' -maxdepth 1, find avisa con warning: -maxdepth is a global option y lo aplica igualmente, pero es una señal de que no has entendido el orden y algún día te morderá con opciones que no perdonan.
- Acciones:
-print, -delete, -exec y -execdir
-print, -delete, -exec y -execdirfind ~/veloz-ops/logs -name 'informe-*.txt' -mtime +30 -print # listar (por defecto)
find ~/veloz-ops/logs -name 'informe-*.txt' -mtime +30 -delete # borrar
find /var/log/veloz -name '*.log.[0-9]' -exec gzip {} \; # ejecutar por fichero
find /var/log/veloz -name '*.log.[0-9]' -exec gzip {} + # ejecutar por lotes-delete es más seguro y rápido que -exec rm {} \; porque no lanza procesos y no tiene problemas con nombres raros, pero debe ir al final: find . -delete -name '*.txt' borra todo el árbol antes de mirar el nombre.
La diferencia entre \; y + es la que más rendimiento cuesta ignorar. El {} es el marcador donde find inserta el nombre del fichero, y el terminador decide cómo se agrupan:
| Terminador | Invocaciones | Con 5.000 ficheros | Cuándo usarlo |
|---|---|---|---|
\; |
Una por fichero | 5.000 procesos (~30 s) | El comando solo acepta un argumento, o quieres el código de salida individual |
+ |
Una por lote (miles de argumentos) | 2-3 procesos (<1 s) | Casi siempre: grep, gzip, rm, chmod |
El \; va escapado por el mismo motivo que los paréntesis: sin escapar, el ; termina el comando para el shell.
-execdir es la variante segura —find /srv/veloz/datos -name '*.tmp' -execdir rm -- {} \;—: ejecuta el comando dentro del directorio donde está cada fichero y le pasa ./nombre en lugar de la ruta completa. Eso evita ataques en los que alguien renombra un directorio a mitad del recorrido y evita que un fichero llamado -rf se interprete como opción.
- Nombres con espacios:
-print0 y xargs -0
-print0 y xargs -0Este es el clásico que separa a quien copia recetas de quien entiende el shell: find ~/veloz-ops/logs -name '*.txt' | xargs rm está roto.
find imprime un nombre por línea y xargs separa por espacios en blanco. Un fichero llamado informe julio.txt llega como dos argumentos: xargs intenta borrar informe y julio.txt. Un nombre con salto de línea es aún peor. La solución canónica usa el único byte que no puede aparecer en un nombre de fichero, el nulo:
-print0 separa con \0 en lugar de \n; -0 le dice a xargs que espere ese separador. Nunca canalices find a xargs sin ese par. Si el comando lo permite, -exec ... + hace lo mismo sin tubería y sin riesgo.
xargs a fondo
xargs a fondoxargs lee de la entrada estándar y construye líneas de comandos con lo leído. Sus opciones útiles:
| Opción | Efecto |
|---|---|
-0 |
Separador nulo (siempre, con find -print0) |
-n N |
Como mucho N argumentos por invocación |
-I{} |
Sustituye {} por cada elemento, uno a uno (implica -n 1) |
-P N |
Ejecuta hasta N invocaciones en paralelo |
-t |
Muestra en stderr el comando antes de ejecutarlo (depuración) |
-r |
No ejecutar nada si la entrada está vacía (GNU; vital en scripts) |
# Comprimir los CSV del histórico, 4 a la vez, mostrando qué hace
find /srv/veloz/datos/historico -name '*.csv' -print0 \
| xargs -0 -r -t -P 4 -n 1 gzipSin -r, si el find no encuentra nada, xargs ejecuta gzip sin argumentos y se queda esperando en la entrada estándar: el script se cuelga sin explicación. Es un fallo real y frecuente en tareas programadas.
-I{} sirve cuando el nombre no va al final del comando —xargs -0 -I{} cp -- {} /srv/veloz/respaldo/—, pero no agrupa: una invocación por fichero, con el coste que ya conoces. Úsalo solo cuando la posición del argumento lo exija.
- Temporales seguros con
mktemp
mktempinforme-diario.sh necesita un fichero intermedio para ordenar la tabla antes de imprimirla. La tentación es tmp=/tmp/informe.$$, y está mal: $$ es el PID, que ni es secreto ni es impredecible. /tmp lo puede escribir cualquiera. Dos ataques clásicos: la condición de carrera (un atacante crea el fichero entre tu comprobación y tu escritura) y el ataque por enlace simbólico (crea /tmp/informe.12345 apuntando a ~/.ssh/authorized_keys, y tu script escribe ahí con tus permisos). La solución es delegar en el sistema:
tmp=$(mktemp) # /tmp/tmp.aX9k2Lp0
tmpdir=$(mktemp -d) # directorio, permisos 700
tmp=$(mktemp -t informe-diario.XXXXXX) # con prefijo reconocible
tmp=$(mktemp -p ~/veloz-ops/logs inf.XXXXXX) # en un directorio concretomktemp crea el fichero atómicamente, con nombre aleatorio y permisos 600 (700 para directorios). No hay ventana de carrera y nadie más puede leerlo. Las XXXXXX (mínimo seis) son donde se inserta la parte aleatoria.
Falta la otra mitad: borrarlo pase lo que pase, también si el script muere a mitad. Eso se hace con trap, y es la primera línea de 05-03: tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT. Quédate con la pareja: quien crea un temporal, en la línea siguiente programa su limpieza.
- Archivar y comprimir:
tar, gzip, zcat, zgrep
tar, gzip, zcat, zgreptar agrupa muchos ficheros en uno (.tar); gzip comprime un fichero. Juntos dan el .tar.gz de toda la vida:
cd /srv/veloz/datos # clave: nos situamos en la raíz del árbol
tar -czf ~/veloz-ops/logs/historico-2026-07.tar.gz historico/2026/07
tar -tzf ~/veloz-ops/logs/historico-2026-07.tar.gz | head -3 # listar sin extraer
tar -xzf ~/veloz-ops/logs/historico-2026-07.tar.gz -C /tmp/restaurar # extraerLas letras: c crear, x extraer, t listar, z pasar por gzip, f fichero destino, -C cambiar de directorio antes de actuar. j usa bzip2 y J usa xz (más lento, comprime más).
Usa siempre rutas relativas. Si archivas /srv/veloz/datos/historico, tar avisa con Removing leading '/' y, en versiones antiguas o con la opción -P, la extracción escribiría directamente sobre /srv/veloz/datos en cualquier máquina, machacando lo que hubiera. Con rutas relativas decides dónde restaurar con -C. El patrón correcto es el cd previo (o tar -C /srv/veloz/datos -czf ... historico/2026/07).
--exclude acepta globs y se puede repetir. Y para los logs rotados de /var/log/veloz no hace falta descomprimir nada:
tar -czf respaldo.tar.gz --exclude='*.tmp' --exclude='logs/*' veloz-ops
zcat /var/log/veloz/acceso.log.2.gz | wc -l # como cat, pero para .gz
zgrep -c ' 500 ' /var/log/veloz/acceso.log.*.gz # como grep, sobre comprimidos
gzip -d acceso.log.2.gz # descomprime y BORRA el .gz
gzip -k acceso.log.1 # -k conserva el originalzgrep sobre los rotados es lo que permite a informe-diario.sh responder "¿cuántos errores 500 hubo la semana pasada?" sin ocupar disco extra.
- Enlaces duros y simbólicos
ln /srv/veloz/datos/envios.csv /srv/veloz/datos/envios-hoy.csv # duro
ln -s /srv/veloz/datos/envios.csv ~/veloz-ops/envios-actual.csv # simbólico
readlink -f ~/veloz-ops/envios-actual.csv # /srv/veloz/datos/envios.csv
find ~/veloz-ops -type l ! -exec test -e {} \; -print # enlaces rotosEnlace duro (ln) |
Enlace simbólico (ln -s) |
|
|---|---|---|
| Qué es | Otro nombre para los mismos datos | Un fichero que contiene una ruta |
| Entre discos | No | Sí |
| A directorios | No | Sí |
| Si borras el original | Los datos siguen vivos | El enlace queda roto |
ls -l muestra |
Un fichero normal | enlace -> destino |
En la práctica: simbólicos para el 95 % de los casos (apuntar al "último informe", cambiar de versión activa), duros para copias baratas dentro del mismo sistema de ficheros. readlink -f resuelve la cadena completa hasta la ruta real, y es la forma fiable de saber a qué apunta algo.
rsync en dos líneas
rsync en dos líneasrsync -a --delete origen/ destino/ sincroniza árboles copiando solo lo que ha cambiado, funciona sobre SSH y es la herramienta correcta para respaldos incrementales. Su sitio natural es 07-03; por ahora quédate con que existe y con que la barra final del origen cambia el significado.
Errores Comunes y Consejos
- Olvidar las comillas en
-name '*.log'. El shell expande el patrón antes yfindbusca otra cosa. -deleteantes de los criterios. Borra el árbol entero. Ponlo siempre al final y prueba antes con-print.find ... | xargssin-print0/-0. Un espacio en un nombre y borras lo que no debías.xargssin-r. Con entrada vacía ejecuta el comando sin argumentos: cuelgue o desastre.-osin paréntesis. La precedencia de-acambia el sentido de la expresión sin avisar.-mtime 30sin signo. Significa "exactamente el día 30", no "más de 30 días".tarcon rutas absolutas. Restaurar machaca el sistema original. Usacdo-Cy rutas relativas.- Consejo: construye los
finddestructivos en dos pasos. Primero con-print, revisas la lista, y solo entonces cambias la acción. En un script, guarda la lista en un temporal demktempy actúa sobre ella.
Ejercicios
Ejercicio 1. Escribe una función purgar_informes() que borre de ~/veloz-ops/logs los ficheros informe-*.txt de más de 30 días y los ficheros vacíos de cualquier antigüedad, informando de cuántos borró. Debe funcionar con nombres que contengan espacios.
Ejercicio 2. Archiva el histórico del mes anterior (/srv/veloz/datos/historico/AAAA/MM/) en ~/veloz-ops/logs/historico-AAAA-MM.tar.gz excluyendo los .tmp, verifica que el archivo se puede listar y solo entonces borra los originales.
Ejercicio 3. Cuenta cuántas peticiones con código 500 hay en todos los acceso.log* de /var/log/veloz, incluidos los comprimidos, usando un fichero temporal seguro para el resultado intermedio.
Soluciones
Solución 1.
purgar_informes() {
local dir="${1:-$HOME/veloz-ops/logs}" n=0
while IFS= read -r -d '' f; do
rm -- "$f"; (( ++n ))
done < <(find "$dir" -maxdepth 1 -type f \
\( -name 'informe-*.txt' -mtime +30 -o -empty \) -print0)
printf 'Purgados %d ficheros de %s\n' "$n" "$dir"
}read -d '' lee hasta el byte nulo, que es lo que emite -print0: la pareja exacta del patrón de la sección 7. Los paréntesis agrupan la condición "(viejo Y con ese nombre) O vacío", y -maxdepth 1 impide que la purga se cuele en subdirectorios. El -- antes de "$f" protege de nombres que empiecen por guion.
Solución 2.
mes=$(date -d 'last month' +%m); anio=$(date -d 'last month' +%Y)
destino=~/veloz-ops/logs/historico-$anio-$mes.tar.gz
tar -C /srv/veloz/datos --exclude='*.tmp' -czf "$destino" "historico/$anio/$mes"
if tar -tzf "$destino" > /dev/null; then
find "/srv/veloz/datos/historico/$anio/$mes" -type f -delete
printf 'Archivado y purgado %s/%s\n' "$anio" "$mes"
else
printf 'ERROR: el archivo %s no es legible, no se borra nada\n' "$destino" >&2
fiEl date -d 'last month' viene de 04-06. El orden es innegociable: verificar antes de borrar. El tar -tzf a /dev/null no muestra nada pero devuelve código distinto de cero si el archivo está corrupto, y el if lo aprovecha exactamente como en 03-04.
Solución 3.
tmp=$(mktemp -t veloz500.XXXXXX)
zgrep -h ' 500 ' /var/log/veloz/acceso.log* > "$tmp"
printf 'Errores 500 en total: %d\n' "$(wc -l < "$tmp")"
cut -d' ' -f7 "$tmp" | sort | uniq -c | sort -rn | head -5 # rutas más afectadas
rm -f "$tmp"zgrep procesa indistintamente los ficheros planos y los .gz, y -h suprime el prefijo con el nombre del fichero para que cut vea siempre la misma estructura de columnas. El rm final es correcto, pero solo se ejecuta si el script llega vivo hasta ahí: en 05-03 lo sustituirás por un trap que garantice la limpieza incluso ante un fallo.
Conclusión
find es el buscador que el globbing no puede sustituir: selecciona por nombre, ruta, tipo, tamaño, fecha, dueño, permisos o vacuidad, combina criterios con -a, -o, ! y paréntesis escapados, acota el recorrido con -maxdepth/-mindepth —que van antes por ser opciones globales— y actúa con -print, -delete, -exec ... \;, -exec ... + (miles de veces más barato) o -execdir. Cuando hace falta canalizar, la pareja -print0 con xargs -0 es obligatoria, y -r, -n, -I{}, -P y -t completan el instrumental. mktemp y mktemp -d crean temporales atómicos, privados e impredecibles, en lugar del /tmp/fichero.$$ que invita a que te pisen. tar con rutas relativas archiva y zcat/zgrep leen los logs rotados sin descomprimirlos. Y los enlaces, duros o simbólicos, dan nombres alternativos a lo que ya existe.
Con esto informe-diario.sh ya sabe elegir sobre qué ficheros trabaja y dónde deja sus intermedios. Pero un script de operaciones no solo toca ficheros: lanza procesos, y a veces varios a la vez. ¿Está viva la veloz-api antes de pedirle datos? ¿Puedo calcular las cuatro ciudades en paralelo en lugar de una tras otra? ¿Qué pasa si el informe de las 6:00 aún corre cuando arranca el de las 6:05? En 05-02 entramos en la gestión de procesos: PIDs, ps, pgrep, trabajos en segundo plano, wait, señales, timeout y el bloqueo con flock que impide que dos informes se pisen.
Curso de Programación en Bash
Módulo 1: Introducción a Bash
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
