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.