← Automatización con Scripts en Bash Bloque 4 · Bucles · Tema 17 de 42

Tema 17

Controlar los bucles

Un bucle no siempre debe recorrer todos los elementos de principio a fin. A veces un elemento no interesa y hay que pasar al siguiente; a veces ya se encontró lo que se buscaba y no tiene sentido seguir. continue y break resuelven esos dos casos, y ya los usamos varias veces sin detenernos en ellos. Este tema los explica, junto con select, la forma que tiene Bash de construir menús interactivos en una línea.

1. continue: saltar a la vuelta siguiente

continue abandona la vuelta actual y pasa directamente a la siguiente. Lo que queda del cuerpo no se ejecuta para ese elemento. En un for, se toma el elemento siguiente; en un while o un until, se vuelve a evaluar la condición.

terminal
$ for i in 1 2 3 4 5; do (( i == 3 )) && continue; echo -n "$i "; done; echo
1 2 4 5

Su uso más habitual es el de una cláusula de guarda dentro del bucle, la misma idea del tema 12 aplicada a cada vuelta: al comienzo del cuerpo se descartan los elementos que no deben procesarse, y el resto del cuerpo puede asumir que el elemento es válido.

descartar al comienzo, procesar después
for archivo in "$dir"/*; do
  [[ -f "$archivo" ]] || continue          # solo archivos comunes
  [[ -s "$archivo" ]] || continue          # que no estén vacíos
  [[ "$archivo" == *.tmp ]] && continue    # ni temporales

  gzip -- "$archivo"
done

Es preferible a envolver todo el cuerpo en un if con varias condiciones, por la misma razón que en el tema 12: cada descarte queda en su propia línea, con su motivo, y el trabajo real queda sin sangría extra.

2. break: terminar el bucle

break termina el bucle por completo, en el punto en que se ejecuta. El script sigue con lo que viene después de done.

terminal
$ for i in 1 2 3 4 5; do (( i == 4 )) && break; echo -n "$i "; done; echo "fin"
1 2 3 fin

Tiene dos usos principales. El primero, detener una búsqueda en cuanto se encuentra lo que se buscaba, que veremos en la sección 6. El segundo, salir de un bucle while true cuya condición de salida está en medio del cuerpo, como en el tema 15:

repetir hasta recibir algo válido
while true; do
  read -rp "Puerto (1-65535): " puerto
  if [[ "$puerto" =~ ^[0-9]+$ ]] && (( puerto >= 1 && puerto <= 65535 )); then
    break
  fi
  echo "Puerto no válido: $puerto" >&2
done
echo "Usaremos el puerto $puerto"

Después de break, la variable del bucle y cualquier otra conservan el valor que tenían en ese momento. Por eso, en el ejemplo, puerto contiene la respuesta válida al salir del bucle.

3. Bucles anidados: break 2 y continue 2

Cuando hay un bucle dentro de otro, break y continue afectan solo al más interno. Para actuar sobre un bucle exterior se les pasa un número: break 2 termina el bucle interno y el que lo contiene; continue 2 abandona el interno y pasa a la vuelta siguiente del exterior.

terminal
$ for a in 1 2; do for b in x y z; do [[ $b == y ]] && continue 2; echo -n "$a$b "; done; done; echo
1x 2x
$ for a in 1 2; do for b in x y; do [[ $a == 2 ]] && break 2; echo -n "$a$b "; done; done; echo
1x 1y

El caso típico es una búsqueda en dos niveles, como buscar un archivo en varios servidores: en cuanto aparece en uno, no hace falta seguir ni con ese servidor ni con los demás.

buscar un archivo de configuración en varios lugares
encontrado=""
for dir in "$HOME/.config/app" /etc/app /usr/local/etc/app; do
  for nombre in app.conf config.ini; do
    if [[ -r "$dir/$nombre" ]]; then
      encontrado="$dir/$nombre"
      break 2                       # el primero que aparece gana
    fi
  done
done

[[ -n "$encontrado" ]] || { echo "No se encontró la configuración" >&2; exit 1; }
echo "Usando $encontrado"

El orden de la lista de directorios define la prioridad: la configuración del usuario antes que la del sistema. Más de dos niveles de anidamiento, o un break 3, suelen indicar que convendría sacar el bucle interior a una función, que puede terminar con return (tema 18).

4. break, return o exit

Tres comandos interrumpen la ejecución, cada uno a un nivel distinto:

ComandoTerminaLa ejecución sigue
continueLa vuelta actual del bucle.En la vuelta siguiente.
breakEl bucle.Después de done.
returnLa función (tema 19).Después de la llamada a la función.
exitEl script entero.En ningún lado: el proceso termina.

Elegir el nivel adecuado es parte del diseño. Ante un elemento problemático en una lista, continue sigue con los demás; ante un error que hace inútil seguir, como un destino sin espacio, exit termina todo. Usar break o continue fuera de un bucle no hace nada útil: Bash avisa con un mensaje y sigue.

5. continue y los contadores

En un for, continue es siempre seguro: el elemento siguiente lo pone el propio for. En un while que avanza con un contador, en cambio, hay una trampa: si el incremento está al final del cuerpo, continue lo saltea, y el bucle repite la misma vuelta para siempre.

un bucle que nunca termina
i=0
while (( i < 10 )); do
  (( i % 2 == 0 )) && continue      # con i = 0, salta… el incremento
  echo "$i"
  i=$(( i + 1 ))
done

La corrección es incrementar al comienzo del cuerpo, antes de cualquier continue, o usar un for (( )), cuyo paso se ejecuta siempre, aunque se salte la vuelta. Es una buena razón para preferir for (( )) cuando se cuenta.

6. Buscar y detenerse

Un patrón muy frecuente combina un bucle, una condición y break: recorrer una lista hasta encontrar el primer elemento que cumple algo. Por ejemplo, el primer puerto libre a partir de uno dado, o el primer servidor que responde:

el primer servidor disponible
servidores=(espejo1.example.com espejo2.example.com espejo3.example.com)
elegido=""

for s in "${servidores[@]}"; do
  if curl -fsS --max-time 3 -o /dev/null "https://$s/estado"; then
    elegido="$s"
    break
  fi
  echo "$s no responde; pruebo el siguiente" >&2
done

[[ -n "$elegido" ]] || { echo "Ningún servidor responde" >&2; exit 1; }
echo "Descargando desde $elegido"

La variable elegido empieza vacía y solo toma valor si se encontró algo. Después del bucle, comprobarla distingue los dos finales posibles: salió por break, con un servidor elegido, o recorrió toda la lista sin encontrar ninguno. Es más claro que depender del valor que quedó en la variable del bucle, que en el segundo caso sería el último elemento, aunque no haya funcionado.

7. Menús con select

Bash tiene un bucle especial para construir menús: select. Muestra una lista numerada de opciones, pregunta un número, y asigna a una variable la opción correspondiente. Su forma es la de un for:

terminal
$ select color in rojo verde azul; do echo "Elegiste $color"; break; done
1) rojo
2) verde
3) azul
#? 2
Elegiste verde

A diferencia de for, select no recorre la lista: es un bucle infinito que, en cada vuelta, pregunta y ejecuta el cuerpo con la opción elegida. Por eso casi siempre lleva un break: sin él, el menú vuelve a preguntar después de cada elección. Termina solo cuando se ejecuta break o cuando la entrada se acaba, por ejemplo al pulsar Ctrl+D.

La lista de opciones sigue las mismas reglas que la de for: puede ser un array ("${opciones[@]}"), un patrón de archivos (*.conf) o una lista escrita, y cada elemento conserva sus espacios.

8. PS3, REPLY y las opciones inválidas

Tres detalles completan el funcionamiento de select:

ElementoQué es
PS3El texto de la pregunta. Por defecto es #? , poco claro para cualquiera; conviene asignarlo siempre antes del select.
REPLYLo que el usuario escribió, tal cual. Sirve para reconocer respuestas que no son números de la lista, como 0 o q para salir.
La variable del selectLa opción elegida, o vacía si lo escrito no corresponde a ninguna opción.

Si el usuario escribe un número fuera de rango o un texto, select no da error: ejecuta el cuerpo con la variable vacía. El cuerpo debe comprobarlo. Y si pulsa Enter sin escribir nada, select vuelve a mostrar el menú.

un menú que valida la respuesta
PS3="Elige un entorno (0 para salir): "
select entorno in desarrollo pruebas produccion; do
  if [[ "$REPLY" == 0 ]]; then
    echo "Cancelado."
    exit 0
  elif [[ -n "$entorno" ]]; then
    break                           # opción válida
  fi
  echo "Opción no válida: $REPLY" >&2
done
echo "Desplegando en $entorno"
terminal
1) desarrollo
2) pruebas
3) produccion
Elige un entorno (0 para salir): 7
Opción no válida: 7
Elige un entorno (0 para salir): 2
Desplegando en pruebas

El menú se escribe en la salida de errores, no en la estándar, de modo que no se mezcla con los datos si la salida del script se redirige.

9. select con case

Cuando cada opción del menú lleva a una acción distinta, select se combina con el case del tema 13, y el menú se repite hasta que se elige salir. Es la estructura de un pequeño programa interactivo:

un menú de tareas
PS3="¿Qué quieres hacer? "
opciones=("Ver espacio en disco" "Ver memoria" "Ver procesos que más consumen" "Salir")

select opcion in "${opciones[@]}"; do
  case "$REPLY" in
    1) df -h / ;;
    2) free -h ;;
    3) ps aux --sort=-%cpu | head -6 ;;
    4) break ;;
    *) echo "Elige un número del 1 al ${#opciones[@]}" >&2 ;;
  esac
done

Aquí el case compara REPLY, el número escrito, en lugar del texto de la opción: si mañana se cambia la redacción de una opción, el case no se rompe. Sin break en las opciones de tareas, el menú vuelve a aparecer después de cada una. Para volver a mostrar la lista, que puede haber quedado lejos en la pantalla, basta con pulsar Enter.

10. Los límites de select

select es práctico para scripts de uso personal o de administración, pero tiene límites que conviene conocer:

11. Un script completo

Restaurar un respaldo es una tarea poco frecuente, que se hace a mano y en momentos de apuro: el lugar ideal para un menú que evite tener que escribir nombres de archivo largos. Este script lista los respaldos de un directorio, deja elegir uno, pide confirmación y lo extrae en un directorio de destino.

restaurar
#!/usr/bin/env bash
#
# restaurar — elige un respaldo de una lista y lo extrae en un directorio.
#
# Uso: restaurar [DIRECTORIO_DE_RESPALDOS] [DESTINO]
#      Por defecto, /respaldos y ./restaurado. Necesita una terminal.
#
set -euo pipefail
shopt -s nullglob

origen="${1:-/respaldos}"
destino="${2:-./restaurado}"

respaldos=("$origen"/*.tar.gz)
if (( ${#respaldos[@]} == 0 )); then
  echo "No hay respaldos en $origen" >&2
  exit 1
fi
[[ -t 0 ]] || { echo "${0##*/} necesita una terminal para elegir" >&2; exit 2; }

