Accepter les entrées utilisateur en toute sécurité : techniques essentielles pour la commande Bash read

Apprenez à accepter les entrées utilisateur de manière sécurisée et efficace dans les scripts Bash à l'aide de la commande `read`. Ce guide couvre les techniques essentielles pour les invites, la gestion silencieuse des mots de passe avec `-s`, la définition de délais d'attente avec `-t`, et la validation et l'assainissement de base des entrées pour créer des scripts interactifs plus robustes et sécurisés.

Accepter les entrées utilisateur en toute sécurité : techniques essentielles pour la commande Bash read

La commande Bash read semble inoffensive jusqu'à ce que la valeur collectée soit utilisée dans un chemin de fichier, un argument de commande ou une invite de mot de passe. La plupart des problèmes ne viennent pas de read elle-même. Ils viennent du fait de faire trop tôt confiance au texte, d'oublier que les espaces et les métacaractères du shell sont des entrées utilisateur normales, ou de laisser un script se bloquer indéfiniment parce que personne n'a répondu à l'invite.

Un bon script Bash interactif traite les entrées comme du texte non fiable. Il demande clairement, lit attentivement, valide avant d'agir et garde les secrets hors des journaux. Cela semble formel, mais la version quotidienne est simple : mettez les variables entre guillemets, utilisez IFS= read -r par défaut, vérifiez le statut de retour et rejetez les valeurs que vous ne savez pas gérer.

Commencez par la valeur par défaut la plus sûre

Pour la plupart des invites sur une seule ligne, voici le modèle que j'utilise :

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

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

Il y a deux détails à retenir. IFS= empêche Bash de supprimer les espaces de début et de fin lors de la lecture. -r indique à read de ne pas traiter les antislashs comme des caractères d'échappement. Sans -r, quelqu'un qui saisit C:\Users\me ou une chaîne contenant \n peut ne pas récupérer le texte exact qu'il a tapé.

Vous pouvez également utiliser -p pour une invite :

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

C'est bien pour un terminal interactif. J'utilise encore printf quand je veux que l'invite et la lecture soient plus faciles à tester séparément, ou quand j'ai besoin d'habitudes de portabilité plus strictes concernant le formatage de la sortie.

Vérifiez si la lecture a réellement réussi

read renvoie un statut. Utilisez-le. Un échec de lecture peut signifier une fin de fichier, un délai d'attente ou un terminal interrompu. Si la ligne suivante de votre script suppose que la variable est significative, vous pouvez accidentellement exécuter une commande avec une valeur obsolète ou une chaîne vide.

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

C'est important dans les scripts qui sont parfois exécutés par une personne et parfois dans un environnement CI. Dans un travail non interactif, read peut immédiatement atteindre la fin du fichier. Une erreur claire est bien meilleure qu'une commande de déploiement exécutée avec une balise vide.

Utilisez des délais d'attente pour les invites qui ne doivent pas bloquer indéfiniment

Un script de maintenance qui attend une confirmation peut silencieusement bloquer un déploiement ou un travail cron. read -t définit un délai d'attente en secondes :

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

La prise en charge des délais d'attente est une fonctionnalité de Bash, pas une fonctionnalité POSIX sh. C'est généralement acceptable pour un article sur Bash, mais il est bon de s'en souvenir si un script peut être exécuté avec /bin/sh sur une petite image de base.

Cachez les mots de passe, mais ne prétendez pas qu'ils sont protégés pour toujours

read -s empêche l'affichage des caractères tapés dans le 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

Cela protège contre le regard par-dessus l'épaule et le défilement arrière du terminal. Cela ne transforme pas Bash en un gestionnaire de secrets sécurisé. La valeur existe toujours dans une variable shell pendant l'exécution du script. Ne l'imprimez pas avec set -x activé, ne la transmettez pas via des lignes de commande qui apparaissent dans les listes de processus et ne l'écrivez pas dans des fichiers temporaires. Si le secret est destiné à un flux de production sérieux, préférez un coffre-fort de secrets, un fichier de jeton avec des permissions strictes ou l'invite de mot de passe native de l'outil cible.

