Aceptación Segura de Entrada de Usuario: Técnicas Esenciales para el Comando read de Bash

Aprenda a aceptar entrada de usuario de forma segura y eficiente en scripts de Bash usando el comando `read`. Esta guía cubre técnicas esenciales para solicitar entrada, manejar contraseñas silenciosamente con `-s`, establecer tiempos de espera con `-t`, y realizar validación y saneamiento básico de entrada para crear scripts interactivos más robustos y seguros.

Aceptación Segura de Entrada de Usuario: Técnicas Esenciales para el Comando read de Bash

El comando read de Bash parece inofensivo hasta que el valor recolectado se usa en una ruta de archivo, un argumento de comando o un prompt de contraseña. La mayoría de los problemas no provienen de read en sí mismo. Provienen de confiar en el texto demasiado pronto, olvidar que los espacios y los metacaracteres del shell son entrada de usuario normal, o dejar que un script se cuelgue para siempre porque nadie respondió al prompt.

Un buen script interactivo de Bash trata la entrada como texto no confiable. Pregunta claramente, lee con cuidado, valida antes de actuar y mantiene los secretos fuera de los registros. Eso suena formal, pero la versión del día a día es simple: cite variables, use IFS= read -r por defecto, verifique el estado de retorno y rechace valores que no sepa cómo manejar.

Comience con el valor predeterminado más seguro

Para la mayoría de los prompts de una sola línea, este es el patrón al que recurro:

printf 'Project name: '
IFS= read -r project_name

if [[ -z $project_name ]]; then
  printf 'Project name is required.\n' >&2
  exit 1
fi

Hay dos detalles que vale la pena mantener. IFS= evita que Bash recorte espacios en blanco al inicio y al final mientras lee. -r le dice a read que no trate las barras invertidas como caracteres de escape. Sin -r, alguien que ingrese C:\Users\me o una cadena que contenga \n puede no recuperar el texto exacto que escribió.

También puede usar -p para un prompt:

IFS= read -r -p 'Environment [dev/staging/prod]: ' env_name

Eso está bien para una terminal interactiva. Todavía uso printf cuando quiero que el prompt y la lectura sean más fáciles de probar por separado, o cuando necesito hábitos de portabilidad más estrictos en torno al formato de salida.

Verifique si read realmente tuvo éxito

read devuelve un estado. Úselo. Una lectura fallida puede significar fin de archivo, tiempo de espera o una terminal interrumpida. Si la siguiente línea de su script asume que la variable es significativa, puede ejecutarse accidentalmente con un valor antiguo o una cadena vacía.

if ! IFS= read -r -p 'Deploy tag: ' tag; then
  printf 'No input received. Aborting.\n' >&2
  exit 1
fi

Esto importa en scripts que a veces son ejecutados por una persona y a veces en CI. En un trabajo no interactivo, read puede alcanzar EOF inmediatamente. Un error claro es mucho mejor que un comando de despliegue ejecutándose con una etiqueta en blanco.

Use tiempos de espera para prompts que no deberían bloquear para siempre

Un script de mantenimiento que espera confirmación puede retener silenciosamente un despliegue o un trabajo cron. read -t establece un tiempo de espera en segundos:

if IFS= read -r -t 15 -p 'Restart service now? [y/N] ' answer; then
  case $answer in
    y|Y|yes|YES) systemctl restart myapp ;;
    *) printf 'Skipped restart.\n' ;;
  esac
else
  printf '\nNo answer after 15 seconds; skipped restart.\n' >&2
fi

El soporte de tiempo de espera es una característica de Bash, no una característica de POSIX sh. Eso generalmente está bien para un artículo de Bash, pero vale la pena recordarlo si un script puede ejecutarse con /bin/sh en una imagen base pequeña.

Oculte contraseñas, pero no finja que están protegidas para siempre

read -s evita que los caracteres escritos se muestren en la terminal:

IFS= read -r -s -p 'Password: ' password
printf '\n'
IFS= read -r -s -p 'Confirm password: ' confirm_password
printf '\n'

if [[ $password != "$confirm_password" ]]; then
  printf 'Passwords do not match.\n' >&2
  exit 1
fi

Eso protege contra el shoulder-surfing y el desplazamiento hacia atrás de la terminal. No convierte a Bash en un gestor de secretos seguro. El valor todavía existe en una variable de shell mientras el script se ejecuta. No lo imprima con set -x habilitado, no lo pase a través de líneas de comando que aparecen en listados de procesos, y no lo escriba en archivos temporales. Si el secreto es para un flujo de trabajo de producción serio, prefiera un almacén de secretos, un archivo de token con permisos estrictos, o el prompt de contraseña nativo de la herramienta objetivo.