PS3="Número del respaldo (0 para salir): "
select respaldo in "${respaldos[@]##*/}"; do     # solo los nombres, sin el directorio
  if [[ "$REPLY" == 0 ]]; then
    echo "Cancelado."
    exit 0
  elif [[ -n "$respaldo" ]]; then
    break
  fi
  echo "Opción no válida: $REPLY" >&2
done
[[ -n "${respaldo:-}" ]] || exit 0                 # la entrada terminó (Ctrl+D)

read -rp "¿Extraer $respaldo en $destino? [s/N] " r
if [[ "${r,,}" != s* ]]; then
  echo "Cancelado."
  exit 0
fi

mkdir -p -- "$destino"
tar -xzf "$origen/$respaldo" -C "$destino"
echo "Restaurado $respaldo en $destino"
terminal
$ restaurar /respaldos /tmp/recuperado
1) web-2026-10-12.tar.gz
2) web-2026-10-13.tar.gz
3) web-2026-10-14.tar.gz
Número del respaldo (0 para salir): 7
Opción no válida: 7
Número del respaldo (0 para salir): 2
¿Extraer web-2026-10-13.tar.gz en /tmp/recuperado? [s/N] s
Restaurado web-2026-10-13.tar.gz en /tmp/recuperado

12. Errores frecuentes

