Brevidade x legibilidade: um meio termo
Como você viu, esse problema admite soluções moderadamente longas e um tanto repetitivas, mas altamente legíveis ( respostas bash de Terdon e AB ), bem como aquelas que são muito curtas, mas não intuitivas e muito menos auto-documentadas ( python de Tim respostas bash e resposta perl de glenn jackman ). Todas essas abordagens são valiosas.
Você também pode resolver esse problema com o 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 soluções pequenas e 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 do bash, incluí algumas linhas em branco para melhorar a legibilidade, mas você pode removê-las se desejar ainda mais.
Linhas em branco incluídas, na verdade, são apenas um pouco mais curtas do que uma variante compactada, ainda bastante legível da solução bash 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).
- Ele aceita automaticamente entradas com espaços à esquerda e à direita (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
- Leia a entrada do usuário. Deixe que eles usem as teclas de seta para se mover no texto digitado (
-e) e não interpretem \como um caractere de escape ( -r).
Esse script não é uma solução rica em recursos - veja abaixo um refinamento - mas esses recursos úteis apenas aumentam dois caracteres. Eu recomendo sempre usar -rcom read, a menos que você saiba que precisa deixar o fornecimento do usuário \escapar.
- Se o usuário escreveu
qou Q, saia.
- Crie uma matriz associativa ( ). Preencha-o com a nota numérica mais alta associada a cada nota de letra.
declare -A
- Passe pelas notas das letras da mais baixa para a mais alta, verificando se o número fornecido pelo usuário é baixo o suficiente para cair no intervalo numérico de cada letra.
Com a (( ))avaliação aritmética, os nomes de variáveis não precisam ser expandidos $. (Na maioria das outras situações, se você quiser usar o valor de uma variável no lugar de seu nome, faça isso .)
- Se estiver dentro do intervalo, imprima a nota e saia .
Por uma questão de brevidade, uso o curto-circuito e o operador ( &&) em vez de um if- then.
- Se o loop terminar e nenhum intervalo for correspondido, suponha que o número digitado seja muito alto (acima de 100) e informe ao usuário que ele estava fora do intervalo.
Como isso se comporta, com entrada estranha
Como as outras soluções curtas postadas, esse script não verifica a entrada antes de assumir que é um número. Avaliação aritmética ( (( ))) tiras automaticamente líder e espaços em branco, de modo que é nenhum problema, mas:
- A entrada que não se parece com um número é interpretada como 0.
- Com entrada que se parece com um número (ou seja, se começa com um dígito), mas contém caracteres inválidos, o script emite erros.
- Entrada de dígitos múltiplos começando com
0é interpretada como sendo , em octal . Por exemplo, o script dirá que 77 é um C, enquanto 077 é um D. Embora alguns usuários possam querer isso, provavelmente não querem e isso pode causar confusão.
- No lado positivo, quando recebe uma expressão aritmética, esse script a simplifica automaticamente e determina a nota da carta associada. Por exemplo, ele dirá que 320/4 é um B.
Uma versão expandida e com todos os recursos
Por esses motivos, convém 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
Essa ainda é uma solução bastante compacta.
Quais recursos isso adiciona?
Os pontos principais desse script expandido são:
- Validação de entrada. O script de terdon verifica a entrada com , por isso mostro outra maneira, que sacrifica alguma brevidade, mas é mais robusta, permitindo ao usuário entrar em espaços iniciais e finais e se recusando a permitir uma expressão que possa ou não ser pretendida como octal (a menos que seja zero) .
if [[ ! $response =~ ^[0-9]*$ ]] ...
- Eu usei
casecom globbing estendido em vez de [[com o operador de =~ correspondência de expressão regular (como na resposta de terdon ). Fiz isso para mostrar que (e como) isso também pode ser feito dessa maneira. Globs e regexps são duas maneiras de especificar padrões que correspondem ao texto e qualquer um dos métodos é adequado para este aplicativo.
- Como o script bash da AB , incluí a coisa toda em um loop externo (exceto a criação inicial da
cutoffsmatriz). Ele solicita números e fornece notas correspondentes, desde que a entrada do terminal esteja disponível e o usuário não tenha solicitado que saia. A julgar pelo do... doneao redor do código da sua pergunta, parece que você deseja isso.
- Para facilitar a desistência, aceito qualquer variante que não diferencia maiúsculas de minúsculas de
qou quit.
Esse script usa algumas construções que podem não ser familiares para iniciantes; eles estão detalhados abaixo.
Explicação: Uso de continue
Quando eu quero pular o resto do corpo do whileloop externo , eu uso o continuecomando Isso o leva de volta ao topo do loop, para ler mais entradas e executar outra iteração.
A primeira vez que faço isso, o único loop em que estou é o whileloop externo , para que eu possa ligar continuesem nenhum argumento. (Estou em uma caseconstrução, mas isso não afeta a operação de breakou continue.)
*) echo "I don't understand that number."; continue;;
Na segunda vez, no entanto, estou em um forloop interno que está aninhado dentro do whileloop externo . Se eu usasse continuesem argumento, isso seria equivalente continue 1e continuaria o forloop interno em vez do whileloop externo .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Portanto, nesse caso, eu uso o continue 2bash find e continuo o segundo loop.
Explicação: caseEtiquetas com Globs
Não uso casepara descobrir qual classe da letra bin um número cai (como em resposta a festança da AB ). Mas eu uso casepara 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 globs de concha .
- Cada um é seguido por um
)que não corresponde a nenhuma abertura (, que é casea sintaxe para separar um padrão dos comandos que são executados quando é correspondido.
;;é casea sintaxe para indicar o final dos comandos a serem executados para uma correspondência de caso paticular (e que nenhum caso subsequente deve ser testado após a execução).
O globbing de shell comum fornece *para corresponder a zero ou mais caracteres, ?para corresponder exatamente a um caractere e a classes / intervalos de caracteres entre [ ]colchetes. Mas estou usando globbing estendido , o que vai além disso. O globbing estendido é ativado por padrão ao usar bashinterativamente, mas é desativado por padrão ao executar um script. O shopt -s extglobcomando na parte superior do script o ativa.
Explicação: Globbing estendido
*( )@([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, o que aqui é apenas um espaço.
Na verdade, existem dois tipos de espaço em branco horizontal, espaços e guias, e geralmente é desejável combinar as guias também. Mas não estou me preocupando com isso aqui, porque esse script foi escrito para entrada manual, interativa e o -esinalizador para readativar a linha de leitura do GNU. Isso é para que o usuário possa ir e voltar no texto com as teclas de seta esquerda e direita, mas tem o efeito colateral de impedir que as guias sejam inseridas literalmente.
- Uma ocorrência (
@( )) de qualquer um ( |):
- Um dígito diferente de zero (
[1-9]) seguido por zero ou mais ( *( )) de qualquer dígito ( [0-9]).
- Um ou mais (
+( )) de 0.
- 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 (
*( )).
qou Q( [qQ]).
- Opcionalmente - ou seja, zero ou uma ocorrência (
?( )) - de:
uou U( [uU]) seguido por iou I( [iI]) seguido por tou T( [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, você pode preferir usar esta versão, que funciona da mesma forma, mas usa [[e =~(como em resposta de terdon ) em vez de caseum globbing estendido.
#!/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
As possíveis vantagens dessa abordagem são as seguintes:
Nesse caso em particular, a sintaxe é um pouco mais simples, pelo menos no segundo padrão, em que verifico o comando quit. Isso porque pude definir onocasematch opção de shell e, em seguida, todas as variantes de casos qe quitforam cobertas automaticamente.
É isso que o shopt -s nocasematch comando faz. O shopt -s extglobcomando é omitido porque o globbing não é usado nesta versão.
Habilidades de expressão regular são mais comuns que a proficiência em extglobs do 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 início - ou seja, borda esquerda - da linha (
^ ).
- Zero ou mais
*espaços ( , postfix aplicado). Normalmente, um espaço não precisa ser \escapado em uma expressão regular, mas isso é necessário com[[ para evitar um erro de sintaxe.
- Uma substring (
( )) que é uma ou outra (| ) 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) de 0.
- Zero ou mais espaços (
\ * ), como antes.
- O final - ou seja, a borda direita - da linha (
$).
Ao contrário dos caserótulos, que correspondem à expressão inteira sendo testada, =~retornará true se alguma 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 casee extglobs.
Os parênteses são necessários para criar ^e $vincular à disjunção de [1-9][0-9]*e 0+. Caso contrário, seria a disjunção de ^[1-9][0-9]*e 0+$, e combinar qualquer entrada que começa com um dígito diferente de zero ou terminando com um0 (ou ambos, que ainda pode incluir não-dígitos no meio).
^\ *q(uit)?\ *$, que verifica o comando quit , corresponde a uma sequência de:
- O início da linha (
^ ).
- Zero ou mais espaços (
\ * veja a explicação acima).
- A carta
q. Ou Q, desdeshopt nocasematch está ativado.
- Opcionalmente - ou seja, zero ou uma ocorrência (postfix
?) - da substring (( ) ):
u, seguido por i, seguido por t. Ou, uma vez que shopt nocasematchestá ativado, upode ser U; independentemente, ipode ser I; e independentemente, tpode ser T. (Ou seja, as possibilidades não se limitam a uite UIT.)
- Zero ou mais espaços novamente (
\ * ).
- O fim da linha (
$).