28

Menús para tus scripts en Bash

Vistas: 0
Tutorial: Scripts en Bash
Menús para tus scripts en Bash

Hasta ahora has escrito scripts que se ejecutan de principio a fin sin intervención del usuario. Pero muchas herramientas del día a día — instaladores, asistentes de configuración, gestores de archivos, menús de administración — necesitan interactuar con quien las usa.

Un script interactivo bien diseñado:

  • Guía al usuario con menús claros y prompts informativos
  • Valida la entrada antes de procesarla
  • Muestra progreso en tareas largas (barras, spinners)
  • Usa colores para diferenciar información, advertencias y errores
  • Acepta argumentos como un comando de terminal profesional

En este capítulo verás todas las herramientas que Bash pone a tu disposición para construir interfaces de línea de comandos (CLI) de nivel profesional: desde menús con select, hasta parseo de argumentos con getopts, animaciones en consola y personalización del terminal.

Filosofía del capítulo: no necesitas dialog, whiptail ni ncurses. Bash tiene todo lo necesario para construir CLIs potentes y atractivas. Domínalo y podrás crear herramientas que otros querrán usar.

select — Menús nativos de Bash

¿Qué es select?

select es una palabra reservada de Bash que genera menús numéricos automáticos. Presenta una lista de opciones, muestra un prompt (PS3), y almacena la opción elegida en una variable. Se usa casi siempre con case.

Sintaxis básica:

select variable in opcion1 opcion2 opcion3; do
    # procesar $variable
    break  # salir del bucle
done

Ejemplo mínimo:

#!/bin/bash
echo "Elige tu sistema operativo favorito:"
select os in "Linux" "macOS" "Windows" "BSD" "Salir"; do
    echo "Has elegido: $os"
    break
done

Salida:

1) Linux
2) macOS
3) Windows
4) BSD
5) Salir
Elige tu sistema operativo favorito: 3
Has elegido: Windows

Bash genera automáticamente la numeración, el formateo en columnas, y la captura del número. Si el usuario introduce un valor inválido, la variable queda vacía y el bucle se repite.

PS3 — El prompt del menú

La variable PS3 controla el texto que select muestra como prompt. Por defecto es #? , pero puedes personalizarlo:

#!/bin/bash
PS3="👉 Elige una opción (1-4): "
select accion in "Backup" "Restaurar" "Ver logs" "Salir"; do
    case "$accion" in
        "Backup")    echo "Iniciando backup..."; break ;;
        "Restaurar") echo "Iniciando restauración..."; break ;;
        "Ver logs")  echo "Mostrando logs..."; break ;;
        "Salir")     echo "¡Hasta luego!"; exit 0 ;;
        *)           echo "Opción inválida: $REPLY";;
    esac
done

La variable REPLY contiene el texto exacto que el usuario escribió (el número o el texto). Es útil para validar o depurar.

Estructura típica: select + case

El patrón profesional combina select con case para manejar cada opción:

#!/bin/bash
# gestor_servicios.sh — Menú de administración
PS3="Selecciona un servicio [1-6]: "
select svc in "nginx" "postgresql" "redis" "docker" "ssh" "Salir"; do
    case "$svc" in
        "Salir") echo "👋 Saliendo..."; exit 0 ;;
        *)
            echo "=== Estado del servicio: $svc ==="
            systemctl status "$svc" 2>/dev/null || echo "⚠ Servicio no encontrado"
            echo ""
            ;;
    esac
done

Ventajas de este patrón:

  • select genera el menú automáticamente
  • case maneja cada opción de forma legible
  • Si el usuario elige un número inválido, $svc queda vacío y el bucle se repite
  • REPLY siempre contiene lo que el usuario escribió

Menús anidados y submenús

Puedes crear menús dentro de menús para estructuras jerárquicas:

#!/bin/bash
# menú_anidado.sh
PS3="Menú principal [1-4]: "
select main in "Gestión de usuarios" "Gestión de archivos" "Configuración" "Salir"; do
    case "$main" in
        "Gestión de usuarios")
            PS3="Submenú usuarios [1-4]: "
            select sub in "Listar" "Crear" "Eliminar" "Volver"; do
                case "$sub" in
                    "Listar")  cut -d: -f1 /etc/passwd | head -10 ;;
                    "Crear")   read -p "Nombre de usuario: " u; echo "Creando $u..." ;;
                    "Eliminar") read -p "Usuario a eliminar: " u; echo "Eliminando $u..." ;;
                    "Volver")  break ;;
                esac
            done
            PS3="Menú principal [1-4]: "  # restaurar prompt
            ;;
        "Gestión de archivos")
            echo "📂 Gestión de archivos — implementar..."
            ;;
        "Configuración")
            echo "⚙ Configuración — implementar..."
            ;;
        "Salir")
            echo "👋 Hasta luego"; exit 0 ;;
    esac
done

💡 Consejo: al salir de un submenú, restaura PS3 al valor del menú principal. Si no, el prompt del submenú persiste.

read — Lectura avanzada de entrada

Ya conoces read del capítulo 9 (preguntar al usuario). Pero read tiene opciones mucho más potentes para entornos interactivos.

-p (prompt), -t (timeout), -n (chars)

#!/bin/bash

# -p: Prompt en línea
read -p "🌍 Tu nombre: " nombre
echo "Hola, $nombre"

# -t: Timeout en segundos (si no responde, sigue)
if read -t 5 -p "⏰ Tienes 5 segundos: " respuesta; then
    echo "Respondiste: $respuesta"
else
    echo -e "\n⏱ Tiempo agotado"
fi

# -n: Leer N caracteres (sin esperar Enter)
echo -n "Presiona cualquier tecla para continuar... "
read -n 1 tecla
echo -e "\nPresionaste: '$tecla'"

# Combinación: timeout + N caracteres
read -t 10 -n 3 -p "Código (3 chars, 10s): " codigo
echo -e "\nCódigo: $codigo"

Usos prácticos de -n 1:

  • Confirmaciones: read -n 1 -p "¿Continuar? (s/N): " resp
  • Pausas: read -n 1 -s -p "Presiona una tecla para continuar..."
  • Menús de tecla única

-s (silent) — Introducir contraseñas

#!/bin/bash

read -s -p "🔐 Contraseña: " password
echo  # read -s no añade salto de línea
echo "Contraseña recibida (${#password} caracteres)"

# Con confirmación: leer dos veces
read -s -p "Nueva contraseña: " p1
echo
read -s -p "Repite contraseña: " p2
echo

if [[ "$p1" == "$p2" ]]; then
    echo "✅ Contraseña actualizada"
else
    echo "❌ Las contraseñas no coinciden"
