Краткость и удобочитаемость: золотая середина
Как вы видели, эта проблема допускает решения, которые являются умеренно длинными и несколько повторяющимися, но очень удобочитаемыми ( ответы Терда и А. Б. на bash), а также очень короткими, но не интуитивными и гораздо менее самодокументируемыми ( Python Тима). и Bash ответы и Perl ответ Гленна Джекмана ). Все эти подходы ценны.
Вы также можете решить эту проблему с помощью кода в середине континуума между компактностью и удобочитаемостью. Этот подход почти так же читабелен, как и более длинные решения, с длиной, близкой к маленьким, эзотерическим решениям.
#!/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."
В этом решении bash я добавил несколько пустых строк для улучшения читабельности, но вы можете удалить их, если хотите, чтобы они были еще короче.
Пустые строки включены, это на самом деле лишь немного короче, чем компактный, все еще довольно читаемый вариант решения bash от AB . Его основные преимущества перед этим методом:
- Это более интуитивно понятно.
- Проще изменить границы между оценками (или добавить дополнительные оценки).
- Он автоматически принимает ввод с начальными и конечными пробелами (объяснение того, как
(( ))работает, см. Ниже ).
Все три из этих преимуществ возникают из-за того, что этот метод использует ввод пользователя в качестве числовых данных, а не путем ручного изучения составляющих его цифр.
Как это устроено
- Прочитать ввод от пользователя. Позвольте им использовать клавиши со стрелками для перемещения по тексту, который они ввели (
-e), и не интерпретировать \как escape-символ ( -r).
Этот скрипт не является многофункциональным решением - уточнение приведено ниже - но эти полезные функции делают его на два символа длиннее. Я рекомендую всегда использовать -rс read, если только вы не знаете, что нужно разрешить пользователю \сбегать.
- Если пользователь написал
qили Q, выйти.
- Создать ассоциативный массив (
declare -A). Заполните его наивысшей числовой оценкой, связанной с каждой буквенной оценкой.
- Перебирайте буквенные оценки от самых низких до самых высоких, проверяя, достаточно ли низкое число, предоставленное пользователем, чтобы попасть в числовой диапазон каждой буквенной оценки.
При (( ))арифметической оценке имена переменных не нужно расширять $. (В большинстве других ситуаций, если вы хотите использовать значение переменной вместо ее имени, вы должны сделать это .)
- Если он попадает в диапазон, выведите оценку и выйдите .
Для краткости я использую короткое замыкание и operator ( &&), а не if- then.
- Если цикл завершается и диапазон не соответствует, предположите, что введенное число слишком велико (более 100) и сообщите пользователю, что оно вышло за пределы диапазона.
Как это ведет себя со странным вводом
Как и в других опубликованных кратких решениях, этот сценарий не проверяет ввод перед тем, как предположить, что это число. Арифметическая оценка ( (( ))) автоматически удаляет начальные и конечные пробелы, так что это не проблема, но:
- Ввод, который совсем не похож на число, интерпретируется как 0.
- В случае ввода, которое выглядит как число (т. Е. Если оно начинается с цифры), но содержит недопустимые символы, скрипт выдает ошибки.
- Мульти-значный вход начиная с
0будет интерпретироваться как в восьмеричной системе . Например, скрипт скажет вам, что 77 - это C, а 077 - это D. Хотя некоторые пользователи могут этого хотеть, скорее всего, нет, и это может вызвать путаницу.
- С другой стороны, когда задано арифметическое выражение, этот скрипт автоматически упрощает его и определяет соответствующую буквенную оценку. Например, он скажет вам, что 320/4 - это B.
Расширенная, полнофункциональная версия
По этим причинам вы можете захотеть использовать что-то вроде этого расширенного скрипта, который проверяет правильность ввода и включает некоторые другие улучшения.
#!/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
Это все еще довольно компактное решение.
Какие функции это добавляет?
Ключевые пункты этого расширенного сценария:
- Подтверждение ввода. Скрипт terdon проверяет ввод с помощью , поэтому я показываю другой способ, который жертвует некоторой краткостью, но является более надежным, позволяя пользователю вводить начальные и конечные пробелы и отказываясь разрешить выражение, которое может или не может быть задано как восьмеричное (если оно не равно нулю) ,
if [[ ! $response =~ ^[0-9]*$ ]] ...
- Я использовал
caseс расширенной подстановкой вместо [[с =~ регулярными выражениями оператора (как в ответе terdon в ). Я сделал это, чтобы показать, что (и как) это тоже можно сделать таким образом. Globs и regexps - это два способа задания шаблонов, которые соответствуют тексту, и любой метод подходит для этого приложения.
- Как и сценарий bash AB , я заключил все это во внешний цикл (за исключением первоначального создания
cutoffsмассива). Он запрашивает цифры и выставляет соответствующие буквенные оценки, пока имеется доступ к терминалу и пользователь не велел ему выйти. Судя по do... doneвокруг кода в вашем вопросе, похоже, что вы этого хотите.
- Чтобы облегчить выход, я принимаю любой вариант
qили без учета регистра quit.
Этот скрипт использует несколько конструкций, которые могут быть незнакомы новичкам; они подробно описаны ниже.
Пояснение: использование continue
Когда я хочу пропустить остальную часть тела внешнего whileцикла, я использую continueкоманду. Это возвращает его к началу цикла, чтобы прочитать больше входных данных и выполнить еще одну итерацию.
В первый раз, когда я делаю это, единственный цикл, в котором я нахожусь, это внешний whileцикл, поэтому я могу вызывать continueбез аргументов. (Я нахожусь в caseконструкции, но это не влияет на работу breakили continue.)
*) echo "I don't understand that number."; continue;;
Второй раз, однако, я нахожусь во внутреннем forцикле, который сам вложен во внешний whileцикл. Если бы я использовал continueбез аргумента, это было бы эквивалентно continue 1и продолжило бы внутренний forцикл вместо внешнего whileцикла.
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Поэтому в этом случае я использую continue 2bash для поиска и продолжения второго цикла.
Пояснение: caseЯрлыки с шариками
Я не использую , caseчтобы выяснить , какая буква класса бен число попадает (как в Баш ответ АБ ). Но я использую, caseчтобы решить, следует ли учитывать ввод пользователя:
- действительный номер,
*( )@([1-9]*([0-9])|+(0))*( )
- команда выхода,
*( )[qQ]?([uU][iI][tT])*( )
- что-нибудь еще (и, следовательно, неверный ввод),
*
Это ракушки .
- За каждым следует a,
)которому не соответствует ни одно открытие (, то есть caseсинтаксис для отделения шаблона от команд, запускаемых при его сопоставлении.
;;caseСинтаксис is для указания конца команд, запускаемых для совпадения конкретного случая (и что последующие случаи не должны проверяться после их запуска).
Обычная глобализация оболочки обеспечивает *совпадение с нулем или более символов, ?чтобы точно соответствовать одному символу, а также классы / диапазоны символов в [ ]скобках. Но я использую расширенную глобализацию , которая выходит за рамки этого. При использовании в bashинтерактивном режиме расширенная глобализация включена по умолчанию , но по умолчанию она отключена при запуске сценария. Команда shopt -s extglobв верхней части скрипта включает его.
Пояснение: расширенная глобализация
*( )@([1-9]*([0-9])|+(0))*( ), который проверяет числовой ввод , соответствует последовательности:
- Ноль или более пробелов (
*( )). В *( )конструкт соответствует нулю или более шаблона в скобках, что здесь есть только пространство.
На самом деле есть два вида горизонтальных пробелов, пробелов и табуляции, и часто желательно также сопоставлять табуляции. Но я не беспокоюсь об этом здесь, потому что этот сценарий написан для ручного, интерактивного ввода и -eфлага для включения readGNU readline. Это позволяет пользователю перемещаться по тексту назад и вперед с помощью клавиш со стрелками влево и вправо, но это имеет побочный эффект, как правило, предотвращает буквальный ввод вкладок.
- Одно вхождение (
@( )) любого ( |):
- Ненулевая цифра (
[1-9]), за которой следует ноль или более ( *( )) любой цифры ( [0-9]).
- Один или несколько (
+( )) из 0.
- Ноль или более пробелов (
*( )), снова.
*( )[qQ]?([uU][iI][tT])*( ), который проверяет команду quit , соответствует последовательности:
- Ноль или более пробелов (
*( )).
qили Q( [qQ]).
- Необязательно - то есть ноль или одно вхождение (
?( )) - из:
uили U( [uU]) с последующим iили I( [iI]) с последующим tили T( [tT]).
- Ноль или более пробелов (
*( )), снова.
Вариант: проверка ввода с расширенным регулярным выражением
Если вы предпочитаете проверять вводимые пользователем данные вместо регулярного выражения, а не глобуса оболочки, вы можете предпочесть использовать эту версию, которая работает так же, но использует [[и =~(как в ответе Тердона ) вместо caseрасширенного глобирования.
#!/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
Возможные преимущества этого подхода заключаются в том, что:
В этом конкретном случае синтаксис немного проще, по крайней мере, во втором шаблоне, где я проверяю команду quit. Это потому, что я смог установить параметр nocasematchоболочки, а затем все варианты вариантов qи quitбыли покрыты автоматически.
Это то, что shopt -s nocasematchделает команда. Команда shopt -s extglobне указана, поскольку в этой версии не используется глобализация.
Навыки регулярного выражения более распространены, чем умение использовать экглобы bash.
Пояснение: Регулярные выражения
Что касается шаблонов, указанных справа от =~оператора, то вот как работают эти регулярные выражения.
^\ *([1-9][0-9]*|0+)\ *$, который проверяет числовой ввод , соответствует последовательности:
- Начало - т. Е. Левый край - линии (
^).
- Ноль или более (с
*применением постфикса) пробелов. Пробел обычно не нужно \экранировать в регулярном выражении, но это необходимо [[для предотвращения синтаксической ошибки.
- Подстрока (
( )), которая является одним или другим ( |) из:
[1-9][0-9]*: ненулевая цифра ( [1-9]), за которой следует ноль или более ( *примененный постфикс) любой цифры ( [0-9]).
0+: один или несколько ( +примененный постфикс) из 0.
- Ноль или более пробелов (
\ *), как и раньше.
- Конец - то есть правый край - линии (
$).
В отличие от caseметок, которые соответствуют всему тестируемому выражению, =~возвращает true, если какая-либо часть его левого выражения совпадает с шаблоном, заданным как его правое выражение. Вот почему ^и $якорь, указав начало и конец строки, которые необходимы здесь, и не соответствует синтаксический ни к чему появляющемуся в методе с caseи extglobs.
Скобки необходимы, чтобы сделать ^и $связать с дизъюнкцией [1-9][0-9]*и 0+. В противном случае это будет дизъюнкция ^[1-9][0-9]*и 0+$, и совпадение с любым вводом, начинающимся с ненулевой цифры или заканчивающимся на 0(или оба, которые могут все еще включать нецифровые цифры между ними).
^\ *q(uit)?\ *$, который проверяет команду quit , соответствует последовательности:
- Начало строки (
^).
- Ноль или более пробелов (
\ *см. Объяснение выше).
- Письмо
q. Или Q, так shopt nocasematchкак включен.
- Опционально - то есть ноль или одно вхождение (постфикс
?) - подстроки ( ( )):
u, сопровождаемый i, сопровождаемый t. Или, так shopt nocasematchкак включен uможет быть U; независимо, iможет быть I; и независимо, tможет быть T. (То есть возможности не ограничиваются uitа UIT.)
- Ноль или более пробелов снова (
\ *).
- Конец строки (
$).