Loop 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.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripción General
Iterar sobre archivos es una de las operaciones más comunes en Bash, pero frecuentemente se hace incorrectamente. Los nombres de archivo con espacios, saltos de línea o caracteres glob (*, ?) rompen los loops ingenuos. Aqui se muestra la forma de patrones seguros y portables para iterar archivos, filtrar por extensión, recursar subdirectorios y procesar resultados.
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 en batch
- Generar reportes desde un directorio de archivos de entrada
- Reemplazar texto a través de múltiples archivos
Cuándo NO Usar
- Procesar millones de archivos — los límites de longitud de lista de argumentos (
ARG_MAX) fallarán - Filtrado complejo que es más fácil 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
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 "Linting: $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
# Orden numérico en nombres como report_001.txt, report_002.txt
for file in $(ls -1 report_*.txt | sort -t_ -k2 -n); do
echo "Procesando en orden: $file"
done
# Alternativa más segura usando array + glob
files=(report_*.txt)
IFS=$'\n' sorted=($(sort -t_ -k2 -n <<< "${files[*]}")); unset IFS
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 en Batch
#!/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
Lo que funciona
- Siempre entrecomilla variables de archivo.
"$file"previene el splitting de palabras en espacios y la interpretación de caracteres glob. - Usa
find -print0 | while read -r -d ''para filtrado recursivo o complejo. Es la única forma portable de manejar todos los nombres de archivo válidos. - 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.mv -- "$file" "$dest"previene que nombres que empiezan con-sean interpretados como opciones. - Verifica
[ -e "$file" ]al inicio del loop. Maneja tantonullglobdeshabilitado como directorios vacíos.
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 $destfalla conMy Document.txtporque se divide en dos argumentos. - Olvidar
nullglob: El cuerpo del loop corre una vez con*.txtcomo nombre de archivo cuando no hay coincidencias. - 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 con un loop ingenuo puede producir comportamiento inesperado o errores.
Mejores Prácticas Adicionales
- For a deeper guide, see Bash Parallel Execution.
- 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. Maneja batching 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= previene trimming de espacios iniciales/finales
echo "My File.txt" | while IFS= read -r file; do echo "$file"; done
# Salida: "My File.txt"
Errores Comunes Adicionales
- Usar
for file in $(find ...)en vez dewhile read. El loopfordivide en espacios, rompiendo nombres con espacios. Siempre pipeafind -print0awhile IFS= read -r -d '':
# 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 Related Resources
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.
RecipeProcesamiento de Texto en Bash
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.
Preguntas frecuentes
- ¿Por qué debo evitar for f in $(ls)?
- Fallan con nombres de archivo que contienen 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, filtrado por tamaño o fecha, o cuando debas manejar nombres de archivo arbitrarios de forma segura con -print0.
- ¿Cómo proceso archivos en subdirectorios?
- Usa find . -type f -name "*.txt" -print0 | while IFS= read -r -d file; do ... done para recorrer directorios anidados de forma segura.