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,whiptailnincurses. 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:
selectgenera el menú automáticamentecasemaneja cada opción de forma legible- Si el usuario elige un número inválido,
$svcqueda vacío y el bucle se repite REPLYsiempre 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
PS3al 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ímbolo | Significado |
|---|---|
a | Flag simple (sin argumento) |
b: | Opción con argumento (requiere valor: -b valor) |
:ab:c | El : inicial suprime errores automáticos (modo silencioso) |
? | Captura opciones no reconocidas en modo silencioso |
$OPTARG | Contiene el valor del argumento (para opciones con :) |
OPTIND y OPTARG
OPTIND: índice del siguiente argumento a procesar. Empieza en 1. Después degetopts,OPTINDapunta 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ística | getopts | getopt (GNU) |
|---|---|---|
| Estándar | POSIX | GNU |
| Integrado | Sí (builtin) | No (comando externo) |
| Opciones largas | No | Sí (--verbose) |
| Reordenamiento | No | Sí |
| Valores opcionales | No | Sí |
| Portabilidad | Universal | Linux (no BSD/macOS sin GNU) |
| Complejidad | Baja | Media |
| Manejo de errores | Automático | Personalizado |
| Valores con espacios | Manual ("$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:
\rsolo funciona si stdout va a un terminal. Cuando rediriges a un archivo o pipe, cada\rse 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
| Concepto | Sintaxis / Comando | Propósito |
|---|---|---|
| select | select var in lista; do ... done | Menú numérico automático |
| PS3 | PS3="Elige: " | Prompt del menú select |
| REPLY | $REPLY | Texto que escribió el usuario en select |
| read -p | read -p "Texto: " var | Prompt en línea para entrada |
| read -s | read -s -p "Pass: " var | Entrada silenciosa (contraseñas) |
| read -t | read -t 5 -p "> " var | Timeout de espera |
| read -n | read -n 1 var | Leer N caracteres sin Enter |
| read -e -i | read -e -i "defecto" var | Edición de línea con valor por defecto |
| getopts | while getopts ":ab:c" opt | Parseo POSIX de argumentos |
| OPTIND | shift $((OPTIND - 1)) | Índice tras parseo |
| OPTARG | $OPTARG | Valor de opción con argumento |
| getopt | getopt -o "ab:c" -l "all" -- "$@" | Parseo GNU con opciones largas |
| tput setaf | tput setaf 1 | Color de texto (0-7, 0=negro, 1=rojo) |
| tput setab | tput setab 2 | Color de fondo |
| tput bold | tput bold | Texto en negrita |
| tput smul | tput smul / tput rmul | Subrayado on/off |
| tput rev | tput rev | Video inverso |
| tput sgr0 | tput sgr0 | Reset completo de estilos |
| tput cup | tput cup 10 20 | Mover cursor a fila 10, columna 20 |
| tput civis | tput civis | Ocultar cursor |
| tput cnorm | tput cnorm | Mostrar cursor |
| tput sc/rc | tput sc / tput rc | Guardar/restaurar posición cursor |
| \r | printf "\rProgreso: %d%%" $pct | Retorno de carro (sobrescribir línea) |
| $LINES | $LINES | Número de líneas del terminal |
| $COLUMNS | $COLUMNS | Número de columnas del terminal |
| ~/.inputrc | set completion-ignore-case on | Personalización de readline |
| less -FXR | less -FXR | Pager que respeta colores y tamaño |
Más información,
- Bash Reference Manual — select — Documentación oficial de select
- Bash Reference Manual — read — Documentación oficial de read
- Bash Reference Manual — getopts — Documentación oficial de getopts
- GNU Readline Library — Documentación completa de readline
- FZF — Fuzzy Finder — Selector difuso de archivos, imprescindible en CLIs avanzados
- Bash Hackers Wiki — getopts tutorial — Tutorial detallado de getopts
- Bash Hackers Wiki — select — Guía de select con ejemplos
- Terminfo capabilities database — Todas las capacidades terminfo disponibles
- ANSI escape codes — Referencia de códigos de escape ANSI
- .inputrc examples — Ejemplos de configuración de readline
- Progress bars in Bash — Implementaciones de barras de progreso
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