fi

-i (default) y -e (line edit)

#!/bin/bash

# -i: Valor por defecto (requiere -e en Bash < 5, pero funciona solo en Bash 4+)
read -e -i "lorenzo" -p "👤 Usuario: " usuario
echo "Usuario: $usuario"

# -e: Habilita edición de línea (flechas, retroceso, tab)
read -e -p "Ruta del archivo: " ruta
echo "Ruta: $ruta"

# Combinación completa
read -e -i "/home/lorenzo/scripts" -p "📁 Directorio de trabajo: " dir
echo "Directorio: $dir"

Con -e, el usuario puede usar las flechas del teclado, Backspace, Supr, y Tab (si está configurado) dentro de la línea.

Validación y bucles de entrada

El patrón profesional para entrada validada:

#!/bin/bash

# Validar email
while true; do
    read -p "📧 Email: " email
    if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        echo "✅ Email válido"
        break
    else
        echo "❌ Formato de email inválido (ej: usuario@dominio.com)"
    fi
done

# Validar número de puerto (1-65535)
while true; do
    read -p "🔌 Puerto: " puerto
    if [[ "$puerto" =~ ^[0-9]+$ ]] && (( puerto >= 1 && puerto <= 65535 )); then
        echo "✅ Puerto válido: $puerto"
        break
    else
        echo "❌ Debe ser un número entre 1 y 65535"
    fi
done

# Validar sí/no
while true; do
    read -n 1 -p "💾 ¿Guardar cambios? (s/N): " resp
    echo
    case "$resp" in
        s|S) echo "✅ Guardado"; break ;;
        n|N|"") echo "❌ Cancelado"; break ;;
        *) echo "❌ Responde s o n" ;;
    esac
done

getopts — Parseo de argumentos POSIX

¿Qué es getopts?

getopts es un comando integrado de Bash que parsea opciones y argumentos de línea de comandos. Es el estándar POSIX — funciona en cualquier shell compatible con POSIX (bash, dash, zsh, ksh).

Con getopts puedes crear scripts que se usen como comandos profesionales:

./script.sh -v -f archivo.txt -o salida.txt arg1 arg2

Sintaxis básica

while getopts ":ab:c" opt; do
    case "$opt" in
        a) echo "Flag -a activado" ;;
        b) echo "Opción -b con valor: $OPTARG" ;;
        c) echo "Flag -c activado" ;;
        ?) echo "Opción inválida: -$OPTARG" >&2; exit 1 ;;
    esac
done

Reglas de sintaxis:

SímboloSignificado
aFlag simple (sin argumento)
b:Opción con argumento (requiere valor: -b valor)
:ab:cEl : inicial suprime errores automáticos (modo silencioso)
?Captura opciones no reconocidas en modo silencioso
$OPTARGContiene el valor del argumento (para opciones con :)

OPTIND y OPTARG

  • OPTIND: índice del siguiente argumento a procesar. Empieza en 1. Después de getopts, OPTIND apunta al primer argumento no-opción.
  • OPTARG: valor del argumento para opciones que requieren valor.
#!/bin/bash
# parseo_basico.sh

uso() {
    echo "Uso: $0 [-v] [-o archivo] [-n nombre] [args...]"
    exit 1
}

verbose=false
archivo=""
nombre="mundo"

while getopts ":vo:n:" opt; do
    case "$opt" in
        v) verbose=true ;;
        o) archivo="$OPTARG" ;;
        n) nombre="$OPTARG" ;;
        :) echo "❌ -$OPTARG requiere un argumento" >&2; uso ;;
        ?) echo "❌ Opción inválida: -$OPTARG" >&2; uso ;;
    esac
done

shift $((OPTIND - 1))  # Eliminar opciones, dejar args posicionales

$verbose && echo "📢 Modo verbose activado"
echo "👤 Nombre: $nombre"
[[ -n "$archivo" ]] && echo "📁 Archivo: $archivo"
echo "📦 Argumentos posicionales: $*"

Uso:

$ ./parseo_basico.sh -v -n Carlos -o reporte.txt arg1 arg2
📢 Modo verbose activado
👤 Nombre: Carlos
📁 Archivo: reporte.txt
📦 Argumentos posicionales: arg1 arg2

Flags, opciones con valor y argumentos posicionales

Ejemplo profesional que combina todo:

#!/bin/bash
# copia_segura.sh — Copia segura con opciones

set -euo pipefail

# ── Configuración por defecto ──
verbose=false
compress=false
destino="/tmp/backup"
exclude=""

# ── Función de ayuda ──
ayuda() {
    cat <<EOF
Uso: $0 [opciones] <origen> [origen2...]

Opciones:
  -d <dir>    Directorio de destino (defecto: /tmp/backup)
  -e <pat>    Patrón de exclusión (ej: '*.log')
  -c          Comprimir con gzip
  -v          Modo verbose
  -h          Mostrar esta ayuda
EOF
    exit 0
}

# ── Parseo con getopts ──
while getopts ":d:e:cvh" opt; do
    case "$opt" in
        d) destino="$OPTARG" ;;
        e) exclude="$OPTARG" ;;
        c) compress=true ;;
        v) verbose=true ;;
        h) ayuda ;;
        :) echo "❌ -$OPTARG requiere un argumento" >&2; exit 1 ;;
        ?) echo "❌ Opción desconocida: -$OPTARG" >&2; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

# ── Validaciones ──
[[ $# -eq 0 ]] && { echo "❌ Debes indicar al menos un origen"; ayuda; }
[[ ! -d "$destino" ]] && { echo "❌ Destino '$destino' no existe"; exit 1; }

# ── Ejecución ──
$verbose && echo "📢 Origenes: $*"
$verbose && echo "📁 Destino: $destino"
$verbose && [[ -n "$exclude" ]] && echo "🚫 Excluyendo: $exclude"

for origen in "$@"; do
    if [[ -e "$origen" ]]; then
        $verbose && echo "→ Copiando $origen..."
        if $compress; then
            tar czf "${destino}/$(basename "$origen").tar.gz" "$origen"
        else
            cp -r "$origen" "$destino/"
        fi
    else
        echo "⚠ '$origen' no existe — saltando" >&2
    fi
done

$verbose && echo "✅ Completo"

Manejo de errores en getopts

Hay dos modos de manejar errores:

Modo normal (sin : inicial):

while getopts "ab:c" opt; do
    # getopts muestra su propio error y asigna ? a $opt
    # -a sin argumento: getopts muestra error
    # -z no definida: getopts muestra error
    :
done

Modo silencioso (con : inicial):

while getopts ":ab:c" opt; do
    case "$opt" in
        a) ... ;;
        b) valor="$OPTARG" ;;
        :) echo "❌ -$OPTARG necesita un valor" ;;    # Opción sin argumento
        ?) echo "❌ Opción inválida: -$OPTARG" ;;      # Opción desconocida
    esac
