Cualquiera que programe en shell conoce while. Casi nadie usa a su hermano
gemelo: until. Es el mismo bucle con la condición invertida — y en el contexto
adecuado se lee como una frase en castellano, mientras que while ! se lee como
un pequeño rompecabezas.
Sintaxis
until CONDICIÓN; do
CUERPO
done
El bucle ejecuta el cuerpo repetidamente hasta que CONDICIÓN tenga éxito
(es decir, termine con estado 0). Dicho de otra forma: sigue iterando mientras
la condición falle.
Compara:
# while: itera mientras la condición sea verdadera
while ! pgrep -x redis-server >/dev/null; do sleep 1; done
# until: itera hasta que la condición se vuelva verdadera
until pgrep -x redis-server >/dev/null; do sleep 1; done
Mismo comportamiento. La segunda se lee como lo describirías en voz alta:
espera hasta que Redis esté corriendo. Ese es todo el argumento a favor de
until — elige la forma que no obligue a quien lee a invertir un booleano
mentalmente.
Cinco ejemplos prácticos
1. Esperar a que un servicio esté listo
Arrancas un contenedor y luego esperas a que su endpoint HTTP responda:
docker compose up -d api
until curl -sf http://localhost:8080/health >/dev/null; do
sleep 1
done
echo "API lista"
curl -sf devuelve un estado distinto de cero ante conexiones rechazadas,
tiempos de espera o respuestas no 2xx — todos los estados en los que "listo" aún
es falso.
2. Esperar a que aparezca un archivo
Útil cuando otro proceso debe dejar un marcador:
until [ -f /tmp/import.done ]; do
sleep 0.5
done
process_the_import3. Reintentar un comando inestable, con un tope
Los bucles infinitos son una trampa. Empareja siempre until con un máximo:
intento=0
max=10
until curl -sf https://example.com/api/data -o data.json; do
intento=$((intento + 1))
if [ "$intento" -ge "$max" ]; then
echo "me rindo tras $intento intentos" >&2
exit 1
fi
sleep $((intento * 2)) # retroceso lineal
done4. Esperar a que un puerto TCP esté abierto
Antes de lanzar las migraciones contra un Postgres recién arrancado:
until nc -z localhost 5432; do
sleep 1
done
psql -h localhost -U app -d app -f migrations.sql
nc -z (o nc -zv si prefieres salida detallada) sondea el puerto sin enviar
datos.
5. Bloquear hasta que un trabajo en segundo plano termine
kill -0 "$pid" no mata nada — solo pregunta "¿este pid sigue vivo?" (termina
con 0 si sí).
long_running_job &
pid=$!
# Haz otras cosas aquí si hace falta, luego bloquea:
until ! kill -0 "$pid" 2>/dev/null; do
sleep 1
done
wait "$pid" # recupera el estado de salida
Normalmente wait "$pid" es lo que realmente quieres, pero el patrón de arriba
es útil cuando necesitas vigilar alguna otra condición en paralelo (un
indicador, una fecha límite).
Trampas
Ten siempre una salida. Un until sin tiempo límite y sin contador es un
script que se cuelga para siempre cuando su dependencia no llega nunca. Usa
timeout, un contador, o ambos:
timeout 60 bash -c 'until curl -sf http://localhost:8080/health; do sleep 1; done'
set -e no rompe el bucle si falla el cuerpo. Los comandos dentro del
bloque do/done que fallen no abortan el bucle — solo la condición influye en
la iteración. Si necesitas fallar rápido a mitad del cuerpo, comprueba
explícitamente y usa exit o break.
Portabilidad. until es POSIX. Funciona en bash, zsh, dash, ksh y
ash (BusyBox). Si tu script empieza con #!/bin/sh, no hay problema.
No inviertas solo por estilo. Si la manera natural de describir el bucle es
"mientras X sigue ocurriendo", usa while. until se gana su sitio solo cuando
la condición de éxito es la que resulta obvia nombrar.
Por qué vale la pena ponerlo en la memoria muscular
until no hace nada que while no pueda hacer. Solo produce, de vez en cuando,
código que se entiende de un vistazo en lugar de tras negar mentalmente una
condición. Para una palabra clave tan pequeña, es un intercambio justo. La
próxima vez que estés a punto de escribir while !, detente medio segundo y
pregúntate si la otra forma no se lee mejor. A menudo sí.