Brevità contro leggibilità: una via di mezzo
Come hai visto, questo problema ammette soluzioni moderatamente lunghe e in qualche modo ripetitive ma altamente leggibili ( risposte di basdon's e AB bash), così come quelle che sono molto brevi ma non intuitive e molto meno auto-documentanti ( pitone di Tim e bash risposte e glenn jackman's perl risposta ). Tutti questi approcci sono preziosi.
È inoltre possibile risolvere questo problema con il codice nel mezzo del continuum tra compattezza e leggibilità. Questo approccio è quasi leggibile come le soluzioni più lunghe, con una lunghezza più vicina alle soluzioni piccole ed esoteriche.
#!/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."
In questa soluzione bash, ho incluso alcune righe vuote per migliorare la leggibilità, ma potresti rimuoverle se lo volessi ancora più breve.
Linee vuote incluse, in realtà è solo leggermente più corta di una variante compattata, ancora piuttosto leggibile della soluzione bash di AB . I suoi principali vantaggi rispetto a tale metodo sono:
- È più intuitivo.
- È più facile cambiare i confini tra i voti (o aggiungere voti aggiuntivi).
- Accetta automaticamente input con spazi iniziali e finali (vedi sotto per una spiegazione di come
((
))
funziona).
Tutti e tre questi vantaggi derivano dal fatto che questo metodo utilizza l'input dell'utente come dati numerici anziché esaminando manualmente le sue cifre costitutive.
Come funziona
- Leggi l'input dell'utente. Lasciateli usare i tasti freccia per spostarsi nel testo che hanno inserito (
-e
) e non interpretarli \
come caratteri di escape ( -r
).
Questo script non è una soluzione ricca di funzionalità - vedi sotto per un perfezionamento - ma queste utili funzionalità ne aumentano solo due caratteri. Consiglio di utilizzare sempre -r
con read
, a meno che tu non sappia che devi lasciare scappare la fornitura dell'utente \
.
- Se l'utente ha scritto
q
o Q
, esci.
- Crea un array associativo ( ). Inseriscilo con il voto numerico più alto associato a ciascun voto di lettera.
declare -A
- Scorri tra i gradi delle lettere dal più basso al più alto, controllando se il numero fornito dall'utente è abbastanza basso da rientrare nell'intervallo numerico di ciascuna lettera.
Con ((
))
la valutazione aritmetica, non è necessario espandere i nomi delle variabili $
. (Nella maggior parte delle altre situazioni, se si desidera utilizzare il valore di una variabile al posto del suo nome, è necessario farlo .)
- Se rientra nell'intervallo, stampare il voto ed uscire .
Per brevità, uso il cortocircuito e l' operatore ( &&
) anziché un if
- then
.
- Se il ciclo termina e nessun intervallo è stato abbinato, supponiamo che il numero inserito sia troppo alto (oltre 100) e dire all'utente che era fuori intervallo.
Come si comporta, con input strani
Come le altre brevi soluzioni pubblicate, quello script non controlla l'input prima di assumere che sia un numero. Valutazione aritmetica ( ((
))
) strisce automaticamente spazi iniziali e finali, in modo tale non un problema, ma:
- L'input che non assomiglia affatto a un numero viene interpretato come 0.
- Con un input che assomiglia a un numero (ovvero, se inizia con una cifra) ma contiene caratteri non validi, lo script emette errori.
- Input multi cifre iniziano
0
viene interpretato come essere in ottale . Ad esempio, lo script ti dirà che 77 è una C, mentre 077 è una D. Anche se alcuni utenti potrebbero volerlo, molto probabilmente no e può causare confusione.
- Tra i lati positivi, quando viene data un'espressione aritmetica, questo script semplifica automaticamente e determina il grado di lettera associato. Ad esempio, ti dirà che 320/4 è un B.
Una versione estesa e completamente in primo piano
Per questi motivi, potresti voler utilizzare qualcosa di simile a questo script espanso, che verifica che l'input sia buono e include alcuni altri miglioramenti.
#!/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
Questa è ancora una soluzione piuttosto compatta.
Quali funzionalità aggiunge?
I punti chiave di questo script espanso sono:
- Convalida input. lo script di terdon controlla l'input con
if [[ ! $response =~ ^[0-9]*$ ]] ...
, quindi mostro un altro modo, che sacrifica un po 'di brevità ma è più robusto, consentendo all'utente di entrare in spazi iniziali e finali e rifiutando di consentire un'espressione che potrebbe o non potrebbe essere intesa come ottale (a meno che non sia zero) .
- Ho usato
case
con globbing esteso invece che [[
con l' operatore di =~
corrispondenza delle espressioni regolari (come nella risposta di Terdon ). L'ho fatto per dimostrare che (e come) può essere fatto anche in quel modo. Globi e regexps sono due modi per specificare modelli che corrispondono al testo, e entrambi i metodi vanno bene per questa applicazione.
- Come lo script bash di AB , ho racchiuso il tutto in un ciclo esterno (tranne la creazione iniziale
cutoffs
dell'array). Richiede numeri e fornisce i corrispondenti voti in lettere fintanto che l'input del terminale è disponibile e l'utente non gli ha detto di smettere. A giudicare dal do
... done
intorno al codice nella tua domanda, sembra che tu lo voglia.
- Per facilitare la chiusura, accetto qualsiasi variante senza distinzione tra maiuscole e minuscole di
q
o quit
.
Questo script utilizza alcuni costrutti che potrebbero non essere familiari ai principianti; sono dettagliati di seguito.
Spiegazione: Uso di continue
Quando voglio saltare il resto del corpo del while
loop esterno , utilizzo il continue
comando. Questo lo riporta in cima al ciclo, per leggere più input ed eseguire un'altra iterazione.
La prima volta che lo faccio, l'unico loop in cui mi trovo è il while
loop esterno , quindi posso chiamare continue
senza argomenti. (Sono in un case
costrutto, ma ciò non influisce sul funzionamento di break
o continue
.)
*) echo "I don't understand that number."; continue;;
La seconda volta, tuttavia, mi trovo in un for
circuito interno annidato all'interno del while
circuito esterno . Se avessi usato continue
senza alcun argomento, questo sarebbe equivalente continue 1
e continuerebbe il for
ciclo interno anziché il while
ciclo esterno .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Quindi in quel caso, uso invece continue 2
per trovare bash e continuare il secondo loop.
Spiegazione: case
etichette con globi
Non ho l'abitudine case
di capire in quale scomparto di grado lettera cade un numero (come nella risposta bash di AB ). Ma uso case
per decidere se l'input dell'utente debba essere considerato:
- un numero valido,
*( )@([1-9]*([0-9])|+(0))*( )
- il comando quit,
*( )[qQ]?([uU][iI][tT])*( )
- qualsiasi altra cosa (e quindi input non valido),
*
Questi sono globs di shell .
- Ciascuno è seguito da un
)
non corrispondente ad alcuna apertura (
, che ècase
la sintassi per separare un modello dai comandi eseguiti quando viene abbinato.
;;
è case
la sintassi per indicare la fine dei comandi da eseguire per un caso paticolare (e che nessun caso successivo deve essere testato dopo averli eseguiti).
Il globbing di shell ordinario prevede la *
corrispondenza di zero o più caratteri, la ?
corrispondenza di esattamente un carattere e le classi / intervalli di caratteri tra [
]
parentesi. Ma sto usando un globbing esteso , che va oltre. Il globbing esteso è abilitato per impostazione predefinita quando si utilizza in modo bash
interattivo, ma è disabilitato per impostazione predefinita quando si esegue uno script. Il shopt -s extglob
comando nella parte superiore dello script lo attiva.
Spiegazione: Extended Globbing
*( )@([1-9]*([0-9])|+(0))*( )
, che verifica l'immissione numerica , corrisponde a una sequenza di:
- Zero o più spazi (
*( )
). Il *(
)
costrutto corrisponde a zero o più del motivo tra parentesi, che qui è solo uno spazio.
Esistono in realtà due tipi di spazi bianchi orizzontali, spazi e schede, e spesso è preferibile abbinare anche le schede. Ma non mi preoccupo di questo qui, perché questo script è scritto per input manuale e interattivo e il -e
flag per read
abilitare readline GNU. In questo modo l'utente può spostarsi avanti e indietro nel proprio testo con i tasti freccia sinistra e destra, ma ha l'effetto collaterale di impedire l'inserimento letterale di schede.
- Una ricorrenza (
@(
)
) di uno dei due ( |
):
- Una cifra
[1-9]
diversa da zero ( ) seguita da zero o più ( *(
)
) di qualsiasi cifra ( [0-9]
).
- Uno o più (
+(
)
) di 0
.
- Zero o più spazi (
*( )
), di nuovo.
*( )[qQ]?([uU][iI][tT])*( )
, che verifica il comando quit , corrisponde a una sequenza di:
- Zero o più spazi (
*( )
).
q
o Q
([qQ]
).
- Facoltativamente, ovvero zero o una occorrenza (
?(
)
) - di:
u
o U
( [uU]
) seguito da i
o I
( [iI]
) seguito da t
o T
( [tT]
).
- Zero o più spazi (
*( )
), di nuovo.
Variante: convalida dell'input con un'espressione regolare estesa
Se si preferisce testare l'input dell'utente rispetto a un'espressione regolare piuttosto che a una shell glob, si potrebbe preferire utilizzare questa versione, che funziona allo stesso modo ma usa [[
e =~
(come nella risposta di Terdon ) invece di case
estendere il 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
I possibili vantaggi di questo approccio sono:
In questo caso particolare, la sintassi è un po 'più semplice, almeno nel secondo modello, dove controllo il comando quit. Questo perché sono stato in grado di impostare l' nocasematch
opzione shell e quindi tutte le varianti di case di q
e quit
sono state coperte automaticamente.
Questo è ciò che fa il shopt -s nocasematch
comando. Il shopt -s extglob
comando è stato omesso poiché il globbing non è utilizzato in questa versione.
Le abilità di espressione regolare sono più comuni della competenza nei extglob di bash.
Spiegazione: espressioni regolari
Per quanto riguarda i modelli specificati a destra =~
dell'operatore, ecco come funzionano quelle espressioni regolari.
^\ *([1-9][0-9]*|0+)\ *$
, che verifica l'immissione numerica , corrisponde a una sequenza di:
- L'inizio - ovvero il bordo sinistro - della linea (
^
).
- Zero o più
*
spazi ( , postfix applicato). \
Normalmente uno spazio non deve essere salvato in un'espressione regolare, ma ciò è necessario [[
per prevenire un errore di sintassi.
- Una sottostringa (
(
)
) che è l'una o l'altra ( |
) di:
[1-9][0-9]*
: una cifra [1-9]
diversa da zero ( ) seguita da zero o più ( *
, postfisso applicato) di qualsiasi cifra ( [0-9]
).
0+
: uno o più ( +
, applicato postfix) di 0
.
- Zero o più spazi (
\ *
), come prima.
- L'estremità, ovvero il bordo destro, della linea (
$
).
A differenza delle case
etichette, che corrispondono a tutta l'espressione in fase di test, =~
restituisce true se una parte dell'espressione della mano sinistra corrisponde al modello fornito come espressione della mano destra. Questo è il motivo per cui le ancore ^
e $
, specificando l'inizio e la fine della linea, sono necessarie qui, e non corrispondono sintatticamente a qualsiasi cosa appaia nel metodo con case
e extglobs.
Le parentesi sono necessarie per creare ^
e $
legare alla disgiunzione di [1-9][0-9]*
e 0+
. Altrimenti sarebbe la disgiunzione di ^[1-9][0-9]*
e 0+$
, e corrisponderebbe a qualsiasi input che inizia con una cifra diversa da zero o termina con un 0
(o entrambi, che potrebbe comunque includere non cifre tra).
^\ *q(uit)?\ *$
, che verifica il comando quit , corrisponde a una sequenza di:
- L'inizio della riga (
^
).
- Zero o più spazi (
\ *
, vedere la spiegazione sopra).
- La lettera
q
. Oppure Q
, poiché shopt nocasematch
è abilitato.
- Opzionalmente - ovvero zero o una occorrenza (postfisso
?
) - della sottostringa ( (
)
):
u
, seguito da i
, seguito da t
. Oppure, poiché shopt nocasematch
è abilitato u
può essere U
; indipendentemente, i
può essere I
; e indipendentemente, t
può essere T
. (Cioè, le possibilità non sono limitate a uit
e UIT
.)
- Zero o più spazi di nuovo (
\ *
).
- La fine della linea (
$
).