Brevedad vs. Legibilidad: un término medio
Como ha visto, este problema admite soluciones que son moderadamente largas y algo repetitivas pero altamente legibles ( respuestas bash de terdon y AB ), así como aquellas que son muy cortas pero no intuitivas y mucho menos auto-documentadas (la pitón de Tim y bash respuestas y la respuesta perl de glenn jackman ). Todos estos enfoques son valiosos.
También puede resolver este problema con código en el medio del continuo entre compacidad y legibilidad. Este enfoque es casi tan legible como las soluciones más largas, con una longitud más cercana a las soluciones pequeñas y esotéricas.
#!/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."
En esta solución bash, he incluido algunas líneas en blanco para mejorar la legibilidad, pero puede eliminarlas si lo desea aún más corto.
Se incluyen líneas en blanco, esto en realidad es solo un poco más corto que una variante compacta, aún bastante legible, de la solución bash de AB . Sus principales ventajas sobre ese método son:
- Es mas intuitivo.
- Es más fácil cambiar los límites entre las calificaciones (o agregar calificaciones adicionales).
- Acepta automáticamente la entrada con espacios iniciales y finales (consulte a continuación para obtener una explicación de cómo
((
))
funciona).
Las tres ventajas surgen porque este método utiliza la entrada del usuario como datos numéricos en lugar de examinar manualmente sus dígitos constituyentes.
Cómo funciona
- Leer la entrada del usuario. Permítales usar las teclas de flecha para moverse en el texto que ingresaron (
-e
) y no interpretarlo \
como un carácter de escape ( -r
).
Esta secuencia de comandos no es una solución rica en funciones, consulte a continuación para un refinamiento, pero esas funciones útiles solo hacen que sean dos caracteres más largos. Recomiendo usar siempre -r
con read
, a menos que sepa que necesita dejar que el usuario proporcione \
escapes.
- Si el usuario escribió
q
o Q
, salga.
- Crear una matriz asociativa ( ). Rellene con la calificación numérica más alta asociada con cada calificación de letra.
declare -A
- Recorra las calificaciones de letras de menor a mayor, verificando si el número proporcionado por el usuario es lo suficientemente bajo como para caer en el rango numérico de cada calificación de letra.
Con la ((
))
evaluación aritmética, no es necesario expandir los nombres de las variables $
. (En la mayoría de las otras situaciones, si desea utilizar el valor de una variable en lugar de su nombre, debe hacerlo ).
- Si cae en el rango, imprima el grado y salga .
Por brevedad, uso el cortocircuito y el operador ( &&
) en lugar de un if
- then
.
- Si el ciclo finaliza y no se ha igualado ningún rango, suponga que el número ingresado es demasiado alto (más de 100) y dígale al usuario que está fuera de rango.
Cómo se comporta esto, con información extraña
Al igual que las otras soluciones cortas publicadas, ese script no verifica la entrada antes de asumir que es un número. La evaluación aritmética ( ((
))
) elimina automáticamente los espacios en blanco iniciales y finales, por lo que no hay problema, pero:
- La entrada que no parece un número en absoluto se interpreta como 0.
- Con una entrada que parece un número (es decir, si comienza con un dígito) pero contiene caracteres no válidos, el script emite errores.
- De entrada de múltiples dígitos empezando con
0
se interpreta como siendo en octal . Por ejemplo, el script le dirá que 77 es una C, mientras que 077 es una D. Aunque algunos usuarios pueden querer esto, probablemente no lo hagan y puede causar confusión.
- En el lado positivo, cuando se le da una expresión aritmética, este script lo simplifica automáticamente y determina el grado de letra asociado. Por ejemplo, le dirá que 320/4 es una B.
Una versión ampliada y totalmente destacada
Por esas razones, es posible que desee usar algo como este script expandido, que verifica para asegurarse de que la entrada sea buena e incluye algunas otras mejoras.
#!/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
Esta sigue siendo una solución bastante compacta.
¿Qué características agrega esto?
Los puntos clave de este script expandido son:
- Validación de entrada. El script de terdon verifica la entrada con , así que muestro otra forma, que sacrifica algo de brevedad pero es más robusta, lo que permite al usuario ingresar espacios iniciales y finales y se niega a permitir una expresión que podría o no ser octal (a menos que sea cero) .
if [[ ! $response =~ ^[0-9]*$ ]] ...
- Lo he usado
case
con globbing extendido en lugar de [[
con el operador de =~
coincidencia de expresiones regulares (como en la respuesta de terdon ). Lo hice para mostrar que (y cómo) también se puede hacer de esa manera. Globs y regexps son dos formas de especificar patrones que coinciden con el texto, y cualquiera de los métodos está bien para esta aplicación.
- Al igual que el script bash de AB , he incluido todo en un bucle externo (excepto la creación inicial de la
cutoffs
matriz). Solicita números y otorga las letras correspondientes siempre que la entrada del terminal esté disponible y el usuario no le haya dicho que abandone. A juzgar por el do
... done
alrededor del código en tu pregunta, parece que quieres eso.
- Para que dejar de fumar sea fácil, acepto cualquier variante que distinga entre mayúsculas y minúsculas de
q
o quit
.
Este script utiliza algunas construcciones que pueden ser desconocidas para los principiantes; se detallan a continuación.
Explicación: uso de continue
Cuando quiero omitir el resto del cuerpo del while
bucle externo , utilizo el continue
comando. Esto lo lleva de vuelta a la parte superior del bucle, para leer más entradas y ejecutar otra iteración.
La primera vez que hago esto, el único bucle en el que estoy es el while
bucle externo , por lo que puedo llamar continue
sin argumento. (Estoy en una case
construcción, pero eso no afecta la operación de break
o continue
.)
*) echo "I don't understand that number."; continue;;
La segunda vez, sin embargo, estoy en un for
bucle interno que está anidado dentro del while
bucle externo . Si lo usara continue
sin argumento, esto sería equivalente continue 1
y continuaría con el for
bucle interno en lugar del while
bucle externo .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Entonces, en ese caso, uso continue 2
para hacer que bash encuentre y continúe el segundo bucle.
Explicación: case
etiquetas con globos
Yo no uso case
de averiguar qué grado de la letra bin varios cae en (como en la respuesta de fiesta AB ). Pero sí uso case
para decidir si la entrada del usuario debe considerarse:
- un número válido
*( )@([1-9]*([0-9])|+(0))*( )
- el comando para dejar de fumar,
*( )[qQ]?([uU][iI][tT])*( )
- cualquier otra cosa (y por lo tanto entrada no válida),
*
Estos son globos de concha .
- A cada uno le sigue una
)
apertura que no coincide con ninguna apertura (
, que es case
la sintaxis para separar un patrón de los comandos que se ejecutan cuando coincide.
;;
es case
la sintaxis para indicar el final de los comandos que se ejecutarán para una coincidencia de caso particular (y que no se deben probar los casos posteriores después de ejecutarlos).
El globbing de shell ordinario proporciona *
coincidir con cero o más caracteres, ?
para coincidir exactamente con un carácter y clases / rangos de caracteres [
]
entre paréntesis. Pero estoy usando globbing extendido , que va más allá de eso. El globbing extendido está habilitado de manera predeterminada cuando se usa de forma bash
interactiva, pero está deshabilitado de manera predeterminada cuando se ejecuta un script. El shopt -s extglob
comando en la parte superior del script lo activa.
Explicación: Globbing extendido
*( )@([1-9]*([0-9])|+(0))*( )
, que comprueba la entrada numérica , coincide con una secuencia de:
- Cero o más espacios (
*( )
). La *(
)
construcción coincide con cero o más del patrón entre paréntesis, que aquí es solo un espacio.
En realidad, hay dos tipos de espacios en blanco horizontales, espacios y pestañas, y a menudo también es deseable hacer coincidir las pestañas. Pero no me preocupo por eso aquí, porque este script está escrito para entrada manual, interactiva y el -e
indicador para read
habilitar la línea de lectura de GNU. Esto es para que el usuario pueda moverse hacia adelante y hacia atrás en su texto con las teclas de flecha izquierda y derecha, pero tiene el efecto secundario de evitar generalmente que las pestañas se ingresen literalmente.
- Una aparición (
@(
)
) de cualquiera de ( |
):
- Un dígito distinto de cero (
[1-9]
) seguido de cero o más ( *(
)
) de cualquier dígito ( [0-9]
).
- Uno o más (
+(
)
) de 0
.
- Cero o más espacios (
*( )
), nuevamente.
*( )[qQ]?([uU][iI][tT])*( )
, que comprueba el comando salir , coincide con una secuencia de:
- Cero o más espacios (
*( )
).
q
o Q
( [qQ]
).
- Opcionalmente, es decir, cero o una ocurrencia (
?(
)
) - de:
u
o U
( [uU]
) seguido de i
o I
( [iI]
) seguido de t
o T
( [tT]
).
- Cero o más espacios (
*( )
), nuevamente.
Variante: Validación de entrada con una expresión regular extendida
Si prefiere probar la entrada del usuario contra una expresión regular en lugar de un glob de shell, es posible que prefiera usar esta versión, que funciona igual pero usa [[
y =~
(como en la respuesta de terdon ) en lugar de case
globbing extendido.
#!/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
Las posibles ventajas de este enfoque son que:
En este caso particular, la sintaxis es un poco más simple, al menos en el segundo patrón, donde verifico el comando salir. Esto se debe a que pude establecer la nocasematch
opción de shell, y luego todas las variantes de casos q
y quit
fueron cubiertas automáticamente.
Eso es lo que hace el shopt -s nocasematch
comando. El shopt -s extglob
comando se omite ya que no se usa globbing en esta versión.
Las habilidades de expresión regular son más comunes que el dominio de los globos externos de bash.
Explicación: expresiones regulares
En cuanto a los patrones especificados a la derecha del =~
operador, así es como funcionan esas expresiones regulares.
^\ *([1-9][0-9]*|0+)\ *$
, que comprueba la entrada numérica , coincide con una secuencia de:
- El comienzo, es decir, el borde izquierdo, de la línea (
^
).
- Cero o más (
*
postfix aplicado) espacios. Normalmente, un espacio no necesita ser \
-escapado en una expresión regular, pero esto es necesario [[
para evitar un error de sintaxis.
- Una subcadena (
(
)
) que es una u otra ( |
) de:
[1-9][0-9]*
: un dígito distinto de cero ( [1-9]
) seguido de cero o más ( *
, postfix aplicado) de cualquier dígito ( [0-9]
).
0+
: uno o más ( +
, postfix aplicado) de 0
.
- Cero o más espacios (
\ *
), como antes.
- El final, es decir, el borde derecho, de la línea (
$
).
A diferencia de las case
etiquetas, que coinciden con la expresión completa que se está probando, =~
devuelve verdadero si alguna parte de su expresión de la izquierda coincide con el patrón dado como su expresión de la derecha. Es por eso que los anclajes ^
y $
, que especifican el principio y el final de la línea, son necesarios aquí, y no se corresponden sintácticamente con nada que aparezca en el método con case
y extglobs.
Los paréntesis son necesarios para hacer ^
y $
unirse a la disyunción de [1-9][0-9]*
y 0+
. De lo contrario, sería la disyunción de ^[1-9][0-9]*
y 0+$
, y coincidiría con cualquier entrada que comience con un dígito distinto de cero o que termine con un 0
(o ambos, que aún podrían incluir no dígitos entre ellos).
^\ *q(uit)?\ *$
, que comprueba el comando salir , coincide con una secuencia de:
- El comienzo de la línea (
^
).
- Cero o más espacios (
\ *
ver explicación anterior).
- La carta
q
. O Q
, ya que shopt nocasematch
está habilitado.
- Opcionalmente, es decir, cero o una aparición (postfix
?
) de la subcadena ( (
)
):
u
, seguido de i
, seguido de t
. O, ya que shopt nocasematch
está habilitado u
puede ser U
; independientemente, i
puede ser I
; e independientemente, t
puede ser T
. (Es decir, las posibilidades no se limitan a uit
y UIT
.)
- Cero o más espacios de nuevo (
\ *
).
- El final de la línea (
$
).