Cómo Iterar sobre Archivos en Bash
Cómo iterar de forma segura sobre archivos y directorios en Bash, manejando espacios, globs y listas grandes de archivos con patrones correctos.
Descripción General
Iterar sobre archivos es una de las operaciones más comunes en Bash, pero con frecuencia se hace mal. Los nombres de archivo con espacios, saltos de línea o caracteres glob (*, ?) rompen los loops ingenuos. Abajo están los patrones que sobreviven a esos nombres — globs simples, find, mapfile y xargs — y cuándo conviene cada uno.
Cuándo Usar
- Ejecutar el mismo comando sobre muchos archivos (convertir, analizar, mover)
- Encontrar archivos que coincidan con un patrón y procesarlos en orden
- Renombrar, cambiar permisos o validar por lotes (ver Cómo Copiar y Mover Archivos)
- Generar reportes desde un directorio de archivos de entrada
- Reemplazar texto en un directorio de archivos (ver Procesamiento de Texto en Bash)
Cuándo NO Usar
- Procesar millones de archivos — los límites de longitud de lista de argumentos (
ARG_MAX) fallarán - Filtrado complejo que se expresa mejor con
findusando-execoxargs - Operaciones que requieren estado entre archivos — usa un lenguaje de scripting apropiado (Python, Perl)
- Tareas que necesitan recuperación de errores por archivo —
set -econ loops es complicado
Implementación Paso a Paso
Dos preguntas eligen la herramienta correcta: ¿necesitas recursión o filtros complejos, y puede estar vacío el directorio?
Loop Seguro Básico con Glob
#!/bin/bash
set -euo pipefail
# CORRECTO: Siempre entrecomilla la variable
txt_count=0
for file in *.txt; do
# Manejar el caso de no-coincidencia (glob deja literal '*.txt')
[ -e "$file" ] || continue
echo "Procesando: $file"
((txt_count++))
done
echo "Total archivos .txt: $txt_count"
Recursar con find
#!/bin/bash
set -euo pipefail
# Procesar todos los archivos .py bajo src/, manejando espacios de forma segura
while IFS= read -r -d '' file; do
echo "Analizando: $file"
pylint "$file"
done < <(find src/ -type f -name '*.py' -print0)
# Versión one-liner con xargs (no se necesita loop)
find src/ -type f -name '*.py' -print0 | xargs -0 pylint
# Procesar con un límite (más seguro para directorios enormes)
find src/ -maxdepth 2 -type f -name '*.py' -print0 | \
xargs -0 -n 10 -P 4 pylint
Filtrar y Ordenar
#!/bin/bash
set -euo pipefail
shopt -s nullglob
# Orden numérico en nombres como report_001.txt, report_002.txt.
# Expandir el glob a un array y ordenarlo — nunca `for f in $(ls ...)`.
files=(report_*.txt)
mapfile -t sorted < <(printf '%s\n' "${files[@]}" | sort -t_ -k2 -n)
for file in "${sorted[@]}"; do
echo "Ordenado: $file"
done
Procesar Archivos con Espacios y Caracteres Especiales
#!/bin/bash
set -euo pipefail
# Manejar nombres de archivo con espacios, saltos de línea y globs
srcdir="/data/uploads"
# Enfoque 1: read con find -print0
while IFS= read -r -d '' filepath; do
filename=$(basename "$filepath")
echo "Archivo: $filename"
done < <(find "$srcdir" -type f -print0)
# Enfoque 2: shopt nullglob + expansión entrecomillada
shopt -s nullglob
targets=("$srcdir"/*)
shopt -u nullglob
for filepath in "${targets[@]}"; do
[ -f "$filepath" ] || continue
echo "Encontrado: $(basename "$filepath")"
done
Operaciones por Lotes
#!/bin/bash
set -euo pipefail
# Renombrar .jpeg a .jpg
for file in *.jpeg; do
[ -e "$file" ] || continue
mv -- "$file" "${file%.jpeg}.jpg"
done
# Convertir todas las imágenes HEIC a JPEG
for file in *.heic; do
[ -e "$file" ] || continue
base="${file%.heic}"
heif-convert "$file" "$base.jpg"
done
# Validar todos los archivos JSON
error_count=0
for file in *.json; do
[ -e "$file" ] || continue
if ! jq empty "$file" 2>/dev/null; then
echo "ERROR: JSON inválido en $file" >&2
((error_count++))
fi
done
[ "$error_count" -eq 0 ] || exit 1
Trabajar con Listas Grandes de Archivos
Para ejecución paralela más allá de un solo xargs -P, consulta Ejecución Paralela en Bash. Cuando la lista de archivos es grande o impredecible, tres formas idiomáticas superan a un loop for simple:
- Usa
mapfilepara leer listas de archivos en arrays. Es más rápido que un loopwhile readpara listas grandes y preserva caracteres especiales:
#!/bin/bash
# Leer todos los archivos .py en un array de forma segura
mapfile -d '' -t pyfiles < <(find src/ -type f -name '*.py' -print0)
echo "Encontrados ${#pyfiles[@]} archivos Python"
for file in "${pyfiles[@]}"; do
echo " $file"
done
- Usa
xargspara operaciones simples de un comando por archivo. Gestiona lotes y paralelismo automáticamente:
#!/bin/bash
# Comprimir todos los archivos .log en paralelo (4 a la vez)
find /var/log -type f -name '*.log' -print0 | xargs -0 -P 4 -I{} gzip "{}"
# Ejecutar eslint en todos los archivos .js, 20 a la vez
find src/ -type f -name '*.js' -print0 | xargs -0 -n 20 eslint
- Configura
IFScorrectamente al parsear salida de comandos. El IFS por defecto incluye espacios, lo que rompe nombres de archivo. Siempre usaIFS=conread:
#!/bin/bash
# Mal: IFS incluye espacio, rompe con "My File.txt"
# echo "My File.txt" | while read file; do echo "$file"; done
# Salida: "My" y "File.txt" en líneas separadas
# Bien: IFS= evita recortar espacios iniciales/finales
echo "My File.txt" | while IFS= read -r file; do echo "$file"; done
# Salida: "My File.txt"
Archivos del companion: todos los scripts de esta página son ejecutables desde el companion repo bash-loop-over-files, un .sh por patrón.
Lo que funciona
- Siempre entrecomilla variables de archivo.
"$file"previene la división de palabras en espacios y la interpretación de caracteres glob. - Usa
find -print0 | while read -r -d ''para recursión o filtros complejos — es la única forma que sobrevive a un nombre de archivo que contiene un salto de línea. - Habilita
nullglobal usar globs en loops. De lo contrario*.txtsin coincidencias itera una vez con el string literal*.txt. - Usa
--antes de nombres de archivo en comandos. Sin él, un archivo llamado-rfse lee como flags pormvorm. - Verifica
[ -e "$file" ]al inicio del loop. Es una guarda de una línea para cuandonullglobestá desactivado y el glob no encontró nada.
Errores Comunes
for file in $(ls *.txt)— nunca hagas esto. La salida delsno es parseable; los espacios y saltos de línea en nombres de archivo rompen el loop.- Variables sin entrecomillar:
mv $file $destdivideMy Document.txtenMyyDocument.txt, así quemvrecibe rutas equivocadas. - Olvidar
nullglob: un glob sin coincidencias se expande a sí mismo, así que el loop corre una vez sobre el literal*.txt. - Usar
catpara alimentar un solo archivo a un programa:cat "$file" | grep patternes un uso inútil decat. Usagrep pattern "$file". - No manejar el caso de no-coincidencia: Un directorio vacío hace que un loop ingenuo corra una vez sobre el literal
*.txt— el mismo fallo quenullglobpreviene.
- Usar
for file in $(find ...)en vez dewhile read. La división de palabras destroza cualquier nombre que contenga espacios. Envíafindpor-print0awhile IFS= read -r -d ''en su lugar:
# Mal: rompe con espacios
# for file in $(find . -name "*.txt"); do echo "$file"; done
# Bien: maneja todos los nombres de archivo
while IFS= read -r -d '' file; do
echo "$file"
done < <(find . -name "*.txt" -print0)
- No usar
set -euo pipefailen scripts. Sinset -e, los errores en el cuerpo del loop se ignoran silenciosamente. Sinset -u, las variables indefinidas se expanden a strings vacíos. Sinpipefail, los comandos pipeados que fallan se enmascaran:
#!/bin/bash
# Mal: los errores se ignoran silenciosamente
# for file in *.txt; do
# process "$file"
# done
# Bien: aborta en error, variable indefinida o fallo de pipe
set -euo pipefail
for file in *.txt; do
[ -e "$file" ] || continue
process "$file"
done
- Modificar archivos mientras se itera. Añadir, eliminar o renombrar archivos durante un loop glob puede causar procesamiento saltado o duplicado. Recolecta la lista de archivos primero, luego itera:
#!/bin/bash
shopt -s nullglob
files=(*.tmp)
shopt -u nullglob
for file in "${files[@]}"; do
# Seguro: ya capturamos la lista
rm -- "$file"
done
Más Patrones de Iteración
¿Cómo itero sobre archivos ordenados por fecha de modificación?
Usa find con -printf y sort, o ls -t con mapfile:
#!/bin/bash
# Ordenar por fecha de modificación (más reciente primero)
mapfile -d '' -t files < <(find . -type f -name '*.log' -printf '%T@ %p\0' | sort -rz -n | cut -z -d' ' -f2-)
for file in "${files[@]}"; do
echo "$(date -r "$file" '+%Y-%m-%d %H:%M') $file"
done
¿Cómo salto archivos mayores a cierto tamaño?
Usa find con filtro -size:
#!/bin/bash
# Procesar solo archivos menores a 10MB
while IFS= read -r -d '' file; do
size=$(stat -c%s "$file")
echo "Procesando $file ($((size / 1024)) KB)"
done < <(find . -type f -name '*.log' -size -10M -print0)
¿Cómo itero sobre archivos con múltiples extensiones?
Usa brace expansion con nullglob, o find con múltiples patrones -name:
#!/bin/bash
shopt -s nullglob
# Brace expansion
for file in *.{jpg,jpeg,png,webp}; do
[ -f "$file" ] || continue
echo "Imagen: $file"
done
# find con múltiples -name
while IFS= read -r -d '' file; do
echo "Imagen: $file"
done < <(find . -type f \( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' -o -name '*.webp' \) -print0)
¿Cómo manejo nombres de archivo con saltos de línea?
Solo find -print0 con read -d '' maneja nombres de archivo que contienen saltos de línea. Los patrones glob también funcionan ya que Bash los expande correctamente. Nunca uses salida de ls o sustitución de comandos sin entrecomillar:
#!/bin/bash
# Esto maneja nombres con saltos de línea, espacios y caracteres especiales
while IFS= read -r -d '' file; do
echo "Procesando: $file"
done < <(find . -type f -print0)
Lectura Adicional
- BashFAQ/020 — How can I loop over files? — la referencia canónica sobre iteración segura de archivos en Bash.
- BashPitfalls — por qué
for f in $(ls)y afines se rompen con nombres de archivo reales. - Manual de GNU Bash — Looping Constructs — semántica de
for,whileyuntil.
Preguntas frecuentes
¿Por qué debo evitar for f in $(ls)?
Falla con nombres de archivo con espacios o caracteres especiales. Usa un patrón glob como for f in *.txt o while IFS= read -r con find -print0.
¿Cuándo debo usar find en lugar de un loop con glob?
Usa find cuando necesites recursión, filtros por tamaño o fecha, o manejo garantizado de nombres arbitrarios con -print0. Un simple for file in *.txt basta para un solo directorio plano.
¿Cómo proceso archivos en subdirectorios?
Ejecuta find . -type f -name "*.txt" -print0 y aliméntalo a while IFS= read -r -d '' file. Los globs como **/*.txt también recorren en Bash 4+ con globstar activado, pero find te da filtros gratis.
¿Por qué mi loop corre una vez con el literal *.txt cuando no hay coincidencias?
Con nullglob desactivado, un glob sin coincidencias se expande a sí mismo. Activa shopt -s nullglob o añade [ -e "$file" ] || continue al inicio del loop.
¿Cómo detengo el loop en el primer error?
Un loop while alimentado por un pipeline corre en un subshell, así que set -e no aborta de forma fiable. Verifica el resultado dentro del cuerpo (cmd "$file" || break) o recolecta la lista primero con mapfile y usa un loop for.
¿Cómo itero solo sobre archivos más antiguos de 30 días?
Añade -mtime +30 a find — el + significa estrictamente más de N días: find . -type f -name '*.log' -mtime +30 -print0 | while IFS= read -r -d '' file; do ... done.
Recursos Relacionados
Ejecución Paralela en Bash
Cómo ejecutar comandos de shell en paralelo con xargs, GNU parallel y trabajos en segundo plano de Bash controlando la concurrencia y recolectando resultados. Incluye manejo de exit codes y un semáforo para APIs con límite de tasa.
RecipeProcesamiento de Texto en Bash: grep, sed y awk
Cómo construir pipelines de procesamiento de texto con grep, sed, awk, cut, sort, uniq y tr para análisis de logs y transformación de datos.
RecipeGenerar Archivos Temporales
Cómo crear archivos y directorios temporales de forma segura con limpieza automática en Python, Node.js, Java y Bash.
RecipeRotación de copias en Bash: retención e integridad
Automatiza la rotación de copias en Bash con archivos diarios, semanales y mensuales, retención, cifrado y verificación de integridad.
RecipeRotación y Compresión de Logs en Bash
Rota, comprime y purga logs de aplicaciones con configuraciones de logrotate y scripts bash personalizados —incluyendo zstd, copytruncate, chequeo de disco y envío a S3.
RecipeMonitorear Uso de Disco
Alerta cuando el espacio en disco cruza umbrales usando un script de Bash que verifica mount points y notifica operadores.