Un menú interactivo en Bash: read, validación, select y confirmaciones

Cómo hacer un script de Bash que pregunta: leer respuestas con read, validarlas hasta que tengan sentido, montar un menú con select, pedir confirmación antes de lo peligroso, leer contraseñas sin mostrarlas y usar colores sin estropear la salida cuando no hay terminal.

Los scripts de esta página leen del teclado. Para ejecutarlos en las pruebas les dimos las respuestas desde un archivo, y por eso usan una pequeña función, leer, que muestra lo que se «tecleó» cuando la entrada no viene de una terminal. En tu terminal no hace falta: lo que escribes ya se ve, y puedes usar directamente read -rp "Pregunta: " variable.

Preguntar y leer la respuesta

read lee una línea y la guarda en una variable. Úsalo siempre con -r, para que las barras invertidas se lean tal cual. Con -p muestra una pregunta antes (solo si se lee de una terminal):

#!/usr/bin/env bash
# leer "pregunta": muestra la pregunta, lee una línea y la devuelve por la salida.
# La pregunta va a stderr para que no acabe dentro de la variable; si la entrada no es
# una terminal, repite la respuesta, como se vería al teclearla.
leer() {
    local respuesta
    printf '%s' "$1" >&2
    IFS= read -r respuesta || return 1
    [[ -t 0 ]] || printf '%s\n' "$respuesta" >&2
    printf '%s' "$respuesta"
}

nombre=$(leer "¿Cómo te llamas? ")
ciudad=$(leer "¿De qué ciudad eres? ")
echo "Hola, $nombre, de $ciudad."
salida¿Cómo te llamas? Ana López
¿De qué ciudad eres? Monterrey
Hola, Ana López, de Monterrey.
#!/usr/bin/env bash
read ruta
echo "Has escrito: $ruta"

Sin -r, read se come las barras invertidas, y ShellCheck lo avisa:

shellcheckIn sin_r.sh line 2:
read ruta
^--^ SC2162 (info): read without -r will mangle backslashes.

For more information:
  https://www.shellcheck.net/wiki/SC2162 -- read without -r will mangle backs...
salidaHas escrito: C:Usersananuevo

Validar lo que se escribe

No te fíes nunca de lo que escribe el usuario. Lo habitual es preguntar en un bucle hasta que la respuesta tenga sentido, comprobándola con una expresión regular:

#!/usr/bin/env bash
leer() { local r; printf '%s' "$1" >&2; IFS= read -r r || return 1; [[ -t 0 ]] || printf '%s\n' "$r" >&2; printf '%s' "$r"; }

while true; do
    copias=$(leer "¿Cuántas copias quieres guardar (1-30)? ") || exit 1
    if [[ $copias =~ ^[0-9]+$ ]] && (( copias >= 1 && copias <= 30 )); then
        break
    fi
    echo "  «$copias» no vale: escribe un número del 1 al 30." >&2
done
echo "Se guardarán $copias copias."
salida¿Cuántas copias quieres guardar (1-30)? muchas
  «muchas» no vale: escribe un número del 1 al 30.
¿Cuántas copias quieres guardar (1-30)? 0
  «0» no vale: escribe un número del 1 al 30.
¿Cuántas copias quieres guardar (1-30)? 45
  «45» no vale: escribe un número del 1 al 30.
¿Cuántas copias quieres guardar (1-30)? 14
Se guardarán 14 copias.

El || exit 1 importa: si la entrada se acaba (el usuario pulsa Ctrl+D o el archivo no tiene más líneas), read falla y, sin esa salida, el bucle no terminaría nunca.

Un menú con select

Bash trae una orden pensada para menús: select numera las opciones, muestra la pregunta que haya en la variable PS3 y guarda en la variable la opción elegida (o la deja vacía si el número no existe). Se repite hasta que haces break:

#!/usr/bin/env bash
PS3="Elige una opción: "
opciones=("Ver la fecha" "Contar archivos" "Salir")

select opcion in "${opciones[@]}"; do
    [[ -t 0 ]] || echo "$REPLY"
    case $opcion in
        "Ver la fecha")    date -d '2026-10-30 10:00' '+Hoy es %F' ;;
        "Contar archivos") echo "Archivos en esta carpeta: $(find . -maxdepth 1 -type f | wc -l)" ;;
        "Salir")           echo "Hasta luego"; break ;;
        *)                 echo "«$REPLY» no es una opción válida" ;;
    esac
done
salida1) Ver la fecha
2) Contar archivos
3) Salir
Elige una opción: 1
Hoy es 2026-10-30
Elige una opción: 7
«7» no es una opción válida
Elige una opción: 2
Archivos en esta carpeta: 1
Elige una opción: 3
Hasta luego

El número que escribe el usuario queda en REPLY. select vuelve a mostrar la lista cuando la respuesta está vacía (si se pulsa Intro sin más), y escribe el menú por la salida de errores, para que no se mezcle con los resultados si rediriges la salida a un archivo.

Confirmar antes de algo peligroso

