Bref contre lisibilité: un terrain d'entente
Comme vous l'avez vu, ce problème admet des solutions qui sont modérément longues et quelque peu répétitives mais très lisibles ( réponses bash de terdon et AB ), ainsi que celles qui sont très courtes mais non intuitives et beaucoup moins auto-documentées ( python de Tim et réponses bash et réponse perl de glenn jackman ). Toutes ces approches sont précieuses.
Vous pouvez également résoudre ce problème avec du code au milieu du continuum entre compacité et lisibilité. Cette approche est presque aussi lisible que les solutions plus longues, avec une longueur plus proche des petites solutions ésotériques.
#!/usr/bin/env bash
read -erp 'Enter numeric grade (q to quit): '
case $REPLY in [qQ]) exit;; esac
declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100
for letter in F D C B A; do
((REPLY <= cutoffs[$letter])) && { echo $letter; exit; }
done
echo "Grade out of range."
Dans cette solution bash, j'ai inclus quelques lignes vides pour améliorer la lisibilité, mais vous pouvez les supprimer si vous le souhaitez encore plus court.
Les lignes vierges incluses, ce n'est en fait que légèrement plus court qu'une variante compactée et encore assez lisible de la solution bash d' AB . Ses principaux avantages par rapport à cette méthode sont:
- C'est plus intuitif.
- Il est plus facile de modifier les limites entre les notes (ou d'ajouter des notes supplémentaires).
- Il accepte automatiquement les entrées avec des espaces de début et de fin (voir ci-dessous pour une explication du
((
))
fonctionnement).
Ces trois avantages découlent du fait que cette méthode utilise l'entrée de l'utilisateur comme données numériques plutôt qu'en examinant manuellement ses chiffres constitutifs.
Comment ça fonctionne
- Lisez les entrées de l'utilisateur. Laissez-les utiliser les touches fléchées pour se déplacer dans le texte qu'ils ont entré (
-e
) et ne pas interpréter \
comme un caractère d'échappement ( -r
).
Ce script n'est pas une solution riche en fonctionnalités - voir ci-dessous pour un raffinement - mais ces fonctionnalités utiles ne font que deux caractères de plus. Je recommande de toujours utiliser -r
avec read
, sauf si vous savez que vous devez laisser l’utilisateur \
s’échapper.
- Si l'utilisateur a écrit
q
ou Q
, quittez.
- Créez un tableau associatif ( ). Remplissez-le avec le grade numérique le plus élevé associé à chaque grade de lettre.
declare -A
- Parcourez les notes des lettres du plus bas au plus élevé, en vérifiant si le nombre fourni par l'utilisateur est suffisamment bas pour tomber dans la plage numérique de chaque lettre.
Avec ((
))
l'évaluation arithmétique, les noms de variables n'ont pas besoin d'être développés avec $
. (Dans la plupart des autres situations, si vous souhaitez utiliser la valeur d'une variable à la place de son nom, vous devez le faire .)
- S'il tombe dans la plage, imprimez la note et quittez .
Par souci de concision, j'utilise le court-circuit et l' opérateur ( &&
) plutôt qu'un if
- then
.
- Si la boucle se termine et qu'aucune plage n'a été mise en correspondance, supposez que le nombre entré est trop élevé (supérieur à 100) et informez l'utilisateur qu'il était hors plage.
Comment cela se comporte, avec une entrée étrange
Comme les autres solutions courtes publiées, ce script ne vérifie pas l'entrée avant de supposer qu'il s'agit d'un nombre. L'évaluation arithmétique ( ((
))
) supprime automatiquement les espaces blancs de début et de fin, ce n'est donc pas un problème, mais:
- Une entrée qui ne ressemble pas du tout à un nombre est interprétée comme 0.
- Avec une entrée qui ressemble à un nombre (c'est-à-dire si elle commence par un chiffre) mais contient des caractères non valides, le script émet des erreurs.
- L'entrée à plusieurs chiffres commençant par
0
est interprétée comme étant en octal . Par exemple, le script vous dira que 77 est un C, tandis que 077 est un D. Bien que certains utilisateurs le veuillent, probablement pas et cela peut créer de la confusion.
- Sur le plan positif, lorsqu'il reçoit une expression arithmétique, ce script la simplifie automatiquement et détermine le grade de lettre associé. Par exemple, il vous dira que 320/4 est un B.
Une version étendue et entièrement en vedette
Pour ces raisons, vous souhaiterez peut-être utiliser quelque chose comme ce script étendu, qui vérifie que l'entrée est bonne et inclut d'autres améliorations.
#!/usr/bin/env bash
shopt -s extglob
declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100
while read -erp 'Enter numeric grade (q to quit): '; do
case $REPLY in # allow leading/trailing spaces, but not octal (e.g. "03")
*( )@([1-9]*([0-9])|+(0))*( )) ;;
*( )[qQ]?([uU][iI][tT])*( )) exit;;
*) echo "I don't understand that number."; continue;;
esac
for letter in F D C B A; do
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
done
echo "Grade out of range."
done
C'est toujours une solution assez compacte.
Quelles fonctionnalités cela ajoute-t-il?
Les points clés de ce script étendu sont:
- Validation des entrées. Le script de terdon vérifie l'entrée avec , donc je montre une autre façon, qui sacrifie une certaine brièveté mais est plus robuste, permettant à l'utilisateur d'entrer dans les espaces de début et de fin et refusant d'autoriser une expression qui pourrait ou non être conçue comme octale (sauf si elle est nulle) .
if [[ ! $response =~ ^[0-9]*$ ]] ...
- J'ai utilisé
case
avec l' extension globale au lieu de [[
l' opérateur de =~
correspondance d'expressions régulières (comme dans la réponse de terdon ). Je l'ai fait pour montrer que (et comment) cela peut aussi être fait de cette façon. Globs et regexps sont deux façons de spécifier des modèles qui correspondent au texte, et l'une ou l'autre méthode convient à cette application.
- Comme le script bash d'AB , j'ai enfermé le tout dans une boucle externe (sauf la création initiale du
cutoffs
tableau). Il demande des chiffres et donne des notes de lettres correspondantes tant que l'entrée du terminal est disponible et que l'utilisateur ne lui a pas dit de quitter. À en juger par do
... done
autour du code dans votre question, il semble que vous le vouliez.
- Pour faciliter la fermeture, j'accepte toute variante insensible à la casse de
q
ou quit
.
Ce script utilise quelques constructions qui peuvent ne pas être familières aux novices; ils sont détaillés ci-dessous.
Explication: utilisation de continue
Lorsque je veux sauter le reste du corps de la while
boucle externe , j'utilise la continue
commande. Cela le ramène au sommet de la boucle, pour lire plus d'entrée et exécuter une autre itération.
La première fois que je fais cela, la seule boucle dans laquelle je suis est la while
boucle externe , donc je peux appeler continue
sans argument. (Je suis dans une case
construction, mais cela n'affecte pas le fonctionnement de break
ou continue
.)
*) echo "I don't understand that number."; continue;;
La deuxième fois, cependant, je suis dans une for
boucle interne qui est elle-même imbriquée dans la while
boucle externe . Si j'utilisais continue
sans argument, ce serait équivalent continue 1
et continuerait la for
boucle interne au lieu de la while
boucle externe .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Donc, dans ce cas, j'utilise continue 2
pour faire bash find et continuer la deuxième boucle à la place.
Explication: case
étiquettes avec des globes
Je ne me case
pour savoir quelle classe lettre bin un certain nombre tombe dans (comme dans la réponse bash AB ). Mais j'utilise case
pour décider si l'entrée de l'utilisateur doit être prise en compte:
- un numéro valide,
*( )@([1-9]*([0-9])|+(0))*( )
- la commande quit,
*( )[qQ]?([uU][iI][tT])*( )
- toute autre chose (et donc une entrée invalide),
*
Ce sont des globes de coquille .
- Chacun est suivi par un
)
qui ne correspond à aucune ouverture (
, qui est case
la syntaxe pour séparer un modèle des commandes qui s'exécutent lorsqu'il est mis en correspondance.
;;
est case
la syntaxe utilisée pour indiquer la fin des commandes à exécuter pour une correspondance de cas particulier (et qu'aucun cas ultérieur ne doit être testé après leur exécution).
La globalisation du shell ordinaire permet de *
faire correspondre zéro ou plusieurs caractères, de ?
faire correspondre exactement un caractère et les classes / plages de caractères [
]
entre parenthèses. Mais j'utilise un globbing étendu , qui va au-delà. La globalisation étendue est activée par défaut lors d'une utilisation bash
interactive, mais elle est désactivée par défaut lors de l'exécution d'un script. La shopt -s extglob
commande en haut du script l'allume.
Explication: Globbing étendu
*( )@([1-9]*([0-9])|+(0))*( )
, qui vérifie la saisie numérique , correspond à une séquence de:
- Zéro ou plusieurs espaces (
*( )
). La *(
)
construction correspond à zéro ou plus du motif entre parenthèses, qui n'est ici qu'un espace.
Il existe en fait deux types d'espaces blancs horizontaux, des espaces et des tabulations, et il est souvent souhaitable de faire correspondre les tabulations également. Mais je ne m'inquiète pas à ce sujet ici, car ce script est écrit pour une entrée manuelle et interactive, et l' -e
indicateur pour read
activer GNU readline. C'est ainsi que l'utilisateur peut se déplacer d'avant en arrière dans son texte avec les touches fléchées gauche et droite, mais cela a pour effet secondaire d'empêcher généralement la saisie littérale des onglets.
- Une occurrence (
@(
)
) de soit ( |
):
- Un chiffre différent de zéro (
[1-9]
) suivi de zéro ou plus ( *(
)
) de tout chiffre ( [0-9]
).
- Un ou plusieurs (
+(
)
) de 0
.
- Zéro ou plusieurs espaces (
*( )
), encore une fois.
*( )[qQ]?([uU][iI][tT])*( )
, qui vérifie la commande quit , correspond à une séquence de:
- Zéro ou plusieurs espaces (
*( )
).
q
ou Q
( [qQ]
).
- Facultativement - c'est-à-dire zéro ou une occurrence (
?(
)
) - de:
u
ou U
( [uU]
) suivi de i
ou I
( [iI]
) suivi de t
ou T
( [tT]
).
- Zéro ou plusieurs espaces (
*( )
), encore une fois.
Variante: validation de l'entrée avec une expression régulière étendue
Si vous préférez tester l'entrée de l'utilisateur par rapport à une expression régulière plutôt qu'à un glob de shell, vous préférerez peut-être utiliser cette version, qui fonctionne de la même façon, mais utilise [[
et =~
(comme dans la réponse de terdon ) au lieu de case
et un globbing étendu.
#!/usr/bin/env bash
shopt -s nocasematch
declare -A cutoffs
cutoffs[F]=59 cutoffs[D]=69 cutoffs[C]=79 cutoffs[B]=89 cutoffs[A]=100
while read -erp 'Enter numeric grade (q to quit): '; do
# allow leading/trailing spaces, but not octal (e.g., "03")
if [[ ! $REPLY =~ ^\ *([1-9][0-9]*|0+)\ *$ ]]; then
[[ $REPLY =~ ^\ *q(uit)?\ *$ ]] && exit
echo "I don't understand that number."; continue
fi
for letter in F D C B A; do
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
done
echo "Grade out of range."
done
Les avantages possibles de cette approche sont les suivants:
Dans ce cas particulier, la syntaxe est un peu plus simple, au moins dans le deuxième modèle, où je vérifie la commande quit. En effet, j'ai pu définir l' nocasematch
option shell, puis toutes les variantes de cas de q
et quit
ont été couvertes automatiquement.
C'est ce que fait la shopt -s nocasematch
commande. La shopt -s extglob
commande est omise car la globalisation n'est pas utilisée dans cette version.
Les compétences d'expression régulière sont plus courantes que la maîtrise des extglobs de bash.
Explication: Expressions régulières
En ce qui concerne les modèles spécifiés à droite de l' =~
opérateur, voici comment ces expressions régulières fonctionnent.
^\ *([1-9][0-9]*|0+)\ *$
, qui vérifie la saisie numérique , correspond à une séquence de:
- Le début - c'est-à-dire le bord gauche - de la ligne (
^
).
- Zéro ou plusieurs
*
espaces ( , postfixés appliqués). Un espace n'a généralement pas besoin d'être \
échappé dans une expression régulière, mais cela est nécessaire avec [[
pour éviter une erreur de syntaxe.
- Une sous-chaîne (
(
)
) qui est l'une ou l'autre ( |
) de:
[1-9][0-9]*
: un chiffre différent de zéro ( [1-9]
) suivi de zéro ou plus ( *
, suffixe appliqué) de tout chiffre ( [0-9]
).
0+
: un ou plusieurs ( +
, suffixe appliqué) de 0
.
- Zéro ou plusieurs espaces (
\ *
), comme précédemment.
- La fin - c'est-à-dire le bord droit - de la ligne (
$
).
Contrairement aux case
étiquettes, qui correspondent à l'expression entière testée, =~
renvoie true si une partie de son expression de gauche correspond au modèle donné comme expression de droite. C'est pourquoi les ancres ^
et $
, spécifiant le début et la fin de la ligne, sont nécessaires ici, et ne correspondent pas syntaxiquement à tout ce qui apparaît dans la méthode avec case
et extglobs.
Les parenthèses sont nécessaires pour faire ^
et $
se lier à la disjonction de [1-9][0-9]*
et 0+
. Sinon, ce serait la disjonction de ^[1-9][0-9]*
et 0+$
, et correspondrait à toute entrée commençant par un chiffre différent de zéro ou se terminant par un 0
(ou les deux, qui pourrait toujours inclure des chiffres non intermédiaires).
^\ *q(uit)?\ *$
, qui vérifie la commande quit , correspond à une séquence de:
- Le début de la ligne (
^
).
- Zéro ou plusieurs espaces (
\ *
voir l'explication ci-dessus).
- La lettre
q
. Ou Q
, puisque shopt nocasematch
est activé.
- Facultativement - c'est-à-dire zéro ou une occurrence (suffixe
?
) - de la sous-chaîne ( (
)
):
u
, suivi de i
, suivi de t
. Ou, puisque shopt nocasematch
est activé u
peut être U
; indépendamment, i
peut être I
; et indépendamment, t
peut être T
. (Autrement dit, les possibilités ne sont pas limitées à uit
et UIT
.)
- Zéro ou plusieurs espaces à nouveau (
\ *
).
- La fin de la ligne (
$
).