Una regla práctica: deshabilite xtrace alrededor del manejo de secretos si el script circundante usa trazado.

set +x
IFS= read -r -s -p 'API token: ' api_token
printf '\n'
set -x

Incluso mejor, evite volver a activar xtrace hasta que el token ya no sea referenciado por comandos.

Valide por lista blanca, no por escapado deseado

La validación de entrada debe coincidir con el trabajo. Un nombre de rama, un nombre de usuario, un número de puerto y una descripción de forma libre son diferentes tipos de texto. No sanitice todo con una función vaga.

Para un entorno de despliegue simple, permita solo valores conocidos:

IFS= read -r -p 'Environment [dev/staging/prod]: ' env_name

case $env_name in
  dev|staging|prod) ;;
  *)
    printf 'Invalid environment: %s\n' "$env_name" >&2
    exit 1
    ;;
esac

Para un puerto TCP, verifique tanto la forma como el rango:

IFS= read -r -p 'Port: ' port

if ! [[ $port =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
  printf 'Enter a port from 1 to 65535.\n' >&2
  exit 1
fi

Para un nombre de archivo local, decida qué permite realmente. Si su script solo admite un nombre de archivo simple en el directorio actual, dígalo y rechace barras:

IFS= read -r -p 'Output filename: ' filename

if ! [[ $filename =~ ^[A-Za-z0-9._-]+$ ]]; then
  printf 'Use only letters, numbers, dot, underscore, and dash.\n' >&2
  exit 1
fi

printf 'Writing to %s\n' "$filename"

Evite el patrón de construir una cadena de comando y luego ejecutarla con eval. printf %q puede mostrar una representación escapada del shell, pero no es una licencia para ensamblar comandos no confiables. Prefiera arrays para que el shell mantenga cada argumento separado:

cmd=(tar -czf "$filename.tar.gz" "$filename")
"${cmd[@]}"

Lea múltiples valores solo cuando la división sea intencional

read first last divide en IFS. Si el usuario ingresa más palabras que variables, la última variable recibe el resto. Eso puede ser útil para nombres, pero también puede sorprenderlo.

IFS= read -r -p 'First and last name: ' first_name last_name

Si la entrada es Mary Jane Watson, first_name se convierte en Mary y last_name se convierte en Jane Watson. Si necesita toda la línea, lea en una variable. Si necesita entrada estructurada, elija un delimitador y analícelo deliberadamente.

Para valores separados por dos puntos:

IFS=: read -r host port <<<"$target"

Luego valide ambos campos. No asuma que el delimitador apareció.

Maneje valores predeterminados sin ocultar errores

Los valores predeterminados son útiles cuando son visibles:

IFS= read -r -p 'Log level [INFO]: ' log_level
log_level=${log_level:-INFO}

Para operaciones destructivas, evite valores predeterminados que hagan lo peligroso. Un prompt como Delete data? [y/N] debe tratar Enter como no, no sí.

IFS= read -r -p 'Delete local cache? [y/N] ' answer
case $answer in
  y|Y|yes|YES) rm -rf -- "$cache_dir" ;;
  *) printf 'Cache left in place.\n' ;;
esac

Observe el -- antes de la ruta. Eso evita que un nombre de archivo que comienza con - sea interpretado como una opción por rm.

Haga que los prompts funcionen en tuberías y scripts

Si su script lee datos de la entrada estándar, un prompt interactivo puede consumir accidentalmente los datos canalizados en lugar de leer de la terminal. En ese caso, lea los prompts desde /dev/tty:

printf 'Continue? [y/N] ' > /dev/tty
IFS= read -r answer < /dev/tty

Este patrón es útil para herramientas como:

generate-list | ./review-and-delete.sh

El script puede procesar registros canalizados desde stdin mientras aún pregunta al operador por confirmación en la terminal de control.

Una pequeña función de prompt reutilizable

Para scripts con varios prompts, un pequeño ayudante mantiene el comportamiento consistente:

prompt_required() {
  local label=$1 value

  while true; do
    IFS= read -r -p "$label: " value || return 1
    if [[ -n $value ]]; then
      printf '%s\n' "$value"
      return 0
    fi
    printf '%s is required.\n' "$label" >&2
  done
}

project_name=$(prompt_required 'Project name') || exit 1

La función imprime el valor aceptado en stdout, para que los llamadores puedan capturarlo. Los errores van a stderr. Eso la mantiene utilizable en sustitución de comandos sin mezclar prompts y resultados.

La versión corta: read es lo suficientemente seguro cuando mantiene el texto como datos. Use IFS= read -r, verifique fallos, oculte secretos con expectativas realistas, valide para lo exacto que planea hacer, y pase valores como argumentos citados o elementos de array. La mayoría de los errores de Bash relacionados con entrada desaparecen cuando esos hábitos se vuelven automáticos.

