Zwięzłość a czytelność: środek
Jak widzieliście, ten problem dotyczy rozwiązań, które są umiarkowanie długie i nieco powtarzalne, ale bardzo czytelne ( odpowiedzi bash Terdona i AB ), a także te, które są bardzo krótkie, ale nieintuicyjne i znacznie mniej samok dokumentujące ( pyton Tima i odpowiedzi bash i perl glenn jackman ). Wszystkie te podejścia są cenne.
Możesz także rozwiązać ten problem za pomocą kodu w środku kontinuum między zwartością a czytelnością. To podejście jest prawie tak samo czytelne jak dłuższe rozwiązania, o długości bliższej małym, ezoterycznym rozwiązaniom.
#!/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."
W tym rozwiązaniu dodałem kilka pustych wierszy w celu zwiększenia czytelności, ale możesz je usunąć, jeśli chcesz, aby był jeszcze krótszy.
Puste linie włączone, to faktycznie tylko nieznacznie krótsza niż w compactified, wciąż dość czytelnej wersji z roztworem bash AB . Jego główne zalety w stosunku do tej metody to:
- To jest bardziej intuicyjne.
- Łatwiej jest zmienić granice między ocenami (lub dodać dodatkowe oceny).
- Automatycznie akceptuje wprowadzanie ze spacjami wiodącymi i końcowymi (wyjaśnienie, jak to
((
))
działa poniżej ).
Wszystkie te trzy zalety wynikają z tego, że metoda ta wykorzystuje dane wejściowe użytkownika jako dane liczbowe, a nie poprzez ręczne sprawdzenie cyfr składowych.
Jak to działa
- Przeczytaj dane wejściowe od użytkownika. Pozwól im używać klawiszy strzałek do poruszania się po wprowadzonym tekście (
-e
) i nie interpretuj ich \
jako znak zmiany znaczenia ( -r
).
Ten skrypt nie jest bogatym w funkcje rozwiązaniem - udoskonalenie znajduje się poniżej - ale te przydatne funkcje wydłużają go o dwa znaki. Zalecam zawsze używać -r
z read
, chyba że wiesz, że musisz pozwolić użytkownikowi na \
ucieczkę.
- Jeśli użytkownik napisał
q
lub Q
, wyjdź.
- Załóż asocjacyjną tablicę (
declare -A
). Wypełnij ją najwyższą oceną numeryczną związaną z każdą oceną literową.
- Pętle przechodź między literami od najniższej do najwyższej, sprawdzając, czy liczba podana przez użytkownika jest wystarczająco niska, aby mieściła się w zakresie liczbowym każdej litery.
Przy ((
))
obliczaniu arytmetycznym nazwy zmiennych nie muszą być rozszerzane o $
. (W większości innych sytuacji, jeśli chcesz użyć wartości zmiennej zamiast jej nazwy, musisz to zrobić .)
- Jeśli mieści się w zakresie, wydrukuj ocenę i wyjdź .
Dla zwięzłości używam zwarcia i operatora ( &&
) zamiast if
- then
.
- Jeśli pętla zakończy się i żaden zakres nie zostanie dopasowany, załóż, że wprowadzona liczba jest zbyt wysoka (ponad 100) i powiedz użytkownikowi, że jest poza zasięgiem.
Jak to się dzieje, z dziwnym wkładem
Podobnie jak inne opublikowane krótkie rozwiązania, skrypt nie sprawdza danych wejściowych przed założeniem, że jest liczbą. Arytmetyczna oceny ( ((
))
) automatycznie pozbawia początkowe i końcowe spacje, więc to nie ma problemu, ale:
- Dane wejściowe, które wcale nie wyglądają jak liczba, są interpretowane jako 0.
- Przy wprowadzeniu, które wygląda jak liczba (tzn. Jeśli zaczyna się cyfrą), ale zawiera nieprawidłowe znaki, skrypt emituje błędy.
- Multi-cyfrowy wejście zaczynając
0
jest interpretowane jako w ósemkowym . Na przykład skrypt powie Ci, że 77 to C, a 077 to D. Chociaż niektórzy użytkownicy mogą tego chcieć, najprawdopodobniej tego nie robią i mogą powodować zamieszanie.
- Z drugiej strony, po otrzymaniu wyrażenia arytmetycznego, ten skrypt automatycznie upraszcza go i określa powiązaną ocenę literową. Na przykład powie ci, że 320/4 to B.
Rozszerzona, w pełni funkcjonalna wersja
Z tych powodów możesz chcieć użyć czegoś takiego jak ten rozszerzony skrypt, który sprawdza, czy dane wejściowe są dobre, i zawiera kilka innych ulepszeń.
#!/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
To wciąż dość kompaktowe rozwiązanie.
Jakie funkcje to dodaje?
Kluczowe punkty tego rozszerzonego skryptu to:
- Sprawdzanie poprawności danych wejściowych. Skrypt terdona sprawdza dane wejściowe za pomocą , więc pokazuję inny sposób, który poświęca pewną zwięzłość, ale jest bardziej niezawodny, pozwalając użytkownikowi na wprowadzanie spacji wiodących i końcowych oraz odmawiając zezwolenia na wyrażenie, które może, ale nie musi być wyrażeniem ósemkowym (chyba że zero) .
if [[ ! $response =~ ^[0-9]*$ ]] ...
- Używałem
case
z rozszerzonym globbing zamiast [[
z =~
dopasowywania wyrażeń regularnych operatora (jak w odpowiedzi terdon za ). Zrobiłem to, aby pokazać, że (i jak) można to zrobić w ten sposób. Globi i wyrażenia regularne to dwa sposoby określania wzorców pasujących do tekstu, a każda z tych metod jest odpowiednia dla tej aplikacji.
- Podobnie jak skrypt bash AB , wszystko zamknąłem w zewnętrznej pętli (oprócz początkowego utworzenia
cutoffs
tablicy). Żąda liczb i podaje odpowiednie stopnie literowe, o ile dostępne jest wejście terminala, a użytkownik nie kazał mu wyjść. Sądząc po do
... done
wokół kodu w twoim pytaniu, wygląda na to, że tego chcesz.
- Aby ułatwić rzucanie, akceptuję każdy wariant
q
lub bez rozróżniania wielkości liter quit
.
Ten skrypt wykorzystuje kilka konstrukcji, które mogą być nieznane nowicjuszom; są wyszczególnione poniżej.
Objaśnienie: Zastosowanie continue
Kiedy chcę pominąć resztę ciała zewnętrznej while
pętli, używam continue
polecenia. Sprowadza to go z powrotem na szczyt pętli, aby przeczytać więcej danych wejściowych i uruchomić kolejną iterację.
Gdy robię to po raz pierwszy, jedyną pętlą, w której jestem, jest while
pętla zewnętrzna , więc mogę wywoływać continue
bez argumentów. (Jestem w case
konstrukcji, ale to nie wpływa na działanie break
lub continue
.)
*) echo "I don't understand that number."; continue;;
Jednak po raz drugi jestem w wewnętrznej for
pętli, która sama jest zagnieżdżona w zewnętrznej while
pętli. Gdybym użył continue
bez argumentu, byłoby to równoważne continue 1
i kontynuowałoby for
pętlę wewnętrzną zamiast while
pętli zewnętrznej .
((REPLY <= cutoffs[$letter])) && { echo $letter; continue 2; }
Więc w takim przypadku używam, continue 2
aby bash znalazł i kontynuował drugą pętlę.
Objaśnienie: case
Etykiety z kulami
Nie używać case
, aby dowiedzieć się, jaka litera gatunek bin numer wpada (jak w AB bash odpowiedź ). Ale case
decyduję, czy należy wziąć pod uwagę wkład użytkownika:
- ważny numer,
*( )@([1-9]*([0-9])|+(0))*( )
- polecenie wyjścia,
*( )[qQ]?([uU][iI][tT])*( )
- wszystko inne (a zatem nieprawidłowe dane wejściowe),
*
To są kule globu .
- Po każdym następuje znak,
)
który nie jest dopasowany żadnym otworem (
, który jest case
składnią służącą do oddzielenia wzorca od poleceń uruchamianych podczas dopasowywania.
;;
to case
składnia wskazująca koniec komend, które mają być uruchamiane w typowym dopasowaniu wielkości liter (i że żadne kolejne przypadki nie powinny być testowane po ich uruchomieniu).
Zwykłe globowanie powłoki zapewnia *
dopasowanie zero lub więcej znaków, ?
dopasowanie dokładnie jednego znaku oraz klas znaków / zakresów w [
]
nawiasach. Ale używam rozszerzonego globowania , które wykracza poza to. Rozszerzone globowanie jest domyślnie włączone, gdy jest używane bash
interaktywnie, ale jest domyślnie wyłączone podczas uruchamiania skryptu. shopt -s extglob
Komenda na górze skryptu włącza go.
Objaśnienie: Rozszerzone globowanie
*( )@([1-9]*([0-9])|+(0))*( )
, który sprawdza wprowadzanie numeryczne , dopasowuje sekwencję:
- Zero lub więcej spacji (
*( )
). Te *(
)
mecze konstrukt zero lub więcej z wzorca w nawiasach, co tutaj jest tylko przestrzeń.
W rzeczywistości istnieją dwa rodzaje poziomych białych znaków, spacje i tabulatory i często pożądane jest również dopasowanie tabulatorów. Ale nie martwię się o to tutaj, ponieważ ten skrypt jest napisany do ręcznego, interaktywnego wprowadzania i -e
flagi read
umożliwiającej readline GNU. Dzieje się tak, aby użytkownik mógł poruszać się w tekście w przód iw tył za pomocą klawiszy strzałek w lewo i w prawo, ale ma to efekt uboczny polegający na tym, że tabulatory nie mogą być wprowadzane dosłownie.
- Jedno wystąpienie (
@(
)
) albo ( |
):
- Niezerowa cyfra (
[1-9]
), po której następuje zero lub więcej ( *(
)
) dowolnej cyfry ( [0-9]
).
- Jeden lub więcej (
+(
)
) z 0
.
- Zero lub więcej spacji (
*( )
) ponownie.
*( )[qQ]?([uU][iI][tT])*( )
, który sprawdza komendę quit , dopasowuje sekwencję:
- Zero lub więcej spacji (
*( )
).
q
lub Q
( [qQ]
).
- Opcjonalnie - tj. Zero lub jedno wystąpienie (
?(
)
) - z:
u
lub U
( [uU]
), po której następuje i
lub I
( [iI]
), po której następuje t
lub T
( [tT]
).
- Zero lub więcej spacji (
*( )
) ponownie.
Wariant: Sprawdzanie poprawności danych wejściowych za pomocą rozszerzonego wyrażenia regularnego
Jeśli wolisz przetestować dane wejściowe użytkownika na podstawie wyrażenia regularnego zamiast globu powłoki, możesz użyć tej wersji, która działa tak samo, ale używa [[
i =~
(jak w odpowiedzi Terdona ) zamiast case
rozszerzonego globowania.
#!/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
Możliwe zalety tego podejścia to:
W tym konkretnym przypadku składnia jest nieco prostsza, przynajmniej w drugim wzorze, w którym sprawdzam komendę quit. Jest tak, ponieważ byłem w stanie ustawić nocasematch
opcję powłoki, a następnie wszystkie warianty przypadków q
i quit
zostały pokryte automatycznie.
To właśnie shopt -s nocasematch
robi polecenie. shopt -s extglob
Komenda zostanie pominięta jako globbing nie jest używane w tej wersji.
Umiejętności wyrażania regularnego są bardziej powszechne niż biegłość w ekstlobach basha.
Objaśnienie: Wyrażenia regularne
Jeśli chodzi o wzorce określone po prawej stronie =~
operatora, oto jak działają te wyrażenia regularne.
^\ *([1-9][0-9]*|0+)\ *$
, który sprawdza wprowadzanie numeryczne , dopasowuje sekwencję:
- Początek - tj. Lewa krawędź - linii (
^
).
- Zero lub więcej (
*
, zastosowanych postfiksów) spacji. Spacja zwykle nie wymaga \
przeszukiwania w wyrażeniu regularnym, ale jest to konieczne, [[
aby zapobiec błędowi składni.
- Podłańcuch (
(
)
), który jest jednym lub drugim ( |
) z:
[1-9][0-9]*
: niezerowa cyfra ( [1-9]
), po której następuje zero lub więcej ( *
stosowana postfiks) dowolnej cyfry ( [0-9]
).
0+
: jeden lub więcej ( +
, zastosowany postfiks) z 0
.
- Zero lub więcej spacji (
\ *
), jak poprzednio.
- Koniec - tj. Prawa krawędź - linii (
$
).
W przeciwieństwie do case
etykiet, które pasują do całego testowanego wyrażenia, =~
zwraca wartość true, jeśli jakakolwiek część jego wyrażenia po lewej stronie odpowiada wzorowi podanemu jako wyrażenie po prawej stronie. Właśnie dlatego potrzebne są tutaj kotwice ^
i $
, określające początek i koniec linii, i nie odpowiadają składniowo niczego, co pojawia się w metodzie z case
i extglobami.
Nawiasy są potrzebne do utworzenia ^
i $
wiązania rozłączności z [1-9][0-9]*
i 0+
. W przeciwnym razie byłoby to rozłączenie ^[1-9][0-9]*
i 0+$
, i pasowałoby do każdego wejścia zaczynającego się od niezerowej cyfry lub kończącego się na 0
(lub oba, które nadal mogą zawierać między nimi cyfry niebędące cyframi).
^\ *q(uit)?\ *$
, który sprawdza komendę quit , dopasowuje sekwencję:
- Początek linii (
^
).
- Zero lub więcej spacji (
\ *
patrz powyższe objaśnienie).
- List
q
. Lub Q
, ponieważ shopt nocasematch
jest włączony.
- Opcjonalnie - tzn. Zero lub jedno wystąpienie (postfiks
?
) - podłańcucha ( (
)
):
u
, a następnie i
, a następnie t
. Lub, ponieważ shopt nocasematch
jest włączony u
może być U
; niezależnie i
może być I
; i niezależnie t
mogą być T
. (To znaczy, możliwości nie są ograniczone do uit
i UIT
.)
- Zero lub więcej spacji ponownie (
\ *
).
- Koniec linii (
$
).