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 -r
com read
, a menos que você saiba que precisa deixar o fornecimento do usuário \
escapar.
- Se o usuário escreveu
q
ou 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
case
com 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
cutoffs
matriz). 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
... done
ao 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
q
ou 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 while
loop externo , eu uso o continue
comando 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 while
loop externo , para que eu possa ligar continue
sem nenhum argumento. (Estou em uma case
construção, 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 for
loop interno que está aninhado dentro do while
loop externo . Se eu usasse continue
sem argumento, isso seria equivalente continue 1
e continuaria o for
loop interno em vez do while
loop externo .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Portanto, nesse caso, eu uso o continue 2
bash find e continuo o segundo loop.
Explicação: case
Etiquetas com Globs
Não uso case
para descobrir qual classe da letra bin um número cai (como em resposta a festança da 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 globs de concha .
- Cada um é seguido por um
)
que não corresponde a nenhuma abertura (
, que é case
a sintaxe para separar um padrão dos comandos que são executados quando é correspondido.
;;
é case
a 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 bash
interativamente, mas é desativado por padrão ao executar um script. O shopt -s extglob
comando 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 -e
sinalizador para read
ativar 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 (
*( )
).
q
ou Q
( [qQ]
).
- Opcionalmente - ou seja, zero ou uma ocorrência (
?(
)
) - de:
u
ou U
( [uU]
) seguido por i
ou I
( [iI]
) seguido por t
ou 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 case
um 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 q
e quit
foram cobertas automaticamente.
É isso que o shopt -s nocasematch
comando faz. O shopt -s extglob
comando é 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 case
ró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 case
e 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 nocasematch
está ativado, u
pode ser U
; independentemente, i
pode ser I
; e independentemente, t
pode ser T
. (Ou seja, as possibilidades não se limitam a uit
e UIT
.)
- Zero ou mais espaços novamente (
\ *
).
- O fim da linha (
$
).