Antes de borrar, sobrescribir o reiniciar, pregunta. La convención es que la opción por omisión (la que se toma si solo se pulsa Intro) sea la segura, y se escribe en mayúscula: [s/N]:

#!/usr/bin/env bash
confirmar() {
    local respuesta
    printf '%s [s/N] ' "$1"
    IFS= read -r respuesta || return 1
    [[ -t 0 ]] || printf '%s\n' "$respuesta"
    [[ $respuesta =~ ^[sS]([iI]|í|Í)?$ ]]
}

if confirmar "¿Borrar las copias de más de 30 días?"; then
    echo "Borrando..."
else
    echo "No se ha borrado nada."
fi

for pregunta in "¿Seguro que quieres continuar?" "¿Vaciar la papelera?"; do
    if confirmar "$pregunta"; then echo "Hecho."; else echo "Cancelado."; fi
done
salida¿Borrar las copias de más de 30 días? [s/N] 
No se ha borrado nada.
¿Seguro que quieres continuar? [s/N] sí
Hecho.
¿Vaciar la papelera? [s/N] no
Cancelado.

Para que un script pueda usarse también desde cron o desde otro script, donde no hay nadie para contestar, añade una opción como -y que dé por buena la confirmación.

Contraseñas, teclas sueltas y tiempo límite

  • Opción de readPara quéEjemplo
  • -sNo muestra lo que se escribe (contraseñas)read -rsp 'Contraseña: ' clave; echo
  • -n 1Lee una sola tecla, sin esperar a Introread -rn 1 -p '¿Seguir? ' tecla
  • -t 10Se rinde a los 10 segundos (código mayor que 128)read -rt 10 -p 'Nombre: ' n || n=anónimo
  • -aGuarda las palabras en un arrayread -ra palabras
  • -ePermite editar la línea con las flechasread -re -p 'Ruta: ' ruta

Con -s la contraseña no se ve en pantalla ni queda en el historial, pero mientras el script corre está en una variable: no la pases como argumento a otra orden, porque los argumentos se ven con ps. Más detalles en la seguridad de los scripts de Bash.

Colores, solo si hay terminal

Los colores se consiguen con secuencias de escape ANSI. Pero si la salida va a un archivo o a otra orden, esas secuencias se quedan como basura en el texto. Lo correcto es usarlos solo cuando la salida es una terminal ([[ -t 1 ]]) y respetar la convención NO_COLOR, con la que el usuario pide que no haya colores:

#!/usr/bin/env bash
if [[ -t 1 && -z ${NO_COLOR:-} ]]; then
    rojo=$'\e[31m' verde=$'\e[32m' negrita=$'\e[1m' normal=$'\e[0m'
else
    rojo='' verde='' negrita='' normal=''
fi

echo "${negrita}Estado de las copias${normal}"
echo "${verde}✔${normal} documentos: correcta"
echo "${rojo}✘${normal} fotos: falta la copia de hoy"

if [[ -t 1 ]]; then echo "(la salida es una terminal)"; else echo "(la salida no es una terminal: sin colores)"; fi
salidaEstado de las copias
✔ documentos: correcta
✘ fotos: falta la copia de hoy
(la salida no es una terminal: sin colores)

Todo junto: un menú de tareas

#!/usr/bin/env bash
set -uo pipefail
leer() { local r; printf '%s' "$1" >&2; IFS= read -r r || return 1; [[ -t 0 ]] || printf '%s\n' "$r" >&2; printf '%s' "$r"; }

mkdir -p notas
mostrar_menu() {
    echo
    echo "=== Mis notas ==="
    echo "1) Añadir una nota"
    echo "2) Ver las notas"
    echo "3) Borrar todas"
    echo "0) Salir"
}

while true; do
    mostrar_menu
    opcion=$(leer "Opción: ") || break
    case $opcion in
        1)  texto=$(leer "Texto de la nota: ") || break
            if [[ -n $texto ]]; then
                printf '%s\n' "$texto" >> notas/notas.txt
                echo "Guardada."
            else
                echo "La nota está vacía; no se guarda."
            fi ;;
        2)  if [[ -s notas/notas.txt ]]; then cat -n notas/notas.txt; else echo "No hay notas."; fi ;;
        3)  seguro=$(leer "¿Seguro? [s/N] ") || break
            if [[ $seguro == [sS] ]]; then : > notas/notas.txt; echo "Notas borradas."; else echo "No se ha borrado nada."; fi ;;
        0)  echo "Adiós."; break ;;
        *)  echo "Opción no válida: $opcion" ;;
    esac
done
salida
=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 2
No hay notas.

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 1
Texto de la nota: Comprar café
Guardada.

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 1
Texto de la nota: Llamar a Bruno
Guardada.

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 2
     1	Comprar café
     2	Llamar a Bruno

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 9
Opción no válida: 9

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 3
¿Seguro? [s/N] n
No se ha borrado nada.

=== Mis notas ===
1) Añadir una nota
2) Ver las notas
3) Borrar todas
0) Salir
Opción: 0
Adiós.

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.