Краткость и удобочитаемость: золотая середина
Как вы видели, эта проблема допускает решения, которые являются умеренно длинными и несколько повторяющимися, но очень удобочитаемыми ( ответы Терда и А. Б. на 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 2
bash для поиска и продолжения второго цикла.
Пояснение: 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
флага для включения read
GNU 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
.)
- Ноль или более пробелов снова (
\ *
).
- Конец строки (
$
).