done

El modo silencioso es el preferido porque te permite personalizar los mensajes de error, mostrarlos en el idioma del usuario, o simplemente tener control total.

getopt vs getopts — ¿Cuál usar?

getopts: POSIX, simple, portátil

Ventajas:

  • Estándar POSIX — funciona en dash, bash, ksh, zsh
  • Integrado en el shell (no es un comando externo)
  • Código simple y predecible
  • Sin dependencias externas

Desventajas:

  • No soporta opciones largas (--verbose, --output=archivo)
  • No reordena argumentos (las opciones deben ir antes que los posicionales)
  • No soporta valores opcionales (la opción siempre requiere o nunca requiere valor)

getopt: GNU, opciones largas, reordenamiento

getopt (el comando externo, no confundir con getopts) es una herramienta GNU que parsea opciones con capacidades avanzadas:

# getopt soporta:
# - Opciones largas: --verbose, --output=archivo
# - Reordenamiento automático de argumentos
# - Valores opcionales para opciones

args=$(getopt -o "vo:n:" --long "verbose,output:,name:" -n "$0" -- "$@")
eval set -- "$args"

while true; do
    case "$1" in
        -v|--verbose)    verbose=true; shift ;;
        -o|--output)     output="$2"; shift 2 ;;
        -n|--name)       name="$2"; shift 2 ;;
        --)              shift; break ;;
        *)               echo "❌ Opción inválida"; exit 1 ;;
    esac
done

Tabla comparativa

Característicagetoptsgetopt (GNU)
EstándarPOSIXGNU
IntegradoSí (builtin)No (comando externo)
Opciones largasNoSí (--verbose)
ReordenamientoNo
Valores opcionalesNo
PortabilidadUniversalLinux (no BSD/macOS sin GNU)
ComplejidadBajaMedia
Manejo de erroresAutomáticoPersonalizado
Valores con espaciosManual ("$var")Automático

Regla práctica:

  • Usa getopts para scripts que necesitan portabilidad y simplicidad
  • Usa getopt cuando necesites opciones largas (más legibles para usuarios) y trabajes exclusivamente en Linux

tput — Terminal bajo control

¿Qué es tput?

tput es una interfaz a la base de datos terminfo del sistema. Te permite:

  • Cambiar colores de texto y fondo
  • Aplicar estilos (negrita, subrayado, inverso)
  • Mover el cursor a posiciones específicas
  • Obtener dimensiones del terminal
  • Controlar visibilidad del cursor

Colores con setaf y setab

#!/bin/bash

# Definir colores con tput
reset=$(tput sgr0)
bold=$(tput bold)
dim=$(tput dim)
smul=$(tput smul)   # inicio subrayado
rmul=$(tput rmul)   # fin subrayado
rev=$(tput rev)     # video inverso

# Colores de texto (setaf = set ansi foreground)
black=$(tput setaf 0)
red=$(tput setaf 1)
green=$(tput setaf 2)
yellow=$(tput setaf 3)
blue=$(tput setaf 4)
magenta=$(tput setaf 5)
cyan=$(tput setaf 6)
white=$(tput setaf 7)

# Colores de fondo (setab = set ansi background)
bg_red=$(tput setab 1)
bg_green=$(tput setab 2)
bg_yellow=$(tput setab 3)
bg_blue=$(tput setab 4)

echo "${bold}${red}Error:${reset} Archivo no encontrado"
echo "${green}✓${reset} Operación completada exitosamente"
echo "${yellow}⚠${reset} ${bold}Advertencia:${reset} disco casi lleno"
echo "${rev}${white}${bg_blue} INFO ${reset} Sistema funcionando"

Paleta de colores ANSI (0-7): negro, rojo, verde, amarillo, azul, magenta, cian, blanco.
Colores brillantes (8-15): algunos terminales soportan tput setaf 9..tput setaf 15 para tonos brillantes.

Estilos de texto

bold=$(tput bold)       # Negrita
dim=$(tput dim)         # Atenuado
smul=$(tput smul)       # Subrayado (set mode underline)
rmul=$(tput rmul)       # Fin subrayado (remove mode underline)
rev=$(tput rev)         # Video inverso
smso=$(tput smso)       # Destacado (standout)
rmso=$(tput rmso)       # Fin destacado
sgr0=$(tput sgr0)       # Reset completo

echo "${bold}Texto en negrita${sgr0}"
echo "${dim}Texto atenuado${sgr0}"
echo "${smul}Texto subrayado${rmul}"
echo "${rev}Texto invertido${sgr0}"
echo "${smso}Texto destacado${rmso}"

Movimiento del cursor

# Guardar y restaurar posición: sc (save cursor), rc (restore cursor)
tput sc    # Guardar posición del cursor
echo "Trabajando..."
tput rc    # Restaurar cursor a la posición guardada
echo "✅ Hecho"    # Sobrescribe "Trabajando..."

# Mover a coordenadas específicas: cup <fila> <col> (1-indexed)
tput cup 5 10   # Mover cursor a fila 5, columna 10
echo "Posicionado"

# Subir/bajar líneas: cuu (cursor up), cud (cursor down)
tput cuu 2    # Subir 2 líneas
tput cud 3    # Bajar 3 líneas

# Avanzar/retroceder columnas: cuf (cursor forward), cub (cursor back)
tput cuf 5    # Avanzar 5 columnas
tput cub 3    # Retroceder 3 columnas

# Borrar: el (clear to end of line), ed (clear to end of display)
tput el     # Borrar desde cursor hasta fin de línea
tput ed     # Borrar desde cursor hasta fin de pantalla

# Ocultar/mostrar cursor: civis (cursor invisible), cnorm (cursor normal)
tput civis  # Ocultar cursor (útil para animaciones)
tput cnorm  # Mostrar cursor

Dimensiones del terminal: $LINES y $COLUMNS

Bash expone automáticamente el tamaño del terminal:

echo "Líneas:   $LINES"
echo "Columnas: $COLUMNS"

# Usar tput para obtener las mismas dimensiones
echo "Líneas:   $(tput lines)"
echo "Columnas: $(tput cols)"

# Dibujar un separador
printf '%*s\n' "$COLUMNS" '' | tr ' ' '─'

