Cerrábamos el Módulo 3 con una promesa: informe-diario.sh dejaría de ser un guion lineal para convertirse en un programa con estructura, y los bucles serían la primera herramienta en llegar. Aquí está. Hasta ahora, cuando querías el informe de Valencia y el de Sevilla, ejecutabas el script dos veces; cuando querías mirar los cuatro ficheros rotados de app.log, copiabas y pegabas la misma tubería cuatro veces cambiando el nombre. Un bucle es exactamente lo contrario: escribes la operación una vez y le das la lista de cosas sobre las que aplicarla. En esta lección verás las cuatro formas de iterar que ofrece Bash, cuándo usar cada una, y el patrón canónico —con sus trampas— para leer un fichero línea a línea.
Contenido
forsobre listas literalesforsobre globs y sobre"$@"forestilo Cwhileyuntil- Cuál elegir: tabla comparativa
- Leer un fichero línea a línea: el patrón canónico
- La trampa de la tubería y el subshell
- Leer un CSV separando campos
breakycontinue, y sus niveles- Bucles infinitos controlados
- Anidamiento, contadores y coste
for sobre listas literales
for sobre listas literalesLa forma más simple de for recorre una lista de palabras separadas por espacios y va asignando cada una a una variable:
Imprime las cuatro líneas, una por ciudad. Desglose de cada pieza:
for ciudad in ...:ciudades la variable de control. No se declara antes ni se borra después: al terminar conserva el último valor (Madrid).- La lista tras
ines una lista de palabras, no una cadena. Bash la genera aplicando las expansiones de 03-06:for n in {1..5}ofor f in $(date +%F)son igual de válidos. do...donedelimitan el cuerpo. El;antes dedosolo hace falta si están en la misma línea.
Cuidado: entrecomillar una cadena con espacios (for c in "$CIUDADES") produce un único elemento, no varios. Para listas de verdad se usan arrays (04-03).
for sobre globs y sobre "$@"
for sobre globs y sobre "$@"Como el globbing (02-05) ocurre antes de ejecutar el comando, un patrón se convierte en la lista de ficheros que existen. Este es el idioma correcto para recorrer ficheros, y la razón por la que en 03-06 insistimos en que nunca se hace for f in $(ls):
for fichero in /var/log/veloz/app.log.*; do
[[ -f "$fichero" ]] || continue # protege del caso "sin coincidencias"
echo "== $fichero: $(wc -l < "$fichero") líneas"
doneLa línea de guarda es importante: si no existe ningún app.log.*, Bash deja el patrón sin expandir y la variable valdrá literalmente /var/log/veloz/app.log.*. Las dos soluciones son la guarda [[ -f ... ]] || continue o activar shopt -s nullglob, que hace que un patrón sin coincidencias produzca una lista vacía y el bucle no se ejecute ninguna vez.
Sin lista in, for recorre implícitamente los argumentos del script, es decir "$@":
Escribir for ciudad in "$@" es más explícito y siempre correcto. Escribir for ciudad in $@ (sin comillas) rompe con cualquier argumento que contenga espacios, por lo que ya sabes de 03-05.
for estilo C
for estilo CCuando lo que necesitas es un contador numérico y no una lista, Bash ofrece la sintaxis de tres expresiones aritméticas heredada de C:
Dentro de los dobles paréntesis estás en contexto aritmético: las variables no llevan $, se comparan con < y > de forma numérica, y i++ funciona. Es el mismo contexto que verás a fondo en 04-06. Las tres expresiones son inicialización, condición de continuación e incremento, y cualquiera puede omitirse (for (( ;; )) es un bucle infinito).
Úsalo cuando el índice importa; para recorrer valores, el for ... in es más legible.
while y until
while y untilwhile repite el cuerpo mientras un comando devuelva código de salida 0, y until hasta que lo devuelva. Nota clave que enlaza con 03-04: la condición no es una expresión booleana, es un comando cuyo $? se evalúa.
intentos=0
while ! curl -sf http://localhost:8080/salud > /dev/null; do
intentos=$(( intentos + 1 ))
(( intentos >= 5 )) && { echo "veloz-api no responde" >&2; exit 4; }
echo "Esperando a veloz-api (intento $intentos)..."
sleep 2
done
echo "veloz-api operativa"El mismo bucle con until elimina la negación y se lee mejor:
Regla práctica: si tu while empieza por !, probablemente querías un until.
- Cuál elegir: tabla comparativa
| Forma | Se usa cuando | Ejemplo típico en Veloz Envíos |
|---|---|---|
for x in lista |
Conoces de antemano los elementos | Las cuatro ciudades, los repartidores |
for (( i=0; i<n; i++ )) |
Necesitas un índice numérico | Recorrer un array por posición, reintentos numerados |
while cond |
Repites mientras algo sea cierto y no sabes cuántas veces | Leer líneas de un log hasta el final |
until cond |
Igual que while, pero la condición natural es la negativa |
Esperar a que la API arranque |
for responde a "para cada uno de estos"; while/until responden a "mientras/hasta que ocurra esto".
- Leer un fichero línea a línea: el patrón canónico
Este es probablemente el fragmento de Bash que más veces escribirás en tu vida. Memorízalo entero:
Cada pieza está ahí por una razón concreta:
IFS=vacía el separador de campos solo para este comando (recuerda de 03-06 queVAR=valor comandoafecta únicamente a ese comando). Sin ello,readrecortaría los espacios y tabuladores del principio y del final de cada línea. ConIFS=la línea llega literal, con su indentación intacta.-rdesactiva la interpretación de la barra invertida. Sin-r, una ruta comoC:\veloz\datosperdería las barras y un\final uniría dos líneas. Salvo que estés implementando un intérprete de escapes, siempre-r.lineaes la variable destino. Si no pones ninguna,readdeposita el contenido enREPLY.< ficheroal final: la redirección se aplica a todo el bucle, que actúa como un único comando compuesto.readva consumiendo esa entrada línea a línea hasta agotarla, momento en que devuelve un código distinto de 0 y elwhiletermina.
Una advertencia poco conocida: si el fichero no acaba en salto de línea, read guarda la última línea pero devuelve código de fallo y el bucle la descarta. El seguro es while IFS= read -r linea || [[ -n "$linea" ]]; do ... done < fichero.
- La trampa de la tubería y el subshell
Es tentador escribir el bucle anterior alimentándolo con una tubería. Funciona… hasta que intentas usar una variable después:
errores=0
grep '\[ERROR\]' /var/log/veloz/app.log | while IFS= read -r linea; do
errores=$(( errores + 1 ))
done
echo "Errores: $errores" # imprime 0, no el total realLa explicación viene directa de 01-04: cada tramo de una tubería se ejecuta en un subshell, un proceso hijo con su propia copia de las variables. El bucle incrementa errores de verdad, pero en la copia del hijo; cuando el hijo muere, el valor muere con él y el padre sigue viendo su 0 original.
flowchart LR
A["Shell padre<br/>errores=0"] -->|"| tubería"| B["Subshell<br/>errores=0→1→2"]
B -.->|"el hijo muere<br/>y se lleva el valor"| C["Shell padre<br/>errores=0"]
A ==>|"< redirección"| D["Mismo shell<br/>errores=2 ✓"]
Las dos soluciones correctas:
# A) Redirección al final: el bucle corre en el shell actual
errores=0
while IFS= read -r linea; do errores=$(( errores + 1 )); done \
< <(grep '\[ERROR\]' /var/log/veloz/app.log)
# B) Sustitución de procesos con un fichero real
while IFS= read -r linea; do ...; done < /var/log/veloz/app.logLa construcción < <(comando) se llama sustitución de procesos: convierte la salida de un comando en algo que se puede redirigir como si fuera un fichero, sin crear ningún subshell para el bucle. Fíjate en el espacio obligatorio entre los dos <.
- Leer un CSV separando campos
read acepta varias variables y reparte los campos según IFS. Fijando IFS=, obtienes un lector de CSV en una línea:
{
read -r _cabecera # descarta la primera línea
while IFS=, read -r id fecha ciudad repartidor estado importe; do
[[ "$estado" == "incidencia" ]] || continue
printf '%-8s %-10s %-10s %s\n' "$id" "$ciudad" "$repartidor" "$importe"
done
} < /srv/veloz/datos/envios.csvSalida: E1043 Sevilla mgarcia 31.20, una línea por incidencia. Tres detalles importantes:
- El
read -r _cabecerainicial consume la líneaid_envio,fecha,.... Como está dentro del mismo bloque{ ... } < fichero, comparte la entrada con el bucle. - Si hay más campos que variables, la última variable recibe todo el resto. Por eso
importe, siendo la última, se queda con lo que sobre. Y si hay menos, las variables finales quedan vacías. - Este lector no entiende comillas ni comas dentro de campos. Para CSV complejos se usa
awk(06-01); para el nuestro, es perfecto.
break y continue, y sus niveles
break y continue, y sus nivelesbreak sale del bucle; continue salta a la siguiente iteración. Ambos aceptan un número que indica cuántos niveles de anidamiento afectan:
for ciudad in Valencia Sevilla Bilbao Madrid; do
for repartidor in alopez mgarcia jruiz; do
if [[ ! -r "$RUTA_CSV" ]]; then
echo "CSV ilegible, abortando todo" >&2
break 2 # sale de LOS DOS bucles
fi
[[ "$repartidor" == "jruiz" && "$ciudad" == "Madrid" ]] && continue
echo "$ciudad / $repartidor"
done
donebreak sin número equivale a break 1 y solo saldría del bucle interno. Los niveles se cuentan de dentro hacia fuera. Úsalos con moderación: un break 3 es señal de que ese bloque pide ser una función con un return (04-02).
- Bucles infinitos controlados
Un bucle sin condición de salida es una herramienta legítima cuando el script es un vigilante:
while true; do
ahora=$(date '+%F %T')
errores=$(grep -c '\[ERROR\]' /var/log/veloz/app.log)
printf '%s errores acumulados: %s\n' "$ahora" "$errores"
sleep 60
donetrue es un comando que no hace nada y siempre devuelve 0, así que la condición nunca falla. Reglas de seguridad:
- Siempre un
sleepdentro. Sin él consumirás una CPU al 100% sin ganar nada. - Siempre una vía de salida: un
breakbajo alguna condición, o al menos queCtrl+Clo mate (por defecto lo hace). - Si el bucle va a vivir para siempre, lo correcto no es un
while truelanzado a mano, sino un temporizador del sistema. Lo verás en 07-04 y 07-05.
- Anidamiento, contadores y coste
Anidar bucles multiplica el trabajo: 4 ciudades × 3 repartidores son 12 iteraciones, y si dentro haces grep sobre un CSV de 50.000 líneas, son 12 lecturas completas. El coste real en Bash casi nunca está en el bucle, sino en los procesos externos que se lanzan dentro de él: un for de 4 ciudades con dos grep cada una son 8 procesos y 8 recorridos del fichero.
Una sola pasada acumulando contadores es mucho más rápida, pero para eso necesitas un contador por ciudad: eso son arrays (04-03), y el porcentaje con decimales es aritmética (04-06). Por ahora, el contador simple, que sí puedes escribir ya:
total=0; incidencias=0
while IFS=, read -r _ _ ciudad _ estado _; do
(( total++ ))
[[ "$estado" == "incidencia" ]] && (( incidencias++ ))
done < <(tail -n +2 "$RUTA_CSV")
echo "$incidencias incidencias de $total envíos"El _ es un nombre de variable convencional para "este campo no me interesa"; no tiene ningún significado especial en Bash, es solo una costumbre legible.
Errores Comunes y Consejos
for f in $(ls *.csv). Ya avisado en 03-06, pero se repite: rompe con espacios en los nombres. Usafor f in *.csv.- Olvidar
IFS=o-renread. Funcionará durante meses y fallará el día que aparezca una ruta con\o una línea indentada. - Contar dentro de una tubería. El contador siempre saldrá a 0. Redirección o
< <(...), nuncacmd | while. - Bucle sobre un glob que no coincide. Añade la guarda
[[ -e "$f" ]] || continueoshopt -s nullglob. - Modificar la lista mientras se recorre. La lista de
forse calcula una sola vez, al principio; borrar ficheros dentro del bucle no cambia lo que queda por recorrer. - Consejo: cuando un bucle supere unas 15 líneas, extrae el cuerpo a una función. Es exactamente lo que harás en la siguiente lección.
Ejercicios
Ejercicio 1. Escribe un bucle que recorra las cuatro ciudades y, para cada una, imprima cuántas líneas del CSV le corresponden, en formato Valencia: 128 envíos.
Ejercicio 2. Recorre los ficheros rotados /var/log/veloz/app.log.* y muestra por cada uno su nombre y el número de líneas [ERROR] que contiene, saltando los que no existan o estén vacíos.
Ejercicio 3. Lee envios.csv línea a línea (sin cabecera) y cuenta cuántos envíos ha hecho alopez y cuántos de ellos acabaron en incidencia. Imprime ambos números después del bucle: deben salir correctos.
Soluciones
Solución 1.
RUTA_CSV="/srv/veloz/datos/envios.csv"
for ciudad in Valencia Sevilla Bilbao Madrid; do
n=$(grep -c ",${ciudad}," "$RUTA_CSV")
echo "${ciudad}: ${n} envíos"
doneEl patrón ,${ciudad}, con comas a ambos lados evita falsos positivos si una ciudad fuera subcadena de otra.
Solución 2.
for f in /var/log/veloz/app.log.*; do
[[ -f "$f" && -s "$f" ]] || continue # -s: existe y no está vacío
printf '%-32s %4d errores\n' "$f" "$(grep -c '\[ERROR\]' "$f")"
done-s (visto en 03-03) descarta de un golpe los ficheros vacíos y el patrón sin expandir.
Solución 3.
envios=0; incidencias=0
while IFS=, read -r _ _ _ repartidor estado _; do
[[ "$repartidor" == "alopez" ]] || continue
(( envios++ ))
[[ "$estado" == "incidencia" ]] && (( incidencias++ ))
done < <(tail -n +2 /srv/veloz/datos/envios.csv)
echo "alopez: ${envios} envíos, ${incidencias} incidencias"La clave está en < <(tail ...): con tail ... | while los dos contadores saldrían a 0. Nota además el || continue en lugar de un if envolvente: reduce un nivel de indentación y se lee igual de bien.
Conclusión
Ya sabes repetir. for recorre listas conocidas —literales, globs o "$@"—, su variante estilo C recorre índices, y while/until repiten mientras un comando siga devolviendo el código que esperas. El patrón while IFS= read -r linea; do ... done < fichero es el lector de ficheros oficial de Bash, y la regla que lo acompaña —redirección al final, nunca una tubería— te ahorrará el bug más frustrante del lenguaje, ese en el que un contador siempre vale 0 porque vivía en un subshell.
Pero fíjate en lo que ha pasado en los últimos ejemplos: dentro de cada bucle repetíamos las mismas cinco líneas de validación y el mismo formato de salida, solo que con otra ciudad. Hemos eliminado la repetición entre invocaciones del script, pero no dentro de él. La herramienta que resuelve eso son las funciones (04-02): bloques de código con nombre, con sus propios argumentos y sus propias variables, que convertirán validar_entorno, contar_errores y resumen_ciudad en piezas que se escriben una vez y se llaman muchas.
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
