Przejdź do treści
Lekcja 36 z 117

Kurs › Moduł 3 › Lekcja 36

Wybór z menu

⏱ ok. 10 min czytania· moduł 3: Pisanie skryptów

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
        ;;
esac

case "$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 Start
Uruchamiam 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: moze

I 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" ;;
esac

Jak 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 nocasematch

Po 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
done
1) 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 zobaczenia

Rozbierzmy 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 select
case is a shell keyword
select is a shell keyword

Co 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

KomendaFlaga / elementCo robi
casewzorzec)początek przegródki
casea|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
caseesackoniec całego case
shopt-s nocasematchwielkość liter bez znaczenia w case i [[ ]]
selectPS3tekst zaproszenia przed numerem
selectREPLYto, co użytkownik naprawdę wpisał
selectbreakwyjście z menu
selectCtrl+Dkoniec wejścia, menu się kończy

⚠ Typowy błąd

Zapomniane dwa średniki:

case "$1" in
    start)
        echo "Start"
    stop)
        echo "Stop"
        ;;
esac
zle.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" ;;
esac

Dla 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" ;;
esac
ZDJECIE.JPG: obraz
notatki.md: tekst
kopia.tar.gz: archiwum
instalka.sh: skrypt
dziwne.xyz: nie wiem, co to

shopt -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
done
1) 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 9

Gwiazdka 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.