1. Cuándo alcanzan los comodines
Para un lote dentro de un solo directorio, seleccionado por el nombre, un bucle sobre un comodín es lo más simple, como vimos en el tema 14:
shopt -s nullglob
for registro in /var/log/app/*.log; do
gzip -- "$registro"
done
Los comodines dejan de alcanzar cuando la selección depende de algo más que el nombre, como la antigüedad o el tamaño; cuando hay que recorrer subdirectorios a cualquier profundidad (globstar lo permite, pero arma toda la lista en memoria); o cuando los archivos son tantos que la lista supera el límite de longitud de una línea de comandos, con el error Argument list too long. Para todo eso está find.
2. Elegir archivos con find
find recorre un árbol de directorios y selecciona los archivos que cumplen una serie de criterios, que se combinan como condiciones. El curso de Terminal lo presentó; estos son los criterios que más aparecen en la automatización:
| Criterio | Selecciona |
|---|---|
-type f, -type d | Archivos comunes; directorios. |
-name '*.log', -iname '*.jpg' | Por nombre, con comodines; -iname sin distinguir mayúsculas. |
-mtime +7, -mtime -1 | Modificados hace más de 7 días; en el último día. |
-mmin -30 | Modificados en los últimos 30 minutos. |
-newer archivo | Más nuevos que un archivo de referencia (tema 12). |
-size +100M, -size -1k | Más de 100 MiB; menos de 1 KiB. |
-empty | Archivos o directorios vacíos. |
-maxdepth 1 | Sin entrar en subdirectorios. Va antes que los demás criterios. |
-path '*/.git' -prune -o … | Saltear un directorio entero. |
\( … -o … \) | Agrupar alternativas: «esto o aquello». |
! -name '*.gz' | Negar un criterio. |
# Registros de más de 7 días que todavía no están comprimidos, sin entrar en .git
find /var/log/app -path '*/.git' -prune -o -type f -name '*.log' ! -name '*.gz' -mtime +7 -print
Los criterios se evalúan en orden y se combinan con «y» implícito; -o significa «o». La forma con -prune se lee: «si la ruta es un .git, no entres; si no, y es un archivo .log…, muéstralo». El -print final es necesario cuando hay un -o, para que solo se muestren los de la segunda rama. Antes de actuar sobre los archivos, conviene siempre ejecutar el find solo, con -print, y mirar la lista.
3. Actuar con find -exec
find puede ejecutar un comando sobre cada archivo que encuentra, con -exec. Las llaves {} se reemplazan por la ruta del archivo, y la acción termina con \; o con +, que se comportan de forma muy distinta:
| Forma | Ejecuta | Cuándo |
|---|---|---|
-exec comando {} \; | Una vez por cada archivo. | Cuando el comando acepta un solo archivo, o hay que procesar uno por vez. |
-exec comando {} + | Una vez con muchos archivos como argumentos. | Cuando el comando acepta varios archivos: mucho más rápido. |
-execdir comando {} \; | Como -exec, pero dentro del directorio de cada archivo. | Comandos que trabajan con el nombre relativo. |
-delete | Borra cada archivo encontrado. | Borrar, sin crear un proceso por archivo. |
$ find . -name '*.log' -exec echo "uno:" {} \;
uno: ./-raro.log
uno: ./a b.log
uno: ./normal.log
$ find . -name '*.log' -exec echo "todos:" {} +
todos: ./-raro.log ./a b.log ./normal.log
Con +, find agrupa tantas rutas como admite una línea de comandos, y ejecuta el comando las veces necesarias. Para comprimir diez mil registros, -exec gzip {} + crea unos pocos procesos en lugar de diez mil. Y como find pasa las rutas directamente como argumentos, sin pasar por la shell, los espacios y caracteres raros no son un problema.
4. Los nombres problemáticos
En Linux, un nombre de archivo puede contener cualquier carácter, salvo la barra y el carácter nulo. Tres casos rompen los scripts descuidados:
| Nombre | Problema | Protección |
|---|---|---|
a b.log | Un espacio parte el nombre en dos al pasar por la división en palabras (tema 3). | Comillas en cada uso, y nunca recorrer la salida de ls. |
-raro.log | Un comando lo toma como un conjunto de opciones. | -- antes de los archivos, o rutas que empiecen con ./. |
| Un nombre con un salto de línea | Cualquier herramienta que trabaje por líneas lo ve como dos archivos. | Separar con nulos: -print0, xargs -0, read -d ''. |
$ touch "-raro.log"
touch: invalid option -- 'r'
$ touch -- "-raro.log" # ahora sí
$ for f in *.log; do ls -d "$f"; done
ls: invalid option -- '.'
a b.log
normal.log
$ for f in ./*.log; do ls -d "$f"; done
./-raro.log
./a b.log
./normal.log
Comprobado así, el nombre con guion inicial confundió a touch y a ls, aunque estaba entre comillas: las comillas protegen de la división en palabras, no de que el comando interprete un argumento como opción. Escribir el patrón como ./*.log hace que todas las rutas empiecen con ./, y el problema desaparece; find ya las devuelve así. Los saltos de línea son raros, pero un script que procesa archivos ajenos, como los de un directorio compartido, debe estar preparado para ellos.
5. xargs
xargs lee una lista de su entrada y la convierte en argumentos de un comando. Combinado con find, es una alternativa a -exec con más opciones de control:
$ find . -name '*.log' | xargs ls -d
ls: cannot access './a': No such file or directory
ls: cannot access 'b.log': No such file or directory
./-raro.log
./normal.log
$ find . -name '*.log' -print0 | xargs -0 ls -d --
./-raro.log
./a b.log
./normal.log
Sin opciones, xargs separa la entrada por espacios y saltos de línea, y además interpreta comillas, de modo que parte los nombres con espacios. Por eso se usa siempre con find -print0 y xargs -0, que separan por el carácter nulo. Estas son sus opciones más útiles:
| Opción | Efecto |
|---|---|
-0 | La entrada está separada por nulos. |
-r | Si la entrada está vacía, no ejecutar el comando. Sin ella, se ejecuta una vez sin argumentos, lo que con rm o ls puede ser sorprendente. |
-n N | Pasar como máximo N argumentos por ejecución. |
-I {} | Ejecutar una vez por elemento, reemplazando {} en cualquier lugar del comando. |
-P N | Ejecutar hasta N comandos a la vez (sección 7). |
find . -name '*.tmp' -print0 | xargs -0 -r rm -f -- # borrar, si hay algo
find fotos -iname '*.png' -print0 | xargs -0 -I {} cp -- {} {}.original # una copia de cada una
printf '%s\0' "${servidores[@]}" | xargs -0 -n 1 ping -c1 -W2 # una lista propia
6. Un bucle sobre la salida de find
Cuando la acción sobre cada archivo tiene varios pasos, o necesita variables del script, o hay que contar resultados, la forma más flexible es el bucle del tema 16, con la salida de find separada por nulos:
comprimidos=0
while IFS= read -r -d '' registro; do
if gzip -- "$registro"; then
comprimidos=$(( comprimidos + 1 ))
else
echo "No se pudo comprimir $registro" >&2
fi
done < <(find /var/log/app -type f -name '*.log' -mtime +7 -print0)
echo "Comprimidos: $comprimidos"
Esta forma combina lo mejor de las dos herramientas: find elige con sus criterios, y el cuerpo del bucle tiene toda la expresividad de Bash. Su costo es que cada vuelta ejecuta los comandos uno por vez, de modo que es más lenta que -exec … + para operaciones simples sobre muchísimos archivos.
7. Procesar en paralelo
Cuando cada archivo requiere un trabajo pesado, como comprimir, convertir una imagen o calcular una suma de verificación, procesarlos de a uno desaprovecha los procesadores del equipo. La opción -P de xargs ejecuta varios comandos a la vez:
$ printf '%s\0' 1 2 3 4 | xargs -0 -n 1 -P 4 sh -c 'sleep 1; echo "terminó $0"'
terminó 1
terminó 2
terminó 3
terminó 4
# en total, un segundo y no cuatro
El número de procesos simultáneos conviene ajustarlo a los procesadores disponibles, que da el comando nproc. Para usar una función del script como tarea, se exporta con export -f, como vimos en el tema 18, y se llama a través de bash -c:
reducir() {
convert "$1" -resize 1600x1600\> "${1%.*}-web.jpg"
}
export -f reducir
find fotos -iname '*.jpg' ! -name '*-web.jpg' -print0 \
| xargs -0 -n 1 -P "$(nproc)" bash -c 'reducir "$1"' _
El _ final ocupa el lugar de $0 dentro de bash -c, de modo que la ruta llega como $1. Dos advertencias: las salidas de las tareas simultáneas pueden mezclarse en la pantalla, y las tareas no deben escribir en los mismos archivos. Para necesidades más complejas existe GNU parallel, una herramienta dedicada con muchas más opciones.
8. Cuál elegir
| Situación | Conviene |
|---|---|
| Un directorio, selección por nombre | for f in ./*.ext, con nullglob |
| Un comando simple sobre muchos archivos | find … -exec comando {} + |
| Borrar lo seleccionado | find … -delete |
| Varios pasos por archivo, contadores, mensajes | while IFS= read -r -d '' f; do …; done < <(find … -print0) |
| Trabajo pesado por archivo, en paralelo | find … -print0 | xargs -0 -P N … |
Una lista que no viene de find | printf '%s\0' … | xargs -0 … |
9. Renombrar en masa
Renombrar muchos archivos es una de las tareas por lotes más frecuentes, y una de las más peligrosas: un error puede hacer que dos archivos reciban el mismo nombre, y que uno pise al otro. Con las expansiones del tema 6, un bucle resuelve casi cualquier renombrado:
for f in ./*.jpeg; do mv -n -- "$f" "${f%.jpeg}.jpg"; done # cambiar la extensión
for f in ./*\ *; do mv -n -- "$f" "${f// /_}"; done # espacios por guiones bajos
for f in ./IMG_*; do mv -n -- "$f" "./vacaciones_${f#./IMG_}"; done # cambiar un prefijo
n=1; for f in ./*.png; do printf -v nuevo './captura-%03d.png' "$n"; mv -n -- "$f" "$nuevo"; n=$(( n + 1 )); done
La opción -n de mv (no clobber) es la protección esencial: si el destino ya existe, mv no lo sobrescribe. Sin ella, un renombrado que produce dos veces el mismo nombre borra un archivo en silencio. Pero -n solo evita el daño; el archivo que no se movió sigue con su nombre viejo, y el script debería detectarlo o, mejor, elegir otro nombre, como hace el script de la sección 11.
Muchas distribuciones incluyen además un comando rename para renombrar con expresiones, aunque hay dos versiones incompatibles con el mismo nombre: la de Perl, que usa rename 's/\.jpeg$/.jpg/' *.jpeg, y la de util-linux, que usa rename .jpeg .jpg *.jpeg. Por esa ambigüedad, en un script es más predecible el bucle de Bash.
10. Antes de tocar mil archivos
Una operación por lotes multiplica las consecuencias de un error, como advirtió el tema 1. Antes de ejecutarla sobre los datos reales:
- Mirar la selección. Ejecutar el
findsolo, con-print, o contar los resultados con| wc -l(o-printf '.' | wc -c, que no se confunde con saltos de línea). ¿Son los archivos esperados? ¿Son tantos como se esperaba? - Simular. Reemplazar la acción por un
echo, o usar una opción-ncomo la del script de este tema, y revisar la lista de cambios. - Probar en una copia. Un
cp -adel directorio a un lugar de pruebas, y ejecutar allí. - Preferir lo reversible. Mover a un directorio de descarte antes que borrar; renombrar con
mv -n; comprimir conservando el original congzip -khasta verificar. - Hacer el proceso repetible. Que una segunda ejecución reconozca lo ya hecho y no lo rehaga, como en el tema 27. Así, si se interrumpe a la mitad, se vuelve a lanzar sin miedo.
11. Un script completo
Las cámaras y los teléfonos nombran las fotos con números, como IMG_4031.JPG, que no dicen nada y se repiten entre dispositivos. Este script las renombra con su fecha y hora de modificación, como 2026-10-14_10-32-07.jpg, de modo que quedan ordenadas cronológicamente. Resuelve los choques de nombres, admite simular y recorrer subdirectorios, y una segunda ejecución no cambia nada.
#!/usr/bin/env bash
#
# renombrar-por-fecha — renombra fotos y videos con su fecha y hora de modificación:
# IMG_4031.JPG → 2026-10-14_10-32-07.jpg
#
# Uso: renombrar-por-fecha [-n] [-r] DIRECTORIO
# -n muestra los cambios sin hacerlos
# -r incluye los subdirectorios
#
set -euo pipefail
simular=0
profundidad=(-maxdepth 1)
while getopts ":nr" opcion; do
case "$opcion" in
n) simular=1 ;;
r) profundidad=() ;;
*) echo "Uso: ${0##*/} [-n] [-r] DIRECTORIO" >&2; exit 2 ;;
esac
done
shift $(( OPTIND - 1 ))
(( $# == 1 )) || { echo "Uso: ${0##*/} [-n] [-r] DIRECTORIO" >&2; exit 2; }
dir="${1%/}"
[[ -d "$dir" ]] || { echo "${0##*/}: no existe el directorio $dir" >&2; exit 1; }
declare -A reservado=() # nombres ya asignados en esta ejecución
renombrados=0
sin_cambio=0
while IFS= read -r -d '' ruta; do
carpeta="${ruta%/*}"
nombre="${ruta##*/}"
ext="${nombre##*.}"
ext="${ext,,}"
[[ "$ext" == jpeg ]] && ext=jpg
base=$(date -r "$ruta" +%Y-%m-%d_%H-%M-%S)
if [[ "$nombre" =~ ^${base}(_[0-9]+)?\.${ext}$ ]]; then
sin_cambio=$(( sin_cambio + 1 )) # ya tiene el nombre que le corresponde
continue
fi
nuevo="$base.$ext"
n=1
while [[ -e "$carpeta/$nuevo" || -n "${reservado[$carpeta/$nuevo]:-}" ]]; do
n=$(( n + 1 ))
nuevo="${base}_$n.$ext" # dos fotos del mismo segundo
done
reservado[$carpeta/$nuevo]=1
if (( simular )); then
printf '%s → %s\n' "${ruta#"$dir"/}" "$nuevo"
else
mv -n -- "$ruta" "$carpeta/$nuevo"
fi
renombrados=$(( renombrados + 1 ))
done < <(find "$dir" "${profundidad[@]}" -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.heic' -o -iname '*.mp4' \) \
-print0 | sort -z)
verbo=$( (( simular )) && echo "se renombrarían" || echo "renombrados" )
echo "Archivos $verbo: $renombrados; ya tenían el nombre correcto: $sin_cambio" >&2
$ renombrar-por-fecha -n ~/fotos
IMG_4031.JPG → 2026-10-14_10-32-07.jpg
IMG_4032.JPG → 2026-10-14_10-32-07_2.jpg
foto de mamá.jpeg → 2026-10-14_10-35-00.jpg
Archivos se renombrarían: 3; ya tenían el nombre correcto: 0
$ renombrar-por-fecha -r ~/fotos
Archivos renombrados: 4; ya tenían el nombre correcto: 0
$ find ~/fotos -type f | sort
/home/ana/fotos/2026-10-14_10-32-07.jpg
/home/ana/fotos/2026-10-14_10-32-07_2.jpg
/home/ana/fotos/2026-10-14_10-35-00.jpg
/home/ana/fotos/viaje 2026/2026-09-01_08-00-00.png
$ renombrar-por-fecha -r ~/fotos
Archivos renombrados: 0; ya tenían el nombre correcto: 4
findelige y el bucle actúa: los archivos llegan separados por nulos y ordenados consort -z, de modo que el resultado no depende del orden en que el sistema de archivos los devuelve, y ningún nombre puede romper el bucle.-rvacía un array de opciones, la técnica del tema 8: sin-r,findrecibe-maxdepth 1; con-r, no recibe nada.- Los choques se resuelven con un sufijo: dos fotos del mismo segundo reciben
_2,_3… El array asociativoreservadorecuerda los nombres ya asignados en esta ejecución, para que la simulación también los respete aunque los archivos todavía no existan con ese nombre. Ymv -nes la última red de seguridad. - El script es idempotente: antes de calcular un nombre nuevo, comprueba con una expresión regular si el actual ya corresponde a su fecha, con o sin sufijo, y en ese caso lo deja.
- La extensión se normaliza a minúsculas, y
jpegpasa ajpg, para que todas las fotos sigan una convención. - El resumen va a la salida de errores, y la estándar queda solo con la lista de cambios de la simulación, que puede guardarse o revisarse.
La fecha de modificación es la que tienen todos los archivos, pero no siempre es el momento en que se tomó la foto: copiarla o editarla la cambia. Las fotos guardan además la fecha original en sus metadatos EXIF, que se leen con herramientas como exiftool; el mismo script, con exiftool en lugar de date -r, daría fechas más fieles.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
for f in $(find …) | Los nombres con espacios se parten. | while IFS= read -r -d '' con -print0. |
find … | xargs comando | Espacios y comillas en los nombres los rompen. | find … -print0 | xargs -0 comando |
xargs rm sin -r | Sin entrada, se ejecuta igual. | xargs -0 -r rm -f -- |
-exec gzip {} \; sobre miles de archivos | Un proceso por archivo: lento. | -exec gzip {} + |
for f in *.log; do cmd "$f" | Un archivo que empieza con guion se toma como opción. | ./*.log, o cmd -- "$f". |
mv "$f" "$nuevo" en un renombrado masivo | Dos archivos con el mismo destino: uno se pierde. | mv -n, y resolver los choques. |
-o en find sin paréntesis ni -print | La acción se aplica solo a una de las ramas, o a ninguna. | \( … -o … \), y -print explícito. |
| Ejecutar directamente sobre los datos reales | Un error afecta a todos los archivos. | Mirar la selección, simular, probar en una copia. |
13. Resumen
- Los comodines alcanzan para un directorio y una selección por nombre; para criterios como la fecha o el tamaño, árboles de directorios o listas enormes, hace falta
find. findcombina criterios como-type,-name,-mtime,-sizey-prune; antes de actuar, se mira la selección con-print.-exec … {} \;ejecuta una vez por archivo;-exec … {} +, con muchos a la vez;-deleteborra.- Los nombres con espacios, con guion inicial o con saltos de línea se manejan con comillas,
--o rutas con./, y separadores nulos. xargs -0, confind -print0, convierte una lista en argumentos;-revita ejecutar sin entrada,-nlimita los argumentos,-Iejecuta uno por uno.while IFS= read -r -d '' fsobrefind -print0permite varios pasos por archivo, con contadores y mensajes.xargs -P Nprocesa en paralelo; una función se usa exportándola y llamándola conbash -c.- Un renombrado masivo usa expansiones de Bash y
mv -n, y resuelve los choques de nombres. - Antes de tocar muchos archivos: mirar la selección, simular, probar en una copia, preferir lo reversible y hacer el proceso repetible.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuándo dejan de alcanzar los comodines para elegir un lote de archivos?
Cuando la selección depende de algo más que el nombre, como la fecha o el tamaño; cuando hay que recorrer subdirectorios a cualquier profundidad; o cuando la lista es tan larga que supera el límite de la línea de comandos.
2. ¿Qué selecciona find . -type f -name '*.log' -mtime +30?
Los archivos comunes con nombre terminado en .log, en el directorio actual y sus subdirectorios, modificados hace más de 30 días.
3. ¿Qué diferencia hay entre -exec gzip {} \; y -exec gzip {} +?
La primera ejecuta gzip una vez por archivo. La segunda lo ejecuta con muchos archivos como argumentos a la vez, creando muchos menos procesos.
4. ¿Por qué un archivo llamado -raro.log causa problemas aunque se escriba entre comillas?
Porque las comillas evitan la división en palabras, pero el comando recibe igual un argumento que empieza con guion, y lo interpreta como opciones. Se evita con -- o escribiendo la ruta como ./-raro.log.
5. ¿Por qué find … | xargs rm es peligroso?
Porque xargs sin -0 separa por espacios y saltos de línea e interpreta comillas, de modo que un nombre con espacios se convierte en varios argumentos, que pueden coincidir con otros archivos. Se usa find … -print0 | xargs -0 rm --.
6. ¿Qué hace xargs -r?
Si la entrada está vacía, no ejecuta el comando. Sin -r, el comando se ejecuta una vez sin argumentos.
7. ¿Cuándo conviene un bucle while read -d '' en lugar de -exec … +?
Cuando cada archivo requiere varios pasos, cuando hay que usar variables del script, contar resultados o informar errores por archivo. Para un comando simple sobre muchos archivos, -exec … + es más rápido.
8. ¿Cómo se usa una función del script con xargs -P?
Se exporta con export -f función y se llama a través de Bash: xargs -0 -n 1 -P 4 bash -c 'función "$1"' _.
9. ¿Qué protege mv -n en un renombrado masivo, y qué no?
Evita sobrescribir un archivo existente si dos archivos reciben el mismo destino. No resuelve el choque: el archivo que no se movió queda con su nombre viejo, y el script debe detectarlo o elegir otro nombre.
10. ¿Qué hace que renombrar-por-fecha sea idempotente?
Que antes de renombrar comprueba si el nombre actual ya corresponde a la fecha del archivo, con o sin sufijo. Si es así, lo deja como está, y una segunda ejecución no cambia nada.
Para practicar
- En un directorio de prueba, crea archivos con espacios, con un guion inicial y, si tu sistema lo permite, con un salto de línea (
touch $'a\nb.txt'). Procésalos confor, confind | xargsy confind -print0 | xargs -0, y compara. - Escribe un
findque liste los archivos de más de 100 MiB de tu directorio personal, sin entrar en.cache. - Comprime en paralelo, con
xargs -P, los.logde más de una semana de un directorio de prueba, conservando los originales congzip -k. Mide el tiempo con y sin-P. - Escribe un bucle que reemplace los espacios por guiones en todos los nombres de un directorio, con
mv -n, y prueba qué pasa si dos nombres terminan iguales. - Copia algunas fotos a un directorio de prueba y usa
renombrar-por-fecha -ny luego sin-n. Comprueba que una segunda ejecución no cambia nada. - Si tienes
exiftool, modifica el script para que use la fecha EXIF cuando existe, y la de modificación cuando no.