Brevidade x legibilidade: um meio-termo
Como você viu, este problema admite soluções que são moderadamente longas e um pouco repetitivas, mas altamente legíveis ( terdon's e respostas AB bash), bem como aquelas que são muito curtas mas não intuitivas e muito menos autodocumentação ( python do Tim e respostas bash e glenn jackman perl resposta ). Todas essas abordagens são valiosas.
Você também pode resolver esse problema com código no meio do continuum entre compacidade e legibilidade. Essa abordagem é quase tão legível quanto as soluções mais longas, com um comprimento mais próximo das pequenas soluções 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."
Nesta solução bash, incluí algumas linhas em branco para melhorar a legibilidade, mas você pode removê-las se quiser ainda menos.
Linhas em branco incluídas, isto é, na verdade, apenas ligeiramente menor do que uma variante compactada, ainda bastante legível de A solução do bas da AB . Suas principais vantagens sobre esse método são:
- É mais intuitivo.
- É mais fácil alterar os limites entre as notas (ou adicionar notas adicionais).
- Aceita automaticamente entradas com espaços iniciais e finais (veja abaixo uma explicação de como
((
))
funciona).
Todas essas três vantagens surgem porque esse método usa a entrada do usuário como dados numéricos em vez de examinar manualmente seus dígitos constituintes.
Como funciona
-
Entrada de leitura do usuário. Deixe-os usar as teclas de seta para se mover no texto eles entraram (
-e
) e não interpretam\
como um caractere de escape (-r
).
Este script não é uma solução rica em recursos - veja abaixo um refinamento -mas esses recursos úteis só fazem dois caracteres a mais. Eu recomendo sempre usar-r
comread
, a menos que você saiba que precisa deixar o usuário fornecer\
escapes. - Se o usuário escreveu
q
ouQ
, saia. - Crie uma associação array (
declare -A
). Preenchê-lo com o maior grau numérico associado a cada grau de letra. -
Efetue o loop das classificações das letras do menor para o maior, verificando se o número fornecido pelo usuário é baixo o suficiente para se encaixar no intervalo numérico de cada letra.
Com a avaliação aritmética de((
))
, os nomes das variáveis não precisam ser expandidos com$
. (Na maioria das outras situações, se você quiser usar o valor de uma variável no lugar de seu nome, você deve fazer isso . - Se ele estiver no intervalo, imprima a nota e saia .
Por brevidade, eu uso o curto-circuito e operador (&&
) em vez de umif
-then
. - Se o loop terminar e nenhum intervalo tiver sido correspondido, suponha que o número digitado seja muito alto (acima de 100) e diga ao usuário que ele estava fora do intervalo.
Como isso se comporta, com entradas estranhas
Como as outras soluções short postadas, esse script não verifica a entrada antes de assumir que é um número. A avaliação aritmética ( ((
))
) automaticamente remove espaços em branco iniciais e finais, então isso não é problema, mas:
- A entrada que não se parece com um número é interpretada como 0.
- Com a entrada que parece um número (ou seja, se começar com um dígito), mas contém caracteres inválidos, o script emite erros.
- Entrada de vários dígitos começando com
0
é interpretada como em octal . Por exemplo, o script dirá a você que 77 é um C, enquanto 077 é um D. Embora alguns usuários possam querer isso, provavelmente não o fazem e isso pode causar confusão. - No lado positivo, quando recebe uma expressão aritmética, esse script automaticamente simplifica e determina o grau de letra associado. Por exemplo, ele dirá que 320/4 é um B.
Uma versão expandida e totalmente apresentada
Por esses motivos, talvez você queira usar algo como esse script expandido, que verifica se a entrada é boa e inclui alguns outros aprimoramentos.
#!/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 ainda é uma solução bastante compacta.
Quais recursos isso adiciona?
Os principais pontos deste script expandido são:
- Validação de entrada. roteiro de terdon verifica entrada com
if [[ ! $response =~ ^[0-9]*$ ]] ...
, então eu mostrar uma outra maneira, que sacrifica um pouco brevidade, mas é mais robusto, permitindo que o usuário para inserir espaços iniciais e finais e se recusar a permitir uma expressão que possa ou não ser destinada como octal (a menos que seja zero). - Eu usei
case
com expandido globbing em vez de[[
com a expressão regular=~
correspondência regular operador (como em resposta de terdon ). Eu fiz isso para mostrar que (e como) também pode ser feito dessa maneira. Os globs e regexps são duas maneiras de especificar padrões que correspondem ao texto, e o método é adequado para esse aplicativo. - da AB script bash , tenho fechado a coisa toda em um loop exterior (exceto a criação inicial da matriz
cutoffs
). Ele solicita números e fornece notas de letras correspondentes, desde que a entrada do terminal esteja disponível e o usuário não tenha solicitado a saída. A julgar pelodo
...done
em torno do código na sua pergunta, parece que você quer isso. - Para facilitar o encerramento, eu aceito qualquer variante de
q
ouquit
que não diferencia maiúsculas de minúsculas.
Este script usa algumas construções que podem não ser familiares aos novatos; eles estão detalhados abaixo.
Explicação: Uso de continue
Quando eu quiser pular o resto do corpo do loop while
externo, eu uso o comando continue
. Isso traz de volta à parte superior do loop, para ler mais entradas e executar outra iteração.
Na primeira vez que faço isso, o único loop em que estou é o loop while
externo, portanto, posso chamar continue
sem argumento. (Estou em uma construção case
, mas isso não afeta a operação de break
ou continue
.)
*) echo "I don't understand that number."; continue;;
Na segunda vez, no entanto, estou em um loop for
interno que está aninhado dentro do loop while
externo. Se eu usasse continue
sem argumento, isso seria equivalente a continue 1
e continuaria o loop for
interno em vez do loop while
externo.
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Então, nesse caso, eu uso continue 2
para fazer o bash encontrar e continuar o segundo loop em vez disso.
Explicação: case
rótulos com Globs
Eu não uso case
para descobrir qual o tamanho da letra bin em que um número entra (como em < um href="https://askubuntu.com/a/621471/22949"> resposta de bash do AB ). Mas eu uso case
para decidir se a entrada do usuário deve ser considerada:
- um número válido,
*( )@([1-9]*([0-9])|+(0))*( )
- o comando quit,
*( )[qQ]?([uU][iI][tT])*( )
- qualquer outra coisa (e, portanto, entrada inválida),
*
Estes são shell globs .
- Cada um é seguido por um
)
que não corresponde a nenhuma abertura(
, que é a sintaxe decase
para separar um padrão dos comandos executados quando é correspondido. -
A sintaxe
;;
écase
para indicar o final dos comandos a serem executados para uma correspondência de maiúsculas e minúsculas (e que nenhum caso subseqüente deve ser testado após executá-los).
A globalização shell ordinária fornece *
para corresponder a zero ou mais caracteres, ?
para corresponder exatamente a um caractere e classes / intervalos de caracteres em [
]
colchetes. Mas eu estou usando globalização estendida , que vai além disso. A globalização estendida é ativada por padrão ao usar bash
interativamente, mas é desativada por padrão ao executar um script. O comando shopt -s extglob
na parte superior do script o ativa.
Explicação: Globbing ampliado
*( )@([1-9]*([0-9])|+(0))*( )
, que verifica a entrada numérica , corresponde a uma sequência de:
- Zero ou mais espaços (
*( )
). A construção*(
)
corresponde a zero ou mais do padrão entre parênteses, que aqui é apenas um espaço.
Na verdade, existem dois tipos de espaço em branco horizontal, espaços e tabulações, e geralmente é desejável coincidir com guias também. Mas não estou me preocupando com isso aqui, porque esse script é escrito para entrada interativa manual e o-e
flag pararead
ativa a linha de leitura GNU. Isso é para que o usuário possa ir e voltar em seu texto com as teclas de seta para a esquerda e para a direita, mas tem o efeito colateral de geralmente impedir que as guias sejam inseridas literalmente. - Uma ocorrência (
@(
)
) de (|
):- Um dígito diferente de zero (
[1-9]
) seguido por zero ou mais (*(
)
) de qualquer dígito ([0-9]
). - Um ou mais (
+(
)
) de0
.
- Um dígito diferente de zero (
- Zero ou mais espaços (
*( )
), novamente.
*( )[qQ]?([uU][iI][tT])*( )
, que verifica o comando quit , corresponde a uma sequência de:
- Zero ou mais espaços (
*( )
). -
q
ouQ
([qQ]
). - Opcionalmente - ou seja, zero ou uma ocorrência (
?(
)
) - de:-
u
ouU
([uU]
) seguido pori
ouI
([iI]
) seguido port
ouT
([tT]
).
-
- Zero ou mais espaços (
*( )
), novamente.
Variante: Validando entrada com uma expressão regular estendida
Se você preferir testar a entrada do usuário em relação a uma expressão regular em vez de um shell glob, talvez prefira usar essa versão, que funciona da mesma forma, mas usa [[
e =~
(como em terdon's answer ) em vez de case
e extensão de globbing.
#!/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
Possíveis vantagens dessa abordagem são:
-
Neste caso particular, a sintaxe é um pouco mais simples, pelo menos no segundo padrão, onde eu verifico o comando quit. Isso ocorre porque eu consegui definir a opção
nocasematch
shell e, em seguida, todas as variantes de caso deq
equit
foram cobertas automaticamente.Isso é o que o comando
shopt -s nocasematch
faz. O comandoshopt -s extglob
é omitido, já que globbing não é usado nesta versão. -
Habilidades de expressões regulares são mais comuns do que proficiência em extlags de bash.
Explicação: expressões regulares
Quanto aos padrões especificados à direita do operador =~
, veja como essas expressões regulares funcionam.
^\ *([1-9][0-9]*|0+)\ *$
, que verifica a entrada numérica , corresponde a uma sequência de:
- O começo - isto é, a margem esquerda - da linha (
^
). - Zero ou mais (
*
, postfix aplicado) espaços. Um espaço normalmente não precisa ser\
-escaped em uma expressão regular, mas isso é necessário com[[
para evitar um erro de sintaxe. - Uma substring (
(
)
) que é um ou o outro (|
) de:-
[1-9][0-9]*
: um dígito diferente de zero ([1-9]
) seguido por zero ou mais (*
, postfix aplicado) de qualquer dígito ([0-9]
). -
0+
: um ou mais (+
, postfix aplicado) de0
.
-
- Zero ou mais espaços (
\ *
), como antes. - O final - ou seja, a borda direita - da linha (
$
).
Ao contrário de case
labels, que correspondem à expressão inteira sendo testada, =~
retorna true se qualquer parte de sua expressão à esquerda corresponder ao padrão fornecido como sua expressão à direita. É por isso que as âncoras ^
e $
, especificando o início e o fim da linha, são necessárias aqui e não correspondem sintaticamente a nada que apareça no método com case
e extglobs.
Os parênteses são necessários para que ^
e $
se vinculem à disjunção de [1-9][0-9]*
e 0+
. Caso contrário, seria a disjunção de ^[1-9][0-9]*
e 0+$
e corresponderia qualquer entrada começando com um dígito diferente de zero ou terminando com 0
(ou ambos, que ainda poderiam incluir não dígitos em entre).
^\ *q(uit)?\ *$
, que verifica o comando quit , corresponde a uma sequência de:
- O começo da linha (
^
). - Zero ou mais espaços (
\ *
, veja a explicação acima). - A letra
q
. OuQ
, poisshopt nocasematch
está ativado. - Opcionalmente - ou seja, zero ou uma ocorrência (postfix
?
) - da subseqüência ((
)
):-
u
, seguido pori
, seguido port
. Ou, comoshopt nocasematch
está ativado,u
pode serU
; independentemente,i
pode serI
; e de forma independente,t
pode serT
. (Isto é, as possibilidades são não limitadas auit
eUIT
.)
-
- Zero ou mais espaços novamente (
\ *
). - O final da linha (
$
).