# Centrar texto
texto="Bienvenido al sistema"
ancho_texto=${#texto}
padding=$(( (COLUMNS - ancho_texto) / 2 ))
printf "%${padding}s%s\n" "" "$texto"

Animaciones en consola

Progress bars con \r

El retorno de carro (\r) mueve el cursor al inicio de la línea actual, permitiendo sobrescribir:

#!/bin/bash
# progress_bar.sh

progreso() {
    local total=$1
    local ancho=50
    local barra=""
    local lleno=""

    for ((i = 0; i <= total; i++)); do
        # Calcular porcentaje
        pct=$((i * 100 / total))
        llenos=$((i * ancho / total))

        # Construir barra visual
        lleno=$(printf "%${llenos}s" | tr ' ' '█')
        vacio=$(printf "%$((ancho - llenos))s" | tr ' ' '░')

        # Mostrar con \r para sobrescribir línea
        printf "\r📦 Progreso: [%s%s] %3d%%" "$lleno" "$vacio" "$pct"
        sleep 0.1
    done
    echo -e "\n✅ Completo"
}

progreso 50

Spinners (señales de actividad)

Cuando no sabes cuánto va a durar una tarea, un spinner indica que el script sigue vivo:

#!/bin/bash
# spinner.sh — Animación de actividad

spinner() {
    local pid=$1
    local mensaje="${2:-Procesando...}"
    local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    local i=0

    # Ocultar cursor mientras anima
    tput civis

    while kill -0 "$pid" 2>/dev/null; do
        local char="${chars:i++%${#chars}:1}"
        printf "\r%s %s" "$char" "$mensaje"
        sleep 0.1
    done

    # Restaurar cursor
    tput cnorm
    printf "\r✅ %s — Hecho\n" "$mensaje"
}

# Uso: ejecutar comando en segundo plano + spinner
sleep 3 &
spinner $! "Instalando dependencias..."

# Otra versión más simple (sin PID):
spin() {
    local chars='|/-\'
    local i=0
    tput civis
    while true; do
        printf "\r%s Procesando..." "${chars:i++%4:1}"
        sleep 0.15
    done
}

# spin &  # Iniciar spinner
# tarea_larga...
# kill %1  # Detener spinner
# tput cnorm

Porcentaje y barras coloreadas

#!/bin/bash
# progress_color.sh — Barra de progreso con colores

barra_color() {
    local actual=$1
    local total=$2
    local ancho=${3:-40}
    local pct=$((actual * 100 / total))
    local llenos=$((actual * ancho / total))

    # Elegir color según progreso
    if (( pct < 30 )); then
        color=$(tput setaf 1)    # Rojo
    elif (( pct < 70 )); then
        color=$(tput setaf 3)    # Amarillo
    else
        color=$(tput setaf 2)    # Verde
    fi
    reset=$(tput sgr0)

    lleno=$(printf "%${llenos}s" | tr ' ' '█')
    vacio=$(printf "%$((ancho - llenos))s" | tr ' ' '░')

    printf "\r${color}[%s%s]%3d%%${reset}" "$lleno" "$vacio" "$pct"
}

# Simular tarea
for i in $(seq 1 100); do
    barra_color "$i" 100
    sleep 0.05
done
echo -e "\n✅ Tarea completa"

💡 Nota: \r solo funciona si stdout va a un terminal. Cuando rediriges a un archivo o pipe, cada \r se escribe literalmente. Puedes detectarlo con [[ -t 1 ]] (file descriptor 1 es terminal).

readline y ~/.inputrc

¿Qué es readline?

GNU Readline es la biblioteca que maneja la edición de línea en Bash (y otros programas como Python REPL, mysql, psql). Cuando usas read -e, estás usando readline.

El archivo ~/.inputrc (o /etc/inputrc) personaliza el comportamiento de readline:

# ~/.inputrc — Configuración de Readline

# Modo vi en vez de emacs (por defecto)
set editing-mode emacs
# set editing-mode vi

# Case-insensitive en tab-completion
set completion-ignore-case on

# Mostrar todas las coincidencias inmediatamente
set show-all-if-ambiguous on
set show-all-if-unmodified on

# Colores en ls y completado
set colored-stats on
set visible-stats on

# Historial: no guardar duplicados
set history-size 10000
set histverify on

# Mostrar número de opciones en completado
set completion-prefix-display-length 2

# Saltar directorios ocultos en completado (.*)
set match-hidden-files off

# Usar vi como editor por defecto (read -e)
set keymap emacs

Atajos de teclado personalizados

# ~/.inputrc — Atajos personalizados

# Ctrl+Arriba: búsqueda en historial (como Ctrl+R pero incremental)
"\e[1;5A": history-search-backward
"\e[1;5B": history-search-forward

# Alt+Del: borrar palabra anterior
"\e[3;3~": kill-word

# Ctrl+Backspace: borrar palabra hacia atrás
"\C-h": backward-kill-word

# Insertar fecha actual con Ctrl+D
"\C-xd": "date +%Y-%m-%d"

# Insertar "sudo " al inicio con Alt+S
"\es": "\C-asudo "

# Expandir ! al presionar Espacio
set expand-tilde on

Compartición de historial entre terminales

# ~/.bashrc — Historial compartido en tiempo real

# Sin compartir (por defecto): cada terminal tiene su historial
# Al cerrar una terminal, se guarda; al abrir otra, se carga

# Compartir en tiempo real:
shopt -s histappend
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
# -a: añadir línea actual al archivo
# -c: limpiar historial en memoria
# -r: recargar archivo de historial

Integración con el sistema

Pager (less) desde scripts

Cuando muestras mucha información, el pager permite navegar:

#!/bin/bash

# Usar less como pager si stdout es terminal
usar_pager() {
    if [[ -t 1 ]]; then
        "$@" | less -FXR
    else
        "$@"
    fi
}

# -F: salir si el contenido cabe en una pantalla
# -X: no limpiar pantalla al salir
# -R: mostrar colores ANSI

# Ejemplo: listar procesos con pager
listar_procesos() {
    ps aux --sort=-%mem | head -50
}

usar_pager listar_procesos

# También puedes pipe directamente a less
systemctl list-units --type=service --all | less -FX

# Detectar si el usuario quiere pager
read -n 1 -p "📄 ¿Usar pager? (s/N): " p
echo
if [[ "$p" =~ ^[sS]$ ]]; then
    comando | less -FX
else
    comando
fi

File picker con ls + FZF

Puedes construir un selector de archivos interactivo sin instalar nada adicional, o aprovechar fzf si está disponible:

#!/bin/bash
# file_picker.sh — Selector interactivo de archivos

# Versión nativa (sin fzf)
selector_archivos() {
    local dir="${1:-.}"
    local archivos=()
    local i=1

    echo "📂 Archivos en: $(realpath "$dir")"
    echo ""

    for archivo in "$dir"/*; do
        if [[ -f "$archivo" ]]; then
            echo "$i) $(basename "$archivo")"
            archivos[i]="$archivo"
            ((i++))
        fi
    done

    echo ""
    read -p "Selecciona archivo [1-$((i-1))]: " sel

    if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel < i )); then
        echo "✅ Seleccionaste: ${archivos[sel]}"
        cat "${archivos[sel]}"
    else
        echo "❌ Selección inválida"
    fi
}

# Versión con FZF (si está instalado)
selector_fzf() {
    if command -v fzf &>/dev/null; then
        local archivo
        archivo=$(find "${1:-.}" -type f | fzf --height 20 --preview 'head -50 {}')
        if [[ -n "$archivo" ]]; then
            echo "✅ Seleccionaste: $archivo"
        fi
    else
        selector_archivos "$1"
    fi
}

# Versión con ls + filtro estilo FZF manual
selector_ls_fzf_style() {
    local dir="${1:-.}"
    local archivos=()

    mapfile -t archivos < <(ls -1 "$dir"/* 2>/dev/null)

    if [[ ${#archivos[@]} -eq 0 ]]; then
        echo "📂 Directorio vacío"
        return
    fi

    echo "🔍 Escribe para filtrar (Enter para aceptar):"
    local filtro
    while true; do
        read -e -p "> " filtro
        [[ -z "$filtro" ]] && { echo "Selección cancelada"; return; }

        # Filtrar resultados
        local resultados=()
        for f in "${archivos[@]}"; do
            [[ "$(basename "$f")" == *"$filtro"* ]] && resultados+=("$(basename "$f")")
        done

        if [[ ${#resultados[@]} -eq 1 ]]; then
            echo "✅ Archivo: ${resultados[0]}"
            return
        elif [[ ${#resultados[@]} -eq 0 ]]; then
            echo "📭 Sin coincidencias para '$filtro'"
        else
            printf '%s\n' "${resultados[@]}"
        fi
    done
}

# Llamar según disponibilidad
if command -v fzf &>/dev/null; then
    selector_fzf "$@"
else
    selector_archivos "$@"
fi

Config wizard interactivo

Un asistente de configuración guía al usuario paso a paso:

#!/bin/bash
# config_wizard.sh — Asistente de configuración

wizard() {
    local config_file="${1:-config.ini}"
    local nombre=""
    local email=""
    local puerto=8080
    local verbose=false
    local auto_start=false

    clear
    echo "╔═══════════════════════════════════╗"
    echo "║    🚀 Asistente de Configuración   ║"
    echo "╚═══════════════════════════════════╝"
    echo ""

    # Paso 1: Nombre
    while [[ -z "$nombre" ]]; do
        read -p "👤 Nombre del proyecto: " nombre
        [[ -z "$nombre" ]] && echo "❌ El nombre no puede estar vacío"
    done

    # Paso 2: Email
    while true; do
        read -p "📧 Email del responsable: " email
        [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] && break
        echo "❌ Email inválido"
    done

    # Paso 3: Puerto
    read -e -i "8080" -p "🔌 Puerto del servicio: " input
    puerto="${input:-8080}"

    # Paso 4: Modo verbose
    read -n 1 -p "📢 ¿Activar modo verbose? (s/N): " resp
    echo
    [[ "$resp" =~ ^[sS]$ ]] && verbose=true

    # Paso 5: Auto-start
    read -n 1 -p "🚀 ¿Iniciar automáticamente al arrancar? (s/N): " resp
    echo
    [[ "$resp" =~ ^[sS]$ ]] && auto_start=true

    # Paso 6: Confirmación
    echo ""
    echo "═══ Resumen de configuración ═══"
    echo "  Proyecto:     $nombre"
    echo "  Email:        $email"
    echo "  Puerto:       $puerto"
    echo "  Verbose:      $verbose"
    echo "  Auto-start:   $auto_start"
    echo ""

    read -n 1 -p "💾 ¿Guardar configuración? (s/N): " guardar
    echo
    if [[ "$guardar" =~ ^[sS]$ ]]; then
        cat > "$config_file" <<EOF
# Configuración de $nombre
# Generado el $(date)
NOMBRE=$nombre
EMAIL=$email
PUERTO=$puerto
VERBOSE=$verbose
AUTO_START=$auto_start
EOF
        echo "✅ Configuración guardada en $config_file"
    else
        echo "❌ Configuración descartada"
    fi
}

wizard "$@"

Logging con colores y niveles

Un sistema de logging profesional para scripts interactivos:

#!/bin/bash
# logging_colores.sh — Sistema de logging con colores

# ── Configuración de logging ──
LOG_FILE=""
LOG_LEVEL=${LOG_LEVEL:-INFO}  # DEBUG, INFO, WARN, ERROR, FATAL
LOG_COLORS=true

# Niveles numéricos
declare -A LOG_NIVELES=(
    [DEBUG]=0
    [INFO]=1
    [WARN]=2
    [ERROR]=3
    [FATAL]=4
)

# Colores con tput
if [[ -t 2 ]] && command -v tput &>/dev/null; then
    C_RESET=$(tput sgr0)
    C_BOLD=$(tput bold)
    C_DEBUG=$(tput setaf 6)    # Cyan
    C_INFO=$(tput setaf 2)     # Verde
    C_WARN=$(tput setaf 3)     # Amarillo
    C_ERROR=$(tput setaf 1)    # Rojo
    C_FATAL=$(tput setaf 1)$(tput bold)$(tput setab 7)  # Rojo negrita fondo blanco
else
    C_RESET=""; C_BOLD=""; C_DEBUG=""; C_INFO=""
    C_WARN=""; C_ERROR=""; C_FATAL=""
    LOG_COLORS=false
fi

# Función principal de logging
log() {
    local nivel="$1"
    shift
    local mensaje="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    # Verificar si debemos loguear este nivel
    local nivel_num=${LOG_NIVELES[$nivel]:-1}
    local umbral=${LOG_NIVELES[$LOG_LEVEL]:-1}
    (( nivel_num < umbral )) && return

    # Formatear según nivel
    local color=""
    local etiqueta=""
    case "$nivel" in
        DEBUG) color="$C_DEBUG";   etiqueta="DEBUG" ;;
        INFO)  color="$C_INFO";    etiqueta="INFO " ;;
        WARN)  color="$C_WARN";    etiqueta="WARN " ;;
        ERROR) color="$C_ERROR";   etiqueta="ERROR" ;;
        FATAL) color="$C_FATAL";   etiqueta="FATAL" ;;
    esac

    # Salida a stderr
    if $LOG_COLORS; then
        echo -e "${C_BOLD}${timestamp}${C_RESET} [${color}${etiqueta}${C_RESET}] ${mensaje}" >&2
    else
        echo "[${timestamp}] [${etiqueta}] ${mensaje}" >&2
    fi

    # También a archivo si está configurado
    [[ -n "$LOG_FILE" ]] && echo "[${timestamp}] [${etiqueta}] ${mensaje}" >> "$LOG_FILE"
}

# Atajos
debug() { log "DEBUG" "$*"; }
info()  { log "INFO"  "$*"; }
warn()  { log "WARN"  "$*"; }
error() { log "ERROR" "$*"; }
fatal() { log "FATAL" "$*"; }

# ── Ejemplo de uso ──
LOG_LEVEL=DEBUG

debug "Iniciando script con PID $$"
info "Conectando a la base de datos..."
warn "Disco al 85% de capacidad"
error "No se pudo conectar al servidor: timeout"
info "Operación completada"

Funciones profesionales (4)

Funciones reutilizables para tu biblioteca personal:

# ──────────────────────────────────────────────
# Funciones profesionales para CLIs interactivos
# ──────────────────────────────────────────────

# menu_select — Menú interactivo con validación
# Uso: menu_select "título" op1 op2 op3 ...
# Devuelve: print de la opción elegida
menu_select() {
    local titulo="$1"
    shift
    local opciones=("$@")
    local PS3="${titulo}: "
    local seleccion=""

    echo ""
    select seleccion in "${opciones[@]}"; do
        if [[ -n "$seleccion" ]]; then
            echo "$seleccion"
            return 0
        else
            echo "❌ Opción inválida: $REPLY" >&2
        fi
    done
}

# confirm_action — Pide confirmación antes de ejecutar
# Uso: confirm_action "¿Eliminar archivo?" && rm archivo
# Devuelve: 0 (sí) o 1 (no)
confirm_action() {
    local mensaje="${1:-¿Estás seguro?}"
    local default="${2:-n}"  # s o n
    local prompt

    if [[ "$default" == "s" ]]; then
        prompt="$mensaje (S/n): "
    else
        prompt="$mensaje (s/N): "
    fi

    local resp
    read -n 1 -p "$prompt" resp
    echo >&2  # salto de línea tras respuesta

    case "$resp" in
        s|S) return 0 ;;
        n|N|"") return 1 ;;
        *) echo "❌ Responde s o n" >&2; return 1 ;;
    esac
}

# read_password — Lee contraseña con confirmación
# Uso: pass=$(read_password "Contraseña de MySQL")
# Devuelve: print de la contraseña
read_password() {
    local prompt="${1:-Contraseña}"
    local pass1 pass2

    while true; do
        read -s -p "🔐 ${prompt}: " pass1
        echo >&2
        read -s -p "🔐 Repite ${prompt}: " pass2
        echo >&2

        if [[ "$pass1" != "$pass2" ]]; then
            echo "❌ Las contraseñas no coinciden" >&2
        elif [[ -z "$pass1" ]]; then
            echo "❌ La contraseña no puede estar vacía" >&2
        else
            echo "$pass1"
            return 0
        fi
    done
}

# progress_bar — Barra de progreso animada
# Uso: progress_bar <actual> <total> [ancho] [mensaje]
progress_bar() {
    local actual=$1
    local total=$2
    local ancho=${3:-40}
    local mensaje="${4:-Progreso}"

    # Solo animar si stdout es terminal
    if [[ ! -t 1 ]]; then
        return
    fi

    local pct=$((actual * 100 / total))
    local llenos=$((actual * ancho / total))

    # Color según porcentaje
    if command -v tput &>/dev/null; then
        if (( pct < 30 )); then
            local color barra reset barra_vacia barra_llena
            color=$(tput setaf 1)
        elif (( pct < 70 )); then
            color=$(tput setaf 3)
        else
            color=$(tput setaf 2)
        fi
        reset=$(tput sgr0)
    fi

    local barra_llena barra_vacia
    barra_llena=$(printf "%${llenos}s" | tr ' ' '█')
    barra_vacia=$(printf "%$((ancho - llenos))s" | tr ' ' '░')

    printf "\r%s ${color}[%s%s]%3d%%${reset}" \
        "$mensaje" "$barra_llena" "$barra_vacia" "$pct"

    if (( actual == total )); then
        echo ""
    fi
}

Script completo: gestor_interactivo.sh

Un gestor de servicios con menús, configuración, colores y argumentos CLI, en ~110 líneas:

#!/bin/bash
# gestor_interactivo.sh — Gestor de servicios interactivo
# Uso: ./gestor_interactivo.sh [opciones]
# Opciones: -s <servicio> (seleccionar servicio inicial)
#           -v (verbose)
#           -h (ayuda)
set -euo pipefail

# ── Colores con tput ──
if command -v tput &>/dev/null; then
    RST=$(tput sgr0); BLD=$(tput bold)
    R=$(tput setaf 1); G=$(tput setaf 2); Y=$(tput setaf 3)
    B=$(tput setaf 4); C=$(tput setaf 6); M=$(tput setaf 5)
else
    RST=""; BLD=""; R=""; G=""; Y=""; B=""; C=""; M=""
fi

# ── Funciones de logging ──
info()  { echo -e "${G}${BLD}INFO${RST}  $*"; }
warn()  { echo -e "${Y}${BLD}WARN${RST}  $*" >&2; }
error() { echo -e "${R}${BLD}ERROR${RST} $*" >&2; }
ok()    { echo -e "${G}✓${RST} $*"; }
fail()  { echo -e "${R}✗${RST} $*" >&2; }

# ── Parseo con getopts ──
servicio_inicial=""
verbose=false

uso() {
    cat <<EOF
Uso: $0 [opciones]
Opciones:
  -s <servicio>  Seleccionar servicio inicial (ej: nginx)
  -v             Modo verbose
  -h             Mostrar esta ayuda
EOF
    exit 0
}

while getopts ":s:vh" opt; do
    case "$opt" in
        s) servicio_inicial="$OPTARG" ;;
        v) verbose=true ;;
        h) uso ;;
        :) error "-$OPTARG requiere un argumento"; exit 1 ;;
        ?) error "Opción inválida: -$OPTARG"; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

# ── Función: listar servicios ──
listar_servicios() {
    local filtro="${1:-}"
    echo -e "${BLD}${B}═══ Servicios del sistema ═══${RST}"
    if [[ -n "$filtro" ]]; then
        systemctl list-units --type=service --all --no-pager 2>/dev/null \
            | grep -i "$filtro" | head -20
    else
        systemctl list-units --type=service --all --no-pager 2>/dev/null \
            | head -25
    fi
    $verbose && info "Mostrados servicios del sistema"
}

# ── Función: estado de servicio ──
estado_servicio() {
    local svc="$1"
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
        ok "Servicio ${BLD}$svc${RST} está ${G}ACTIVO${RST}"
    else
        fail "Servicio ${BLD}$svc${RST} está ${R}INACTIVO${RST}"
    fi
    $verbose && systemctl status "$svc" --no-pager 2>/dev/null | head -10
}

# ── Función: iniciar/detener/reiniciar ──
accion_servicio() {
    local svc="$1"
    local accion="$2"  # start, stop, restart

    case "$accion" in
        start)   sudo systemctl start "$svc"   && ok "Servicio $svc iniciado"   || fail "No se pudo iniciar $svc" ;;
        stop)    sudo systemctl stop "$svc"    && ok "Servicio $svc detenido"   || fail "No se pudo detener $svc" ;;
        restart) sudo systemctl restart "$svc" && ok "Servicio $svc reiniciado" || fail "No se pudo reiniciar $svc" ;;
    esac
    $verbose && info "Acción '$accion' ejecutada sobre $svc"
}

# ── Función: spinner genérico ──
spinner() {
    local pid=$1
    local msg="${2:-Procesando...}"
    local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    local i=0
    tput civis 2>/dev/null || true
    while kill -0 "$pid" 2>/dev/null; do
        printf "\r${C}%s${RST} %s" "${chars:i++%${#chars}:1}" "$msg"
        sleep 0.1
    done
    tput cnorm 2>/dev/null || true
    printf "\r${G}✓${RST} %s — Hecho\n" "$msg"
}

# ── Función: barra de progreso ──
progress() {
    local current=$1 total=$2 msg="${3:-Avance}"
    local w=30 p=$((current * 100 / total)) l=$((current * w / total))
    local full=$(printf "%${l}s" | tr ' ' '█')
    local empty=$(printf "%$((w - l))s" | tr ' ' '░')
    printf "\r${msg} [${G}%s${RST}%s] %3d%%" "$full" "$empty" "$p"
    (( current == total )) && echo ""
}

# ── Menú principal ──
menu_principal() {
    local ops=("Listar servicios" "Estado de servicio" "Iniciar servicio"
               "Detener servicio" "Reiniciar servicio" "Config wizard" "Salir")
    local PS3="${BLD}${B}⚡${RST} Acción [1-${#ops[@]}]: "

    echo ""
    echo -e "${BLD}${M}╔═══════════════════════════════╗${RST}"
    echo -e "${BLD}${M}║   🚀 Gestor de Servicios v1.0  ║${RST}"
    echo -e "${BLD}${M}╚═══════════════════════════════╝${RST}"
    echo ""

    select accion in "${ops[@]}"; do
        [[ -z "$accion" ]] && { error "Opción inválida"; continue; }

        case "$accion" in
            "Listar servicios")
                local filtro=""
                read -p "🔍 Filtrar (Enter= todos): " filtro
                listar_servicios "$filtro"
                ;;

            "Estado de servicio")
                local svc=""
                read -e -i "${servicio_inicial:-}" -p "📦 Servicio: " svc
                [[ -z "$svc" ]] && { error "Servicio requerido"; continue; }
                estado_servicio "$svc"
                ;;

            "Iniciar servicio"|"Detener servicio"|"Reiniciar servicio")
                local svc=""
                read -e -i "${servicio_inicial:-}" -p "📦 Servicio: " svc
                [[ -z "$svc" ]] && { error "Servicio requerido"; continue; }

                # Extraer verbo de la acción
                local verbo="${accion%% *}"
                verbo="${verbo,,}"  # minúsculas

                # Confirmar
                local confirm_msg="${BLD}${Y}⚠${RST} ¿$verbo '${BLD}$svc${RST}'?"
                if [[ "$verbo" == "stop" ]]; then
                    echo -e "${R}⚠ Esta acción detendrá el servicio${RST}"
                fi
                read -n 1 -p "$confirm_msg (s/N): " resp
                echo
                [[ "$resp" =~ ^[sS]$ ]] && accion_servicio "$svc" "$verbo"
                ;;

            "Config wizard")
                echo -e "${BLD}${C}⚙ Asistente de configuración${RST}"
                read -p "📦 Servicio a monitorizar: " svc
                read -p "⏱ Intervalo (segundos): " intervalo
                intervalo="${intervalo:-60}"
                read -p "📁 Archivo de log: " logfile
                logfile="${logfile:-/var/log/monitor.log}"

                echo ""
                echo -e "${BLD}═══ Configuración ═══${RST}"
                echo "  Servicio:  $svc"
                echo "  Intervalo: ${intervalo}s"
                echo "  Log:       $logfile"
                read -n 1 -p "💾 ¿Guardar? (s/N): " resp
                echo
                if [[ "$resp" =~ ^[sS]$ ]]; then
                    cat > "monitor_${svc}.conf" <<<"SERVICIO=$svc INTERVALO=$intervalo LOG=$logfile"
                    ok "Configuración guardada en monitor_${svc}.conf"
                fi
                ;;

            "Salir")
                echo -e "${BLD}${G}👋 Hasta luego${RST}"
                exit 0
                ;;
        esac

        echo ""
        echo -e "${BLD}${C}─── Presiona Enter para volver al menú ───${RST}"
        read -s -r
    done
}

# ── Main ──
info "Gestor de Servicios Interactivo"
$verbose && info "Modo verbose activado"

# Si se especificó servicio inicial, mostrar su estado
if [[ -n "$servicio_inicial" ]]; then
    $verbose && info "Verificando servicio inicial: $servicio_inicial"
    estado_servicio "$servicio_inicial"
    echo ""
fi

menu_principal

Cómo usarlo:

chmod +x gestor_interactivo.sh

# Ejecutar normalmente
./gestor_interactivo.sh

# Con servicio inicial y verbose
./gestor_interactivo.sh -s nginx -v

# Ver ayuda
./gestor_interactivo.sh -h

Los 10 errores mas comunes

Error 1: No validar la entrada de select

# ❌ Asumir que el usuario siempre elige una opción válida
select opt in "Op1" "Op2"; do
    echo "Elegiste: $opt"
    break
done
# Si el usuario escribe "foo", $opt queda vacío y script continúa

# ✅ Validar que la selección no está vacía
select opt in "Op1" "Op2"; do
    if [[ -n "$opt" ]]; then
        echo "Elegiste: $opt"
        break
    else
        echo "❌ Opción inválida: $REPLY"
    fi
done

Error 2: Olvidar que read -s no añade salto de línea

# ❌ La salida queda pegada
read -s -p "Contraseña: " pass
echo "La contraseña es: $pass"
# Salida: Contraseña: La contraseña es: secreto    (todo en una línea)

# ✅ Añadir echo después de read -s
read -s -p "Contraseña: " pass
echo   # ← necesario
echo "La contraseña es: $pass"

Error 3: No escapar o citar correctamente en getopts

# ❌ Opción con argumento que contiene espacios
while getopts "n:" opt; do :; done
./script.sh -n Juan Pérez    # OPTARG = "Juan", "Pérez" se pierde

# ✅ Citar con comillas
./script.sh -n "Juan Pérez"  # OPTARG = "Juan Pérez"

# ❌ No usar shift $((OPTIND - 1)) después de getopts
# Los argumentos posicionales incluyen las opciones parseadas

Error 4: Mezclar getopt y getopts

# ❌ Confundir nombres y sintaxis
while getopt "ab:c" opt; do  # ¡getopt no es getopts!
    case $opt in *) : ;; esac
done

# ✅ getopts (sin 'final') es el builtin
while getopts ":ab:c" opt; do
    case "$opt" in *) : ;; esac
done

# ✅ getopt (con 'final') es el comando externo
args=$(getopt -o "ab:c" -l "all,verbose" -- "$@")
eval set -- "$args"

Error 5: Usar \r en salidas no terminal

# ❌ \r se escribe literalmente en pipes/archivos
./script_com_barra.sh | tee log.txt
# log.txt contiene: \r[████] 50%\r[████████] 100%

# ✅ Detectar si stdout es terminal
if [[ -t 1 ]]; then
    printf "\rProgreso: %d%%" "$pct"
else
    echo "Progreso: ${pct}%"
fi

Error 6: Asumir que tput está disponible

# ❌ En entornos mínimos (Docker, CI) tput falla
rojo=$(tput setaf 1)   # Command not found en Alpine

# ✅ Probar disponibilidad y usar fallback
if command -v tput &>/dev/null; then
    rojo=$(tput setaf 1)
    reset=$(tput sgr0)
else
    rojo=""
    reset=""
fi

Error 7: No restaurar el cursor tras animaciones

# ❌ El cursor desaparece al final del script
tput civis     # ocultar cursor
for i in {1..10}; do
    sleep 0.1
done
# ¡Cursor sigue oculto!

# ✅ Restaurar cursor al terminar
tput civis
for i in {1..10}; do sleep 0.1; done
tput cnorm     # restaurar cursor

# O usar trap para restaurar siempre:
trap 'tput cnorm 2>/dev/null' EXIT

Error 8: No manejar el caso de error en read -t

# ❌ Si el timeout expira, read devuelve código de error
read -t 5 -p "Nombre: " nombre
echo "Nombre: $nombre"   # $nombre está vacío, no hay aviso

# ✅ Verificar si read tuvo éxito
if read -t 5 -p "Nombre: " nombre; then
    echo "Nombre: $nombre"
else
    echo -e "\n⏱ Tiempo agotado, usando valor por defecto"
    nombre="invitado"
fi

Error 9: Olvidar el modo silencioso en getopts

# ❌ Sin ':' inicial, getopts muestra errores en inglés
while getopts "ab:c" opt; do :; done
# ./script -z  →  ./script: illegal option -- z

# ✅ Con ':' inicial, controlas los mensajes
while getopts ":ab:c" opt; do
    case "$opt" in
        ?) echo "❌ Opción desconocida: -$OPTARG" >&2; exit 1 ;;
        :) echo "❌ -$OPTARG necesita un argumento" >&2; exit 1 ;;
    esac
done

Error 10: No manejar SIGINT (Ctrl+C) en scripts interactivos

# ❌ Si el usuario pulsa Ctrl+C durante un menú, el script
#    termina sucio y puede dejar el terminal en mal estado
while true; do
    read -p "> " cmd
    eval "$cmd"  # Sin trap, Ctrl+C mata el script
done

# ✅ Trap para limpiar al salir
trap 'tput cnorm; echo -e "\n👋 Hasta luego"; exit 0' INT TERM

while true; do
    read -p "> " cmd || break  # Ctrl+C → read falla → break
    [[ -n "$cmd" ]] && eval "$cmd"
done

Resumen

ConceptoSintaxis / ComandoPropósito
selectselect var in lista; do ... doneMenú numérico automático
PS3PS3="Elige: "Prompt del menú select
REPLY$REPLYTexto que escribió el usuario en select
read -pread -p "Texto: " varPrompt en línea para entrada
read -sread -s -p "Pass: " varEntrada silenciosa (contraseñas)
read -tread -t 5 -p "> " varTimeout de espera
read -nread -n 1 varLeer N caracteres sin Enter
read -e -iread -e -i "defecto" varEdición de línea con valor por defecto
getoptswhile getopts ":ab:c" optParseo POSIX de argumentos
OPTINDshift $((OPTIND - 1))Índice tras parseo
OPTARG$OPTARGValor de opción con argumento
getoptgetopt -o "ab:c" -l "all" -- "$@"Parseo GNU con opciones largas
tput setaftput setaf 1Color de texto (0-7, 0=negro, 1=rojo)
tput setabtput setab 2Color de fondo
tput boldtput boldTexto en negrita
tput smultput smul / tput rmulSubrayado on/off
tput revtput revVideo inverso
tput sgr0tput sgr0Reset completo de estilos
tput cuptput cup 10 20Mover cursor a fila 10, columna 20
tput civistput civisOcultar cursor
tput cnormtput cnormMostrar cursor
tput sc/rctput sc / tput rcGuardar/restaurar posición cursor
\rprintf "\rProgreso: %d%%" $pctRetorno de carro (sobrescribir línea)
$LINES$LINESNúmero de líneas del terminal
$COLUMNS$COLUMNSNúmero de columnas del terminal
~/.inputrcset completion-ignore-case onPersonalización de readline
less -FXRless -FXRPager que respeta colores y tamaño

Más información,

Libros

  • «Classic Shell Scripting» de Arnold Robbins y Nelson H.F. Beebe — Capítulos sobre interacción con el usuario y parseo de argumentos
  • «Bash Cookbook» de Carl Albing, JP Vossen y Cameron Newham — Recetas para menús, validación de entrada y CLIs profesionales
  • «The Linux Command Line» de William Shotts — Capítulos sobre editores de línea y personalización del terminal
  • «Linux System Programming» de Robert Love — Conceptos subyacentes de terminal y TTY
  • «UNIX Power Tools» de Tim O’Reilly — Trucos de readline y personalización del shell
  • «Learning the bash Shell» de Cameron Newham — Guía completa de Bash con capítulo sobre programación interactiva

Deja una respuesta