EscribisteQué pasaCorrección
continue antes del incremento en un whileEl bucle repite la misma vuelta para siempre.Incrementar al comienzo, o usar for (( )).
break en un bucle interno esperando salir de todosSolo termina el interno; el externo sigue.break 2, o una función con return.
Usar la variable del bucle para saber si se encontró algoSi no hubo break, contiene el último elemento.Una variable aparte, vacía hasta que se encuentra.
select sin breakEl menú vuelve a preguntar después de cada elección.break tras una opción válida.
No comprobar si la variable del select está vacíaUna respuesta inválida se procesa como si fuera una opción.[[ -n "$opcion" ]] antes de usarla.
select sin asignar PS3Aparece #?, que nadie entiende.PS3="Elige…: "
Un menú como única forma de elegirEl script no puede usarse desde cron ni desde otro script.Aceptar la elección como argumento; menú solo con terminal.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué diferencia hay entre continue y break?

continue abandona solo la vuelta actual y sigue con la siguiente. break termina el bucle completo.

2. ¿Qué muestra for i in 1 2 3 4; do (( i == 2 )) && continue; (( i == 4 )) && break; echo $i; done?

1 y 3. El 2 se salta con continue, y al llegar al 4 el bucle termina antes de mostrarlo.

3. ¿Qué hace break 2?

