Wybór z menu
Po tej lekcji: kursant używa case, żeby zamiast drabinki z elif napisać czytelny wybór jednej z wielu dróg, rozumie wzorce w case i buduje proste menu z numerkami przez select.
Wstęp
skrypt z siedmioma elif pod rząd, ekran przewija się w dół jak regulamin promocji
Piszesz skrypt, który obsługuje polecenia start, stop, restart, reload i status. Z tym, co znasz, wygląda to tak: if [ "$1" = "start" ], potem elif [ "$1" = "stop" ], potem elif, elif, elif. Każda linijka powtarza to samo "$1" =. Czytasz to i czujesz się jak w kolejce do okienka, w której każdy petent pyta urzędnika o to samo innymi słowami.
Dziś zrobimy z tego czystą listę: „jeśli start, to to; jeśli stop, to tamto; jeśli coś innego, to pokaż instrukcję”. A na koniec zbudujemy menu z numerkami, które wygląda jak z bankomatu: wybierz 1, żeby sprawdzić dysk, 2, żeby zobaczyć, kto jest zalogowany. Bez żadnych dodatkowych programów, sam Bash.
case, czyli rozdzielnia na poczcie
Wyobraź sobie rozdzielnię listów na poczcie. Pani bierze kopertę, patrzy na kod pocztowy i wrzuca do odpowiedniej przegródki: 30 do Krakowa, 00 do Warszawy, 80 do Gdańska. Nie zadaje sobie siedmiu pytań po kolei. Patrzy raz i wie, gdzie to idzie. A jak kod jest nieczytelny, list ląduje w przegródce „inne”.
W Bashu ta pani nazywa się case:
#!/bin/bash
# usluga.sh — udaje skrypt sterujący usługą
case "$1" in
start)
echo "Uruchamiam usługę"
;;
stop)
echo "Zatrzymuję usługę"
;;
restart|reload)
echo "Restartuję usługę"
;;
status)
echo "Usługa działa (chyba)"
;;
*)
echo "Użycie: $0 {start|stop|restart|status}"
exit 1
;;
esaccase "$1" in znaczy: „weź to, co jest w $1, i porównuj z tym, co niżej”.
Każda przegródka to wzorzec zakończony nawiasem, na przykład start). Pod nim komendy, które mają się wykonać. A na końcu przegródki dwa średniki ;;. To jest „koniec, nie sprawdzaj dalej”.
restart|reload) to dwa wzorce w jednej przegródce. Pionowa kreska znaczy tu „albo”. Uwaga, to nie jest rura. Ten sam znak, inny pokój, inna robota. Jak klucz, który w jednych drzwiach otwiera, a w innych służy do otwierania piwa.
*) to przegródka „wszystko inne”. Gwiazdka pasuje do każdego tekstu, więc stawiamy ją na samym końcu.
I zamknięcie: esac. Tak, to case czytane od tyłu. Tak samo jak fi zamyka if. Autorzy powłoki mieli poczucie humoru, którego nie dało się już cofnąć.
./usluga.sh start
./usluga.sh reload
./usluga.sh; echo "kod $?"
./usluga.sh StartUruchamiam usługę
Restartuję usługę
Użycie: ./usluga.sh {start|stop|restart|status}
kod 1
Użycie: ./usluga.sh {start|stop|restart|status}Zwróć uwagę na ostatnią linijkę. Start z wielkiej litery nie pasuje do start. case porównuje dokładnie, litera w literę. Jak urzędnik, który nie przyjmie wniosku, bo napisałeś „Kraków” zamiast „KRAKÓW” drukowanymi.
To, co właśnie widzisz, to szkielet klasycznych skryptów startowych. Zanim systemd przejął zarządzanie usługami, w /etc/init.d/ leżały skrypty zbudowane dokładnie tak: wielki case ze start, stop, restart i status. Na niektórych systemach ten folder nadal istnieje, więc jak będziesz miał chwilę, zajrzyj. Archeologia dla adminów.
Wzorce, czyli gwiazdka wraca
Pamiętasz gwiazdkę, znak zapytania i nawiasy kwadratowe z lekcji o wzorcach nazw plików? W case działają tak samo, tylko zamiast nazw plików dopasowują tekst.
Najczęstszy przykład z życia: pytanie „na pewno?”.
#!/bin/bash
read -r -p "Na pewno usunąć? [t/n] " odp
case "$odp" in
[tT]|[tT]ak|[yY]|[yY]es)
echo "Usuwam"
;;
[nN]*)
echo "Nic nie robię"
;;
"")
echo "Nic nie wpisałeś, więc nic nie robię"
;;
*)
echo "Nie rozumiem: $odp"
;;
esac[tT] pasuje do małego albo wielkiego t. [tT]ak pasuje do „tak” i „Tak”. [nN]* pasuje do wszystkiego, co zaczyna się na n: „n”, „nie”, „NIE”, a nawet „nigdy w życiu”. "" to pusty tekst, czyli ktoś wcisnął sam Enter.
t -> Usuwam
Tak -> Usuwam
y -> Usuwam
nie -> Nic nie robię
(Enter)-> Nic nie wpisałeś, więc nic nie robię
moze -> Nie rozumiem: mozeI zasada, której warto się trzymać przy wszystkim, co niszczy dane: jak nie wiesz, co użytkownik miał na myśli, nie rób nic. „Może” to nie jest zgoda. Ani w adminowaniu, ani w życiu.
Wzorce w case przydają się też do końcówek plików:
case "$plik" in
*.jpg|*.png) echo "obraz" ;;
*.txt) echo "tekst" ;;
*) echo "nie wiem" ;;
esacJak przegródka mieści się w jednej linijce, możesz napisać ją w jednej linijce, z ;; na końcu. Bash się nie obrazi.
A jeśli chcesz, żeby wielkość liter nie miała znaczenia w całym skrypcie, jest przełącznik:
shopt -s nocasematchPo nim tak pasuje i do „TAK”, i do „tAk”. Przydatne, kiedy użytkownicy piszą tak, jak im się podoba, czyli zawsze.
select, czyli menu jak w bankomacie
Teraz coś, co wygląda na magię, a jest jedną komendą. select wyświetla ponumerowaną listę, pyta o numer i daje Ci to, co wybrano. Jak bankomat: 1 wypłata, 2 saldo, 3 doładowanie telefonu. Nikt nie wpisuje słowa „wypłata”, wszyscy klikają numer.
#!/bin/bash
# menu.sh — małe menu admina
PS3="Wybierz numer: "
select opcja in "Miejsce na dysku" "Kto jest zalogowany" "Czas pracy" "Wyjście"; do
case "$opcja" in
"Miejsce na dysku")
df -h /
;;
"Kto jest zalogowany")
who
;;
"Czas pracy")
uptime
;;
"Wyjście")
echo "Do zobaczenia"
break
;;
*)
echo "Nie ma opcji numer $REPLY"
;;
esac
done1) Miejsce na dysku 3) Czas pracy
2) Kto jest zalogowany 4) Wyjście
Wybierz numer: 3
16:24:51 up 6 min, 0 user, load average: 0.58, 0.20, 0.07
Wybierz numer: 7
Nie ma opcji numer 7
Wybierz numer: 4
Do zobaczeniaRozbierzmy to.
PS3 to tekst zaproszenia, który select wyświetla przed pytaniem o numer. Jak go nie ustawisz, zobaczysz domyślne #?, które wygląda, jakby skrypt sam nie wiedział, czego chce.
select opcja in ... ; do ... done działa jak pętla. Wyświetla listę, czeka na numer, wkłada wybrany tekst do zmiennej opcja, wykonuje to, co jest między do i done, i pyta znowu. W kółko. Pętle dokładnie poznamy w następnej lekcji, na razie wystarczy, że wiesz: to się kręci, dopóki ktoś nie powie dość.
To „dość” to break. Wychodzi z menu. Bez niego jedyną drogą ucieczki jest Ctrl+D albo Ctrl+C. Ctrl+D to koniec wejścia, czyli grzeczne „nie mam już nic do powiedzenia”, i select się wtedy kończy.
Gdy ktoś wpisze numer spoza listy albo w ogóle nie numer, opcja jest pusta, a to, co naprawdę wpisał, leży w zmiennej REPLY. Dlatego w gałęzi *) pokazujemy $REPLY. A jak ktoś wciśnie sam Enter, select po prostu wyświetla menu jeszcze raz. Jak bankomat, który na pusty ekran mówi „Wybierz operację” po raz czwarty.
I zauważ, że select i case to para jak kelner i kucharz. select zbiera zamówienie, case decyduje, co z nim zrobić.
Jak to działa pod spodem
case nie uruchamia żadnego programu. Nie ma pliku /usr/bin/case. To jest słowo zastrzeżone Basha, jak if, for czy while, więc całą robotę robi sam urzędnik w okienku, bez dzwonienia do innych działów. Możesz to sprawdzić:
type case selectcase is a shell keyword
select is a shell keywordCo robi Bash, kiedy dochodzi do case? Najpierw rozwija słowo po case, czyli zamienia "$1" na prawdziwą wartość. Potem bierze przegródki od góry i porównuje tekst z każdym wzorcem po kolei. Przy pierwszym trafieniu wykonuje komendy i przy ;; wyskakuje za esac. Dlatego kolejność ma znaczenie: jeśli *) postawisz na górze, to złapie wszystko i reszta przegródek nigdy nie dostanie żadnego listu. Pani na poczcie, która wszystko wrzuca do „inne”, bo tak szybciej.
Ważne: wzorce w case nie szukają plików na dysku. *.txt w case nie patrzy, jakie pliki leżą w folderze. Porównuje tylko tekst ze wzorcem. To jest ta sama gwiazdka, ale używana jak szablon, a nie jak wyszukiwarka.
select działa tak: wypisuje listę i zaproszenie PS3 na wyjście błędów, czyli na drugi głośnik, dzięki czemu menu widać nawet wtedy, gdy zwykły wynik przekierujesz do pliku. Potem czyta jedną linijkę z wejścia i zapisuje ją w REPLY. Jeśli to numer z listy, wkłada odpowiedni tekst do Twojej zmiennej. Jeśli przeczyta koniec wejścia, kończy się z kodem 1.
Ściąga flag
| Komenda | Flaga / element | Co robi |
|---|---|---|
case | wzorzec) | początek przegródki |
case | a|b) | pasuje do a albo b |
case | *) | wszystko inne, stawiaj na końcu |
case | [tT] | jeden znak z listy |
case | ? | dokładnie jeden dowolny znak |
case | "") | pusty tekst |
case | ;; | koniec przegródki, wyjdź z case |
case | ;& | wykonaj też następną przegródkę |
case | ;;& | sprawdzaj dalej kolejne wzorce |
case | esac | koniec całego case |
shopt | -s nocasematch | wielkość liter bez znaczenia w case i [[ ]] |
select | PS3 | tekst zaproszenia przed numerem |
select | REPLY | to, co użytkownik naprawdę wpisał |
select | break | wyjście z menu |
select | Ctrl+D | koniec wejścia, menu się kończy |
⚠ Typowy błąd
Zapomniane dwa średniki:
case "$1" in
start)
echo "Start"
stop)
echo "Stop"
;;
esaczle.sh: line 5: syntax error near unexpected token `)'
zle.sh: line 5: ` stop)'Bash czyta stop) jako dalszy ciąg komend z przegródki start i nie wie, co zrobić z nawiasem. Zauważ, że skrypt nie wykonał nawet pierwszej linijki. Błąd składni wywala cały blok, zanim cokolwiek się uruchomi. Naprawa: ;; na końcu każdej przegródki. ShellCheck od razu pyta: „Did you forget the ;; after the previous case item?”. Czyli kolega przy biurku obok, który zauważa, zanim szef zauważy.
Drugi błąd jest podstępniejszy, bo nie daje żadnego komunikatu:
case "$1" in
"*.txt") echo "plik tekstowy" ;;
*) echo "coś innego" ;;
esacDla notatki.txt wypisze „coś innego”. Bo wzorzec w cudzysłowie to już nie wzorzec, tylko zwykły tekst. Pasuje wyłącznie do kogoś, kto nazwał plik dosłownie gwiazdka-kropka-txt. Koperta chroni zawartość przed rozwijaniem, także gwiazdkę. Zasada: słowo po case w cudzysłowie, wzorce z gwiazdką bez.
✎ Zadanie
Napisz skrypt typ.sh, który dostaje nazwę pliku i po końcówce mówi, co to jest: .jpg, .jpeg, .png to obraz, .txt i .md to tekst, .tar.gz, .tgz i .zip to archiwum, .sh to skrypt, a wszystko inne „nie wiem, co to”. Wielkość liter ma nie mieć znaczenia, czyli ZDJECIE.JPG też jest obrazem. Bez argumentu skrypt ma wypisać, jak go używać, i zakończyć się kodem 1.
Bonus: drugi skrypt typ_menu.sh, który przez select pokazuje menu ze wszystkimi plikami w bieżącym folderze i dla wybranego woła typ.sh.
Pokaż rozwiązanie
#!/bin/bash
# typ.sh — zgaduje rodzaj pliku po końcówce nazwy
if [ $# -eq 0 ]; then
echo "Użycie: $0 NAZWA_PLIKU"
exit 1
fi
shopt -s nocasematch
case "$1" in
*.jpg|*.jpeg|*.png) echo "$1: obraz" ;;
*.txt|*.md) echo "$1: tekst" ;;
*.tar.gz|*.tgz|*.zip) echo "$1: archiwum" ;;
*.sh) echo "$1: skrypt" ;;
*) echo "$1: nie wiem, co to" ;;
esacZDJECIE.JPG: obraz
notatki.md: tekst
kopia.tar.gz: archiwum
instalka.sh: skrypt
dziwne.xyz: nie wiem, co toshopt -s nocasematch załatwia wielkość liter jedną linijką. Bez niego musiałbyś pisać *.[jJ][pP][gG], co wygląda jak hasło do Wi-Fi w hotelu. I zauważ: *.tar.gz stoi przed jakimkolwiek *.gz, bo case bierze pierwsze trafienie.
Bonus:
#!/bin/bash
# typ_menu.sh — wybierz plik z listy, a skrypt powie, co to
PS3="Który plik? (Ctrl+D kończy) "
select plik in *; do
if [ -n "$plik" ]; then
./typ.sh "$plik"
else
echo "Nie ma pliku numer $REPLY"
fi
done1) lista.txt
2) typ.sh
3) wakacje.PNG
Który plik? (Ctrl+D kończy) 3
wakacje.PNG: obraz
Który plik? (Ctrl+D kończy) 9
Nie ma pliku numer 9Gwiazdka po in zamienia się na listę plików, a select robi z niej menu. Dwie linijki i masz własnego menedżera plików. No, prawie. Bardzo prawie.
Co dalej
W bonusie select kręci się w kółko, dopóki nie przerwiesz. W następnej lekcji dowiesz się, jak takie kręcenie robić świadomie: pętle for, while i until, czyli jak kazać komputerowi zrobić to samo sto razy, kiedy Tobie nie chce się nawet dwa.