1. La forma de while
while ejecuta un comando de condición y, si termina con éxito, ejecuta el cuerpo; después vuelve a evaluar la condición, y así sucesivamente, hasta que la condición falle.
while condición; do
comandos
done
Como en if, la condición es un comando cualquiera, y lo que importa es su código de salida (tema 10): mientras sea 0, el bucle sigue. Puede ser una prueba con [[ ]], una comparación con (( )) o cualquier programa.
- Se ejecuta la condición. cada vuelta
- Si su código es distinto de 0, el bucle termina y el script sigue después de
done. salida - Si es 0, se ejecuta el cuerpo y se vuelve al paso 1. cada vuelta
Si la condición falla desde el principio, el cuerpo no se ejecuta nunca. Y como la condición se vuelve a evaluar en cada vuelta, algo en el cuerpo, o fuera del script, tiene que cambiar para que alguna vez falle. Si nada cambia, el bucle no termina.
2. until: hasta que se cumpla
until es el mismo bucle con la condición invertida: repite mientras la condición falle, y termina cuando tiene éxito. Se lee «hasta que».
until [[ -e listo.flag ]]; do # hasta que exista el archivo
sleep 1
done
while [[ ! -e listo.flag ]]; do # mientras no exista
sleep 1
done
Las dos hacen exactamente lo mismo. until evita la negación, y suele leerse mejor cuando lo que se espera es un éxito: «hasta que el servidor responda», «hasta que el comando funcione». Dentro del cuerpo de un until, $? contiene todavía el código de la condición que falló, lo que permite saber por qué falló, como veremos en la sección 11.
3. Contadores
La forma más simple de controlar un while es un contador que cambia en cada vuelta:
$ i=1
$ while (( i <= 3 )); do echo "Vuelta $i"; i=$(( i + 1 )); done
Vuelta 1
Vuelta 2
Vuelta 3
Para contar de un número a otro, el for al estilo C del tema 14 es más compacto, porque reúne el inicio, la condición y el incremento en una línea, y no hay riesgo de olvidar el incremento. El while con contador conviene cuando el contador no avanza en todas las vueltas, o cuando hay otras razones para terminar además de llegar al límite, como en los ejemplos que siguen.
4. Comandos como condición
La fuerza de while y until está en que la condición puede ser cualquier comando, y el bucle reacciona a lo que pasa fuera del script:
until ping -c1 -W2 servidor > /dev/null; do # hasta que el servidor responda
sleep 5
done
while pgrep -x rsync > /dev/null; do # mientras haya un rsync en ejecución
echo "Esperando a que termine la copia anterior..."
sleep 30
done
while ! mkdir /tmp/mi-script.lock 2> /dev/null; do # hasta conseguir el bloqueo
sleep 1
done
El último ejemplo usa un truco clásico: mkdir falla si el directorio ya existe, y crearlo es una operación atómica, de modo que dos procesos nunca pueden crearlo a la vez. El primero que lo consigue tiene el «bloqueo», y los demás esperan. Veremos una forma más robusta, con flock, en el tema 27.
5. Bucles infinitos a propósito
while true es un bucle cuya condición nunca falla. Tiene dos usos legítimos: programas que deben correr hasta que alguien los detenga, como un monitor que revisa algo cada minuto, y bucles cuya condición de salida está en medio del cuerpo, y no al comienzo.
while true; do
uso=$(df --output=pcent / | tail -1 | tr -dc '0-9')
(( uso >= 90 )) && echo "$(date +%T) disco al $uso %" >&2
sleep 60
done
while true; do
read -rp "Nombre del servidor: " servidor
[[ -n "$servidor" ]] && break # válido: salir del bucle
echo "El nombre no puede estar vacío." >&2
done
El segundo patrón lo usamos en el tema 9 para validar respuestas. break termina el bucle en el punto en que se ejecuta, y lo estudiaremos con detalle en el tema 17. Un bucle infinito en la terminal se detiene con Ctrl+C; uno que corre como servicio, con kill.
6. Esperar a que algo esté listo
Una situación muy frecuente en automatización: un script pone en marcha algo, como un servidor, una base de datos o un contenedor, y debe esperar a que esté listo antes de continuar. Esperar un tiempo fijo con sleep 10 es frágil: si arranca en dos segundos, se pierden ocho; si tarda doce, el script sigue antes de tiempo y falla. Lo correcto es preguntar repetidamente hasta que esté listo, lo que se llama sondeo (polling):
systemctl start postgresql
until pg_isready -q; do
sleep 1
done
echo "La base de datos está lista"
Cada vuelta hace una pregunta barata y, si la respuesta es «todavía no», espera un poco. El intervalo es un compromiso: muy corto desperdicia recursos y puede saturar lo que se consulta; muy largo agrega demora innecesaria. Uno o dos segundos suelen ser razonables para algo que tarda segundos en estar listo; un minuto, para algo que tarda horas.
Bash permite incluso comprobar si un puerto TCP acepta conexiones sin ningún programa externo, abriendo el archivo especial /dev/tcp/HOST/PUERTO, que Bash interpreta como una conexión de red:
until (exec 3<>"/dev/tcp/localhost/8080") 2> /dev/null; do
sleep 1
done
echo "El servicio escucha en el puerto 8080"
Los paréntesis ejecutan la prueba en una subshell, de modo que la conexión abierta se cierra sola al terminar, y la redirección de errores oculta los mensajes de conexión rechazada mientras se espera.
7. Siempre con un límite
Los bucles de las dos secciones anteriores tienen un problema serio: si lo que se espera nunca ocurre, no terminan. Un servidor que no arranca por un error de configuración deja al script esperando para siempre, y si lo ejecuta cron, cada día se acumula una copia más esperando. En automatización, toda espera debe tener un límite, de tiempo o de intentos, y al alcanzarlo el script debe fallar con un mensaje claro.
La variable SECONDS del tema 5, que cuenta los segundos desde que empezó el script, hace muy simple el límite de tiempo:
limite=30
inicio=$SECONDS
until pg_isready -q; do
if (( SECONDS - inicio >= limite )); then
echo "La base de datos no respondió en $limite segundos" >&2
exit 1
fi
sleep 1
done
echo "Lista en $(( SECONDS - inicio )) segundos"
Y un límite de intentos se expresa con un contador, que puede ir directamente en la condición:
intento=1
until descargar-datos; do
if (( intento == 5 )); then
echo "La descarga falló cinco veces" >&2
exit 1
fi
intento=$(( intento + 1 ))
sleep 10
done
8. El comando timeout
El límite de la sección anterior protege la espera entre intentos, pero no un intento que se cuelga: si descargar-datos se queda bloqueado esperando una red que no responde, el bucle nunca llega a comprobar el contador. Para eso existe timeout, que ejecuta un comando y lo interrumpe si tarda más de lo indicado:
$ timeout 5 sleep 60; echo "código: $?"
código: 124
$ timeout 30s rsync -a origen/ servidor:/destino/ # también admite s, m, h y d
Si el comando termina a tiempo, timeout devuelve su código de salida; si se agota el tiempo, lo interrumpe y devuelve 124. Con la opción -k envía además la señal KILL un tiempo después, para los programas que ignoran la interrupción normal: timeout -k 10 60 comando. Combinado con un bucle, cubre los dos riesgos: cada intento tiene un tiempo máximo, y la cantidad de intentos también.
9. Reintentos con esperas crecientes
Muchos fallos son pasajeros: una red congestionada, un servidor que se está reiniciando, un servicio que rechaza pedidos por exceso de carga. Reintentar es la respuesta correcta, pero reintentar inmediatamente y muchas veces seguidas empeora justamente el último caso, porque agrega carga a un servicio que ya está saturado.
La técnica habitual es la espera exponencial (exponential backoff): después de cada fallo se espera el doble que la vez anterior, 2, 4, 8, 16 segundos, hasta un máximo. Los primeros reintentos son rápidos, por si el fallo fue momentáneo, y los siguientes se espacian, para dar tiempo a que el problema se resuelva.
| Intento | Espera fija de 5 s | Espera exponencial desde 2 s (máx. 60) |
|---|---|---|
| 1 → 2 | 5 s | 2 s |
| 2 → 3 | 5 s | 4 s |
| 3 → 4 | 5 s | 8 s |
| 4 → 5 | 5 s | 16 s |
| 5 → 6 | 5 s | 32 s |
| 6 → 7 | 5 s | 60 s |
Cuando muchos clientes reintentan contra el mismo servidor, se suele agregar una pequeña cantidad al azar a cada espera, para que no vuelvan a intentarlo todos en el mismo instante: sleep $(( espera + RANDOM % 3 )). El script de la sección 11 implementa la espera exponencial de forma general, para cualquier comando.
10. while, until o for
| Situación | Conviene |
|---|---|
| Recorrer una lista conocida: archivos, argumentos, elementos de un array | for … in |
| Contar de un número a otro | for (( … )) |
| Repetir mientras algo sea cierto, sin saber cuántas vueltas | while |
| Repetir hasta que algo funcione o esté listo | until |
| Repetir sin fin, o con la salida en medio del cuerpo | while true con break |
| Leer un archivo o una salida línea por línea | while read (tema 16) |
11. Un script completo
Muchos scripts necesitan reintentar algo: una descarga, una conexión, una copia a un servidor. En lugar de escribir el bucle cada vez, este script lo hace para cualquier comando, que se le pasa como argumentos. Si el comando falla, lo reintenta con esperas crecientes, y si falla todas las veces, termina con el código del último intento.
#!/usr/bin/env bash
#
# reintentar — ejecuta un comando hasta que funcione, con esperas crecientes.
#
# Uso: reintentar COMANDO [ARGUMENTOS...]
# Entorno: INTENTOS máximo de intentos (5)
# ESPERA segundos de espera tras el primer fallo (2); se duplica
# ESPERA_MAX espera máxima entre intentos (60)
# Salida: 0 si el comando funcionó; si no, el código del último intento.
#
set -euo pipefail
if (( $# == 0 )); then
echo "Uso: ${0##*/} COMANDO [ARGUMENTOS...]" >&2
exit 2
fi
intentos="${INTENTOS:-5}"
espera="${ESPERA:-2}"
espera_max="${ESPERA_MAX:-60}"
intento=1
until "$@"; do
codigo=$? # el código del comando que falló
if (( intento >= intentos )); then
echo "${0##*/}: falló $intento veces; me rindo (código $codigo)" >&2
exit "$codigo"
fi
echo "${0##*/}: intento $intento falló (código $codigo); reintento en ${espera} s" >&2
sleep "$espera"
intento=$(( intento + 1 ))
espera=$(( espera * 2 > espera_max ? espera_max : espera * 2 ))
done
$ INTENTOS=3 ESPERA=1 reintentar ls /no/existe; echo "código $?"
ls: cannot access '/no/existe': No such file or directory
reintentar: intento 1 falló (código 2); reintento en 1 s
ls: cannot access '/no/existe': No such file or directory
reintentar: intento 2 falló (código 2); reintento en 2 s
ls: cannot access '/no/existe': No such file or directory
reintentar: falló 3 veces; me rindo (código 2)
código 2
$ reintentar curl -fsS -o datos.json https://api.example.com/datos
curl: (7) Failed to connect to api.example.com port 443
reintentar: intento 1 falló (código 7); reintento en 2 s
$ reintentar timeout 60 rsync -a proyecto/ respaldo:/copias/ # cada intento, como máximo un minuto
until "$@"ejecuta el comando recibido, con sus argumentos intactos:"$@"son todos los argumentos del script, cada uno como palabra separada, como veremos en el tema 20. El script no necesita saber qué comando es.codigo=$?al comienzo del cuerpo guarda el código del intento fallido, que en el cuerpo de ununtiltodavía es el de la condición. El script termina con ese mismo código, de modo que quien lo llama recibe la misma información que habría recibido del comando original.- La espera se duplica con un tope, con el operador ternario del tema 7: si el doble supera el máximo, se usa el máximo.
- Los mensajes van a la salida de errores, para no mezclarse con lo que el comando escriba en la salida estándar, que puede ser justamente el dato que se quería obtener.
- Combinado con
timeout, como en el último ejemplo, cubre tanto los fallos rápidos como los intentos que se cuelgan.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
while (( i < 10 )); do …; done sin incrementar i | El bucle no termina nunca. | Actualizar la variable en el cuerpo, o usar for (( )). |
until servicio_listo; do sleep 1; done | Si el servicio nunca arranca, espera para siempre. | Un límite con SECONDS o con un contador. |
sleep 10 para esperar un servicio | Demasiado o demasiado poco, según el día. | Sondear hasta que esté listo, con límite. |
Un intento sin timeout | Un comando colgado bloquea el bucle y su límite. | timeout N comando |
| Reintentar sin espera | Cientos de intentos por segundo contra un servicio caído. | sleep entre intentos, idealmente creciente. |
while true sin break ni exit | Solo se detiene con Ctrl+C o kill. | Correcto solo para monitores que deben correr siempre. |
echo "$?" después de un if en el cuerpo | Muestra el código del if, no el de la condición del bucle. | Guardar codigo=$? en la primera línea del cuerpo. |
13. Resumen
while condiciónrepite el cuerpo mientras la condición termine con éxito;until condición, mientras falle.- La condición es cualquier comando: una prueba, una comparación aritmética o un programa que pregunta por el mundo exterior.
- Algo debe cambiar en cada vuelta para que el bucle termine: un contador, un archivo, el estado de un servicio.
while trueconbreaksirve para bucles con la salida en medio del cuerpo, y para monitores que corren indefinidamente.- Para esperar a que algo esté listo se sondea con un intervalo razonable, en lugar de esperar un tiempo fijo.
- Toda espera necesita un límite, de tiempo con
SECONDSo de intentos con un contador, y fallar con un mensaje al alcanzarlo. timeout N comandointerrumpe un comando que tarda demasiado, y devuelve 124 en ese caso.- Los reintentos esperan cada vez más, hasta un máximo, para no saturar un servicio con problemas.
- En el cuerpo de un
until,$?es el código de la condición que acaba de fallar. forrecorre listas conocidas;whileyuntilrepiten según una condición.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué diferencia hay entre while y until?
while repite mientras la condición tenga éxito; until, mientras falle. until c equivale a while ! c.
2. ¿Cuántas veces se ejecuta el cuerpo de while false; do …; done?
Ninguna. La condición se evalúa antes de cada vuelta, y falla desde el principio.
3. ¿Por qué esperar a un servicio con sleep 10 es frágil?
Porque el tiempo de arranque varía. Si el servicio arranca antes, se pierde tiempo; si tarda más, el script continúa antes de que esté listo y falla. Es mejor preguntar repetidamente si está listo.
4. ¿Por qué toda espera de un script debe tener un límite?
Porque si lo que se espera nunca ocurre, el script quedaría bloqueado para siempre, sin informar el problema. Con un límite, falla con un mensaje y un código de salida que permiten detectarlo.
5. ¿Cómo se limita a 60 segundos un bucle de espera?
Guardando inicio=$SECONDS antes del bucle y comprobando en cada vuelta si (( SECONDS - inicio >= 60 )); si es así, se termina con un error.
6. ¿Qué problema resuelve timeout que un límite en el bucle no resuelve?
El de un intento que se cuelga. Un límite en el bucle se comprueba entre intentos; si un intento nunca termina, nunca se llega a comprobarlo. timeout interrumpe el comando mismo.
7. ¿Qué código devuelve timeout cuando interrumpe el comando?
124. Si el comando termina a tiempo, devuelve el código del comando.
8. ¿Qué es la espera exponencial, y por qué se usa?
Duplicar la espera después de cada fallo, hasta un máximo. Los primeros reintentos son rápidos, por si el fallo fue momentáneo, y los siguientes se espacian para no saturar un servicio que tiene problemas.
9. En el cuerpo de un until, ¿qué contiene $? en la primera línea?
El código de salida de la condición, que acaba de fallar. Hay que guardarlo enseguida si se lo necesita, porque el siguiente comando lo reemplaza.
10. ¿Por qué reintentar termina con el código del último intento y no con 1?
Para que quien lo llama reciba la misma información que habría recibido del comando original, y pueda distinguir, por ejemplo, un error de red de uno de permisos.
11. ¿Cuándo conviene while true?
Cuando el bucle debe correr indefinidamente, como un monitor, o cuando la condición de salida se conoce recién en medio del cuerpo, y se sale con break.
Para practicar
- Escribe un bucle
whileque muestre las potencias de 2 menores que 1000. - Escribe un script que espere hasta que exista un archivo dado como argumento, con un límite de 20 segundos. Pruébalo creando el archivo con
touchdesde otra terminal. - Lanza
python3 -m http.server 8000en segundo plano y escribe un bucle que espere, con límite, a que el puerto 8000 acepte conexiones usando/dev/tcp. - Comprueba el código de salida de
timeout 2 sleep 5y detimeout 5 sleep 2. - Escribe un monitor con
while trueque muestre cada cinco segundos la hora y la carga del sistema (primera columna de/proc/loadavg). Detenlo con Ctrl+C. - Instala
reintentary pruébalo con un comando que siempre falla, uno que siempre funciona, y un script que falle las dos primeras veces y funcione la tercera (puedes usar un archivo como contador). - Agrega a
reintentaruna pequeña espera aleatoria, de 0 a 2 segundos, sumada a cada espera.