Evite prompts de sí/no que aceptan demasiado

Un prompt de confirmación debe ser aburrido y estricto. No trate cualquier respuesta no vacía como aprobación. He visto scripts usar este patrón:

read -r -p 'Continue? ' answer
if [[ $answer ]]; then
  deploy_to_production
fi

Eso significa que no, wait y what does this do? cuentan como sí. Use una declaración case y haga que el valor predeterminado sea seguro:

IFS= read -r -p 'Deploy to production? Type yes to continue: ' answer
case $answer in
  yes) deploy_to_production ;;
  *)
    printf 'Deployment cancelled.\n' >&2
    exit 1
    ;;
esac

Para operaciones especialmente riesgosas, requerir el nombre exacto del recurso es mejor que un prompt de sí/no:

printf 'Type %s to delete this namespace: ' "$namespace"
IFS= read -r confirmation

if [[ $confirmation != "$namespace" ]]; then
  printf 'Name did not match. Nothing deleted.\n' >&2
  exit 1
fi

Esto protege contra alguien que presiona Enter a través de un prompt que no leyó.

Tenga cuidado con opciones solo de terminal

Algunas opciones de read asumen una terminal. La entrada silenciosa, los prompts y los tiempos de espera están diseñados para uso interactivo. Si su script puede ejecutarse en CI, un entrypoint de Docker o cron, verifique si stdin es una terminal:

if [[ -t 0 ]]; then
  IFS= read -r -p 'Release name: ' release_name
else
  release_name=${RELEASE_NAME:?RELEASE_NAME is required in non-interactive mode}
fi

Esto da a los humanos un prompt y a la automatización un contrato claro de variable de entorno. También evita que un trabajo de compilación se cuelgue hasta que la plataforma lo mate.

No use read para formatos estructurados cuando existe un analizador

Está bien leer un valor simple de una persona. Está menos bien analizar JSON, YAML, CSV o sintaxis de shell con un bucle read casual a menos que el formato sea genuinamente simple. Una coma dentro de un campo CSV o una comilla dentro de JSON puede romper rápidamente el análisis escrito a mano.

Para JSON, use jq. Para archivos .env, prefiera un formato deliberadamente pequeño y documéntelo. Si lee configuración basada en líneas, preserve la línea y omita comentarios explícitamente:

while IFS= read -r line; do
  [[ -z $line || $line == \#* ]] && continue
  printf 'config line: %s\n' "$line"
done < settings.conf

Ese bucle no analiza mágicamente cada formato de configuración. Solo lee líneas fielmente, que es el punto de partida correcto.

Una revisión del mundo real antes de enviar

Antes de llamar terminado a un script o configuración de contenedor, léalo una vez como si fuera la próxima persona que tiene que depurarlo a las 2 a.m. Eso cambia lo que nota. Un prompt que tenía sentido mientras escribía el script puede ser ambiguo cuando aparece en un registro de CI. Un nombre de servicio de Docker que parecía obvio puede no coincidir con el nombre de variable en la aplicación. Un valor predeterminado de Bash puede ser seguro para desarrollo y peligroso para producción.

Me gusta hacer una prueba en seco corta con valores deliberadamente incómodos. Use una ruta con espacios. Use un valor opcional vacío. Pruebe un nombre de archivo que comience con un guión. Ejecute el script desde un directorio de trabajo diferente. Inicie el contenedor sin una variable de entorno esperada. Estas pruebas no son elegantes, pero detectan las suposiciones que generalmente se rompen primero.

También verifique el mensaje de error. Si la única salida es failed, el consejo del artículo no ha llegado a la implementación. Un error útil dice qué valor se usó, qué verificación falló y qué puede cambiar el operador. Eso no significa volcar cada variable de entorno o imprimir secretos. Significa ser específico donde la especificidad ayuda: la ruta de configuración, el nombre del comando faltante, el nombre de la red, el nombre de host del servicio o el puerto que el proceso intentó enlazar.

El hábito final es mantener los ejemplos cerca de la forma en que el sistema se ejecuta realmente. Si la producción usa Compose, pruebe con Compose. Si un script es lanzado por systemd, pruébelo con systemd o con un entorno similarmente mínimo. Si se supone que un comando es seguro para copiar y pegar, incluya las comillas, los separadores -- y la validación en el ejemplo mismo. Los lectores copian patrones de trabajo más a menudo de lo que copian advertencias.

Esa revisión no es burocracia. Es cómo la automatización pequeña se mantiene aburrida. Aburrido es lo que quiere de los prompts de shell, cargadores de configuración, expansión de variables, diagnósticos de contenedores y redes de Docker. Cuanto menos sorprendente sea el comportamiento, más fácil será para el próximo operador confiar en él.