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-rcomread, a menos que você saiba que precisa deixar o usuário fornecer\escapes. - Se o usuário escreveu
qouQ, 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
casecom 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...doneem torno do código na sua pergunta, parece que você quer isso. - Para facilitar o encerramento, eu aceito qualquer variante de
qouquitque 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 decasepara separar um padrão dos comandos executados quando é correspondido. -
A sintaxe
;;écasepara 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-eflag parareadativa 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 (
*( )). -
qouQ([qQ]). - Opcionalmente - ou seja, zero ou uma ocorrência (
?()) - de:-
uouU([uU]) seguido poriouI([iI]) seguido portouT([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
nocasematchshell e, em seguida, todas as variantes de caso deqequitforam cobertas automaticamente.Isso é o que o comando
shopt -s nocasematchfaz. 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 nocasematchestá ativado. - Opcionalmente - ou seja, zero ou uma ocorrência (postfix
?) - da subseqüência (()):-
u, seguido pori, seguido port. Ou, comoshopt nocasematchestá ativado,upode serU; independentemente,ipode serI; e de forma independente,tpode serT. (Isto é, as possibilidades são não limitadas auiteUIT.)
-
- Zero ou mais espaços novamente (
\ *). - O final da linha (
$).