Termina el bucle en que se ejecuta y también el bucle que lo contiene. La ejecución sigue después del done del bucle exterior.

4. ¿Por qué este bucle no termina: i=0; while (( i < 5 )); do (( i == 0 )) && continue; i=$(( i + 1 )); done?

Porque con i en 0, continue salta el incremento, la condición se evalúa de nuevo con i todavía en 0, y así indefinidamente.

5. Después de un bucle de búsqueda, ¿por qué no conviene usar la variable del bucle para saber qué se encontró?

Porque si el bucle recorrió toda la lista sin encontrar nada, la variable contiene el último elemento, y no hay forma de distinguir ese caso de un hallazgo. Se usa una variable aparte, vacía hasta que se encuentra algo.

6. ¿Qué contienen la variable del select y REPLY si el usuario escribe 9 en un menú de tres opciones?

La variable queda vacía, porque 9 no corresponde a ninguna opción, y REPLY contiene 9.

7. ¿Para qué sirve PS3?

Es el texto que select muestra al preguntar. Por defecto es #? .

8. ¿Cuándo termina un select?

Cuando se ejecuta break (o exit) dentro de él, o cuando se acaba la entrada, por ejemplo al pulsar Ctrl+D.

9. ¿Por qué en un menú de tareas conviene que el case compare $REPLY y no el texto de la opción?

Porque el número no cambia si se modifica la redacción de la opción. Comparar el texto obligaría a cambiar también el case.

10. ¿Por qué un script que usa select debería aceptar la opción también como argumento?

Porque un menú necesita a alguien delante. Sin una forma no interactiva de elegir, el script no puede ejecutarse desde cron ni desde otro script.

Para practicar

  1. Escribe un bucle que recorra los números del 1 al 30 y muestre solo los que no son múltiplos de 3, usando continue.
  2. Escribe un bucle que busque el primer puerto libre a partir del 8000, comprobando con /dev/tcp si hay algo escuchando, y se detenga con break.
  3. Reproduce el bucle infinito de la sección 5 (detenlo con Ctrl+C) y corrígelo de dos formas.
  4. Escribe un select que muestre los archivos .conf de /etc y abra el elegido con less.
  5. Construye un menú de tareas con select y case con al menos cuatro acciones útiles para ti, y una para salir.
  6. Crea algunos respaldos de prueba con tar -czf en un directorio y usa restaurar para extraer uno. Modifícalo para que acepte el nombre del respaldo como tercer argumento, y use el menú solo si falta.