Une règle pratique : désactivez xtrace autour de la gestion des secrets si le script environnant utilise le traçage.

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

Encore mieux, évitez de réactiver xtrace tant que le jeton n'est plus référencé par des commandes.

Validez par liste blanche, pas par échappement approximatif

La validation des entrées doit correspondre à la tâche. Un nom de branche, un nom d'utilisateur, un numéro de port et une description libre sont différents types de texte. Ne nettoyez pas tout avec une seule fonction vague.

Pour un environnement de déploiement simple, n'autorisez que les valeurs connues :

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

Pour un port TCP, vérifiez à la fois la forme et la plage :

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

Pour un nom de fichier local, décidez ce que vous autorisez réellement. Si votre script ne prend en charge qu'un nom de fichier simple dans le répertoire courant, dites-le et rejetez les barres obliques :

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"

Évitez le modèle consistant à construire une chaîne de commande et à l'exécuter avec eval. printf %q peut afficher une représentation échappée par le shell, mais ce n'est pas une licence pour assembler des commandes non fiables. Préférez les tableaux pour que le shell garde chaque argument séparé :

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

Lisez plusieurs valeurs uniquement lorsque la séparation est intentionnelle

read first last divise sur IFS. Si l'utilisateur saisit plus de mots que de variables, la dernière variable reçoit le reste. Cela peut être utile pour les noms, mais cela peut aussi vous surprendre.

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

Si l'entrée est Mary Jane Watson, first_name devient Mary et last_name devient Jane Watson. Si vous avez besoin de toute la ligne, lisez dans une seule variable. Si vous avez besoin d'une entrée structurée, choisissez un délimiteur et analysez-la délibérément.

Pour les valeurs séparées par deux-points :

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

Validez ensuite les deux champs. Ne supposez pas que le délimiteur est apparu.

Gérez les valeurs par défaut sans cacher les erreurs

Les valeurs par défaut sont utiles lorsqu'elles sont visibles :

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

Pour les opérations destructrices, évitez les valeurs par défaut qui font la chose dangereuse. Une invite comme Delete data? [y/N] doit traiter Entrée comme non, pas oui.

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

Remarquez le -- avant le chemin. Cela empêche un nom de fichier commençant par - d'être interprété comme une option par rm.

Faites fonctionner les invites dans les pipelines et les scripts

Si votre script lit des données depuis l'entrée standard, une invite interactive peut accidentellement consommer les données redirigées au lieu de lire depuis le terminal. Dans ce cas, lisez les invites depuis /dev/tty :

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

Ce modèle est utile pour des outils tels que :

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

Le script peut traiter les enregistrements redirigés depuis stdin tout en demandant une confirmation à l'opérateur sur le terminal de contrôle.

Une petite fonction d'invite réutilisable

Pour les scripts avec plusieurs invites, un petit assistant maintient un comportement cohérent :

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 fonction imprime la valeur acceptée sur stdout, afin que les appelants puissent la capturer. Les erreurs vont sur stderr. Cela la rend utilisable dans la substitution de commande sans mélanger les invites et les résultats.

La version courte : read est suffisamment sûr lorsque vous gardez le texte comme données. Utilisez IFS= read -r, vérifiez les échecs, cachez les secrets avec des attentes réalistes, validez exactement ce que vous prévoyez de faire et transmettez les valeurs comme des arguments entre guillemets ou des éléments de tableau. La plupart des bugs Bash liés aux entrées disparaissent lorsque ces habitudes deviennent automatiques.

Évitez les invites oui/non qui acceptent trop de choses

Une invite de confirmation doit être ennuyeuse et stricte. Ne traitez pas toute réponse non vide comme une approbation. J'ai vu des scripts utiliser ce modèle :

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

