Kürze vs. Lesbarkeit: Ein Mittelweg
Wie Sie gesehen haben, ist dieses Problem bekannt für Lösungen, die mäßig lang sind und sich etwas wiederholen, aber gut lesbar sind ( Antworten von Terdon und AB ) sowie für Lösungen, die sehr kurz, aber nicht intuitiv und viel weniger selbstdokumentierend sind (Tims Python) und bash answers und glenn jackmans perl answer ). All diese Ansätze sind wertvoll.
Sie können dieses Problem auch mit Code in der Mitte des Kontinuums zwischen Kompaktheit und Lesbarkeit lösen. Dieser Ansatz ist fast so lesbar wie die längeren Lösungen, wobei die Länge eher den kleinen, esoterischen Lösungen entspricht.
#!/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 dieser Bash-Lösung habe ich einige leere Zeilen eingefügt, um die Lesbarkeit zu verbessern. Sie können sie jedoch entfernen, wenn Sie es noch kürzer haben möchten.
Leerzeilen eingeschlossen, ist dies eigentlich nur geringfügig kürzer als eine komprimierte, noch gut lesbare Variante der bash-Lösung von AB . Die Hauptvorteile gegenüber dieser Methode sind:
- Es ist intuitiver.
- Es ist einfacher, die Grenzen zwischen den Noten zu ändern (oder zusätzliche Noten hinzuzufügen).
- Eingaben mit führenden und nachfolgenden Leerzeichen werden automatisch akzeptiert (siehe unten für eine Erläuterung der Funktionsweise
((
))
).
Alle drei Vorteile ergeben sich aus der Verwendung der Benutzereingabe als numerische Daten und nicht aus der manuellen Prüfung der einzelnen Ziffern.
Wie es funktioniert
- Lesen Sie die Eingaben des Benutzers. Lassen Sie sie sich mit den Pfeiltasten im eingegebenen Text bewegen (
-e
) und nicht \
als Escape-Zeichen interpretieren ( -r
).
Dieses Skript ist keine funktionsreiche Lösung - eine Verfeinerung finden Sie weiter unten - aber diese nützlichen Funktionen verlängern es nur um zwei Zeichen. Ich empfehle, immer mit -r
zu verwenden read
, es sei denn, Sie wissen, dass Sie dem Benutzer \
erlauben müssen, die Eingabe zu unterbrechen.
- Wenn der Benutzer
q
oder geschrieben hat Q
, beenden Sie.
- Erstellen Sie ein assoziatives Array (
declare -A
). Füllen Sie es mit der höchsten numerischen Note aus, die jeder Buchstabennote zugeordnet ist.
- Durchlaufen Sie die Buchstabenklassen von der niedrigsten zur höchsten und prüfen Sie, ob die vom Benutzer angegebene Zahl niedrig genug ist, um in den numerischen Bereich der einzelnen Buchstabenklassen zu fallen.
Bei der ((
))
arithmetischen Auswertung müssen Variablennamen nicht mit erweitert werden $
. (Wenn Sie in den meisten anderen Situationen den Wert einer Variablen anstelle ihres Namens verwenden möchten, müssen Sie dies tun .)
- Wenn es in den Bereich fällt, drucken Sie die Note und beenden Sie das Programm .
Der Kürze halber verwende ich den Kurzschluss und den Operator ( &&
) anstelle eines if
- then
.
- Wenn die Schleife beendet ist und kein Bereich gefunden wurde, gehen Sie davon aus, dass die eingegebene Zahl zu hoch ist (über 100), und teilen Sie dem Benutzer mit, dass sie außerhalb des Bereichs liegt.
Wie sich das verhält, mit seltsamen Eingaben
Wie die anderen kurzen Lösungen geschrieben, wird das Skript nicht die Eingabe zu überprüfen , bevor vorausgesetzt , es ist eine Zahl ist. Arithmetische Auswertung ( ((
))
) entfernt automatisch führende und nachfolgende Leerzeichen, das ist also kein Problem, aber:
- Eingaben, die überhaupt nicht wie eine Zahl aussehen, werden als 0 interpretiert.
- Bei Eingaben, die wie eine Zahl aussehen (dh wenn sie mit einer Ziffer beginnen), aber ungültige Zeichen enthalten, gibt das Skript Fehler aus.
- Multi-digit - Eingang mit dem Starten
0
wird interpretiert als in oktaler . Das Skript teilt Ihnen beispielsweise mit, dass 77 ein C und 077 ein D ist. Obwohl einige Benutzer dies wünschen, ist dies höchstwahrscheinlich nicht der Fall und kann Verwirrung stiften.
- Auf der positiven Seite, wenn ein arithmetischer Ausdruck gegeben wird, vereinfacht dieses Skript ihn automatisch und bestimmt die zugehörige Buchstabenklasse. Zum Beispiel wird es Ihnen sagen, 320/4 ist ein B.
Eine erweiterte Vollversion
Aus diesen Gründen möchten Sie möglicherweise so etwas wie dieses erweiterte Skript verwenden, das prüft, ob die Eingabe korrekt ist, und einige andere Verbesserungen enthält.
#!/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
Dies ist immer noch eine ziemlich kompakte Lösung.
Welche Funktionen werden hinzugefügt?
Die wichtigsten Punkte dieses erweiterten Skripts sind:
- Eingabevalidierung. Das Skript von terdon prüft Eingaben mit , daher zeige ich einen anderen Weg, der etwas Kürze einbüßt, aber robuster ist, dem Benutzer die Eingabe von führenden und nachfolgenden Leerzeichen ermöglicht und es ablehnt, einen Ausdruck zuzulassen, der als oktal gedacht sein könnte oder nicht (es sei denn, er ist Null) .
if [[ ! $response =~ ^[0-9]*$ ]] ...
- Ich habe verwendet
case
mit erweiterten Globbing statt [[
mit dem =~
regulären Ausdruck Anpassungsoperator (wie in terdon Antwort ). Ich habe das getan, um zu zeigen, dass (und wie) es auch so gemacht werden kann. Globs und Regexps sind zwei Methoden zum Festlegen von Mustern, die mit Text übereinstimmen. Beide Methoden sind für diese Anwendung geeignet.
- Wie das Bash-Skript von AB habe ich das Ganze in eine äußere Schleife eingeschlossen (mit Ausnahme der anfänglichen Erstellung des
cutoffs
Arrays). Es fordert Zahlen an und gibt die entsprechenden Buchstabennoten aus, solange eine Terminaleingabe verfügbar ist und der Benutzer nicht angewiesen hat, das Programm zu beenden. Gemessen an den do
... done
um den Code in Ihrer Frage, es sieht aus wie Sie das wollen.
- Um das Beenden zu vereinfachen, akzeptiere ich alle Varianten von
q
oder , bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird quit
.
In diesem Skript werden einige Konstrukte verwendet, die Anfängern möglicherweise unbekannt sind. Sie sind unten aufgeführt.
Erklärung: Verwendung von continue
Wenn ich den Rest des Körpers der äußeren while
Schleife überspringen möchte , verwende ich den continue
Befehl. Dies bringt es wieder an den Anfang der Schleife, um weitere Eingaben zu lesen und eine weitere Iteration auszuführen.
Wenn ich das zum ersten Mal mache, ist die einzige Schleife, in der ich mich while
befinde, die äußere Schleife, sodass ich continue
ohne Argumente aufrufen kann . (Ich bin in einem case
Konstrukt, aber das hat keinen Einfluss auf den Betrieb von break
oder continue
.)
*) echo "I don't understand that number."; continue;;
Beim zweiten Mal befinde ich mich jedoch in einer inneren for
Schleife, die selbst in der äußeren while
Schleife verschachtelt ist . Wenn ich continue
kein Argument verwenden würde, wäre dies gleichbedeutend mit continue 1
der inneren for
Schleife und würde diese anstelle der äußeren while
Schleife fortsetzen.
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Also verwende ich in diesem Fall, continue 2
um bash find zu machen und stattdessen die zweite Schleife fortzusetzen.
Erläuterung: case
Beschriftungen mit Globs
Ich kann nicht case
herausfinden, in welche Briefklasse eine Zahl fällt (wie in ABs bash-Antwort ). Aber ich benutze es case
, um zu entscheiden, ob die Eingabe des Benutzers berücksichtigt werden soll:
- eine gültige Nummer,
*( )@([1-9]*([0-9])|+(0))*( )
- der beendigungsbefehl,
*( )[qQ]?([uU][iI][tT])*( )
- alles andere (und damit ungültige Eingabe),
*
Das sind Muschelkugeln .
- Nach jedem folgt ein
)
, dem keine Öffnung zugeordnet (
ist. Dies ist case
die Syntax zum Trennen eines Musters von den Befehlen, die ausgeführt werden, wenn es zugeordnet ist.
;;
case
Dies ist die Syntax zum Anzeigen des Endes von Befehlen, die für eine Übereinstimmung mit bestimmten Fällen ausgeführt werden sollen (und dass keine nachfolgenden Fälle getestet werden sollten, nachdem sie ausgeführt wurden).
Durch gewöhnliches Shell-Globbing können *
null oder mehr Zeichen, ?
genau ein Zeichen und Zeichenklassen / -bereiche in [
]
Klammern abgeglichen werden. Aber ich verwende erweitertes Globbing , das darüber hinaus geht. Erweitertes Globbing ist bei der bash
interaktiven Verwendung standardmäßig aktiviert, bei der Ausführung eines Skripts jedoch standardmäßig deaktiviert. Der shopt -s extglob
Befehl oben im Skript aktiviert es.
Erläuterung: Erweitertes Globbing
*( )@([1-9]*([0-9])|+(0))*( )
, die nach numerischen Eingaben sucht , entspricht einer Folge von:
- Null oder mehr Leerzeichen (
*( )
). Das *(
)
Konstrukt stimmt mit keinem oder mehreren Mustern in Klammern überein. Hierbei handelt es sich nur um ein Leerzeichen.
Tatsächlich gibt es zwei Arten von horizontalen Leerzeichen, Leerzeichen und Tabulatoren, und oft ist es wünschenswert, Tabulatoren auch zuzuordnen. Aber ich mache mir darüber hier keine Sorgen, da dieses Skript für manuelle, interaktive Eingaben und das -e
Flag zum read
Aktivieren von GNU-Readline geschrieben wurde. Auf diese Weise kann der Benutzer in seinem Text mit der linken und rechten Pfeiltaste vor und zurück navigieren. Dies hat jedoch den Nebeneffekt, dass Tabulatoren im Allgemeinen nicht buchstäblich eingegeben werden können.
- Ein Vorkommen (
@(
)
) von ( |
):
- Eine von Null verschiedene Ziffer (
[1-9]
), gefolgt von Null oder mehr ( *(
)
) einer beliebigen Ziffer ( [0-9]
).
- Einer oder mehrere (
+(
)
) von 0
.
*( )
Wieder null oder mehr Leerzeichen ( ).
*( )[qQ]?([uU][iI][tT])*( )
, der nach dem Befehl quit sucht , entspricht einer Folge von:
- Null oder mehr Leerzeichen (
*( )
).
q
oder Q
( [qQ]
).
- Optional - dh null oder ein Vorkommen (
?(
)
) - von:
u
oder U
( [uU]
) gefolgt von i
oder I
( [iI]
) gefolgt von t
oder T
( [tT]
).
*( )
Wieder null oder mehr Leerzeichen ( ).
Variante: Eingabe mit einem erweiterten regulären Ausdruck validieren
Wenn Sie die Benutzereingaben lieber mit einem regulären Ausdruck als mit einem Shell-Glob testen möchten, verwenden Sie möglicherweise lieber diese Version, die dasselbe funktioniert, jedoch [[
und =~
(wie in der Antwort von terdon ) anstelle von case
und Extended Globbing verwendet.
#!/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
Mögliche Vorteile dieses Ansatzes sind:
In diesem speziellen Fall ist die Syntax zumindest im zweiten Muster, in dem ich nach dem Befehl quit suche, etwas einfacher. Das liegt daran, dass ich die nocasematch
Shell-Option setzen konnte und dann alle Fallvarianten von q
und quit
automatisch abgedeckt wurden.
Das macht der shopt -s nocasematch
Befehl. Der shopt -s extglob
Befehl wird weggelassen, da das Globbing in dieser Version nicht verwendet wird.
Reguläre Ausdrucksfähigkeiten sind häufiger als das Erlernen von Bash-Extglobs.
Erläuterung: Reguläre Ausdrücke
In Bezug auf die rechts vom =~
Operator angegebenen Muster erfahren Sie, wie diese regulären Ausdrücke funktionieren.
^\ *([1-9][0-9]*|0+)\ *$
, die nach numerischen Eingaben sucht , entspricht einer Folge von:
- Der Anfang - dh der linke Rand - der Linie (
^
).
- Null oder mehr
*
Leerzeichen ( angewendetes Postfix). Ein Leerzeichen muss normalerweise nicht \
in einem regulären Ausdruck -escaped werden, dies wird jedoch benötigt [[
, um einen Syntaxfehler zu vermeiden.
- Eine Teilzeichenfolge (
(
)
), die die eine oder andere ( |
) von:
[1-9][0-9]*
: Eine Ziffer ungleich Null ( [1-9]
), gefolgt von null oder mehr ( *
, angewendetes Postfix) einer beliebigen Ziffer ( [0-9]
).
0+
: ein oder mehrere ( +
, angewendetes Postfix) von 0
.
- Null oder mehr Leerzeichen (
\ *
) wie zuvor.
- Das Ende - dh der rechte Rand - der Linie (
$
).
Im Gegensatz zu case
Beschriftungen, die mit dem gesamten getesteten =~
Ausdruck übereinstimmen , wird true zurückgegeben, wenn ein Teil des Ausdrucks auf der linken Seite mit dem als Ausdruck auf der rechten Seite angegebenen Muster übereinstimmt. Aus diesem Grund werden die Anker ^
und $
, die den Anfang und das Ende der Zeile case
angeben , hier benötigt und entsprechen syntaktisch nichts, was in der Methode with und extglobs vorkommt.
Die Klammern werden benötigt, um die Disjunktion von und herzustellen ^
und $
an diese zu binden . Andernfalls würde dies die Disjunktion von und sein und mit jeder Eingabe übereinstimmen, die mit einer Ziffer ungleich Null beginnt oder mit einer (oder beiden, die möglicherweise noch dazwischen liegende Nicht-Ziffern enthalten) endet .[1-9][0-9]*
0+
^[1-9][0-9]*
0+$
0
^\ *q(uit)?\ *$
, der nach dem Befehl quit sucht , entspricht einer Folge von:
- Der Anfang der Zeile (
^
).
- Null oder mehr Leerzeichen (
\ *
siehe obige Erklärung).
- Der Brief
q
. Oder Q
seit shopt nocasematch
ist aktiviert.
- Optional - dh null oder ein Vorkommen (Postfix
?
) - der Teilzeichenfolge ( (
)
):
u
, gefolgt von i
, gefolgt von t
. Oder, da shopt nocasematch
es aktiviert ist, u
kann es sein U
; unabhängig sein i
kann I
; und unabhängig sein t
kann T
. (Das heißt, die Möglichkeiten sind nicht auf uit
und beschränkt UIT
.)
- Wieder null oder mehr Leerzeichen (
\ *
).
- Das Ende der Zeile (
$
).