Cela signifie que no, wait et what does this do? comptent tous comme oui. Utilisez une instruction case et rendez la valeur par défaut sûre :

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

Pour les opérations particulièrement risquées, exiger le nom exact de la ressource est mieux qu'une invite oui/non :

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

Cela protège contre quelqu'un qui appuie sur Entrée à travers une invite qu'il n'a pas lue.

Soyez prudent avec les options réservées au terminal

Certaines options de read supposent un terminal. La saisie silencieuse, les invites et les délais d'attente sont conçus pour une utilisation interactive. Si votre script peut s'exécuter dans un environnement CI, un point d'entrée Docker ou cron, vérifiez si stdin est un 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

Cela donne aux humains une invite et à l'automatisation un contrat de variable d'environnement clair. Cela empêche également un travail de build de se bloquer jusqu'à ce que la plateforme le tue.

N'utilisez pas read pour des formats structurés lorsqu'un analyseur existe

Il est acceptable de lire une valeur simple d'une personne. Il l'est moins d'analyser du JSON, YAML, CSV ou une syntaxe shell avec une boucle read occasionnelle, sauf si le format est vraiment simple. Une virgule dans un champ CSV ou un guillemet dans du JSON peut rapidement casser une analyse écrite à la main.

Pour JSON, utilisez jq. Pour les fichiers .env, préférez un format délibérément petit et documentez-le. Si vous lisez une configuration ligne par ligne, préservez la ligne et ignorez les commentaires explicitement :

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

Cette boucle n'analyse pas magiquement tous les formats de configuration. Elle lit simplement les lignes fidèlement, ce qui est le bon point de départ.

Une revue de passage dans le monde réel avant de livrer

Avant de déclarer un script ou une configuration de conteneur terminée, lisez-le une fois comme si vous étiez la personne suivante qui devra le déboguer à 2 heures du matin. Cela change ce que vous remarquez. Une invite qui avait du sens lors de l'écriture du script peut être ambiguë lorsqu'elle apparaît dans un journal CI. Un nom de service Docker qui semblait évident peut ne pas correspondre au nom de variable dans l'application. Une valeur par défaut Bash peut être sûre pour le développement et dangereuse pour la production.

J'aime faire un court essai à sec avec des valeurs délibérément gênantes. Utilisez un chemin avec des espaces. Utilisez une valeur optionnelle vide. Essayez un nom de fichier qui commence par un tiret. Exécutez le script depuis un répertoire de travail différent. Démarrez le conteneur sans une variable d'environnement attendue. Ces tests ne sont pas sophistiqués, mais ils attrapent les hypothèses qui se brisent généralement en premier.

Vérifiez également le message d'échec. Si la seule sortie est failed, les conseils de l'article ne sont pas passés dans l'implémentation. Un échec utile indique quelle valeur a été utilisée, quelle vérification a échoué et ce que l'opérateur peut modifier. Cela ne signifie pas vider chaque variable d'environnement ou imprimer des secrets. Cela signifie être spécifique là où la spécificité aide : le chemin de configuration, le nom de commande manquant, le nom de réseau, le nom d'hôte du service ou le port que le processus a essayé de lier.

La dernière habitude est de garder les exemples proches de la façon dont le système est réellement exécuté. Si la production utilise Compose, testez avec Compose. Si un script est lancé par systemd, testez-le avec systemd ou avec un environnement tout aussi minimal. Si une commande est censée être sûre pour le copier-coller, incluez les guillemets, les séparateurs -- et la validation dans l'exemple lui-même. Les lecteurs copient les modèles de travail plus souvent qu'ils ne copient les avertissements.

Cette revue de passage n'est pas de la bureaucratie. C'est ainsi que la petite automatisation reste ennuyeuse. L'ennui est ce que vous voulez des invites shell, des chargeurs de configuration, de l'expansion de variables, des diagnostics de conteneurs et du réseau Docker. Moins le comportement est surprenant, plus il est facile pour l'opérateur suivant de lui faire confiance.