Przejdź do treści
Lekcja 35 z 117

Kurs › Moduł 3 › Lekcja 35

Jeśli… to…

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

Po tej lekcji: kursant pisze skrypty, które podejmują decyzje: if, elif, else, sprawdza pliki, liczby i napisy przez test, [ ] i [[ ]] i wie, które nawiasy wybrać.

Wstęp

bramkarz przed klubem z listą gości, obok terminal z if

Do tej pory nasze skrypty były jak automat z kawą: wrzucasz monetę, dostajesz zawsze to samo. A prawdziwy skrypt admina musi decydować. Czy plik z backupem w ogóle powstał? Czy dysk nie jest pełny? Czy użytkownik podał argument? Czy folder, który zaraz wyczyścimy, to na pewno ten folder?

Dziś skrypt dostaje bramkarza. Kogoś, kto stoi przy drzwiach, patrzy na gościa i decyduje: wchodzisz, nie wchodzisz, a ty idź do drugiego wejścia.

if, czyli bramkarz

if warunek; then
    komendy, gdy warunek jest spełniony
fi

if to „jeśli”. then to „to”. A fi to if czytane od tyłu i znaczy „koniec”. Ktoś uznał, że to zabawne. Miał rację, ale tylko za pierwszym razem.

I teraz najważniejsza rzecz w tej lekcji, którą większość kursów pomija: warunkiem w if jest komenda. Dowolna. Bramkarz nie sprawdza niczego sam. Woła komendę i patrzy na jej kciuk z poprzedniej lekcji. Kciuk w górę, czyli kod 0: wchodzisz. Kciuk w dół: nie.

#!/bin/bash
# czy_ktos.sh - if bez nawiasów: warunkiem może być każda komenda
if getent passwd "$1" > /dev/null; then
    echo "Konto $1 istnieje."
else
    echo "Konta $1 nie ma."
fi
$ ./czy_ktos.sh root
Konto root istnieje.
$ ./czy_ktos.sh nikt
Konta nikt nie ma.

Żadnych nawiasów. if uruchomił getent i spojrzał na kod wyjścia. To ten sam skrypt co w zadaniu, tylko czytelny. else to „w przeciwnym razie”: drugie drzwi, które otwierają się, gdy pierwsze zostały zamknięte.

then może stać w osobnej linijce, ale przyjęło się pisać go po średniku w tej samej. Wcięcia są dla ludzi, Bashowi jest wszystko jedno. Ludziom nie jest.

test i nawiasy kwadratowe

No dobrze, ale co, jak chcę sprawdzić coś, co nie jest komendą? Na przykład czy liczba jest większa od 5 albo czy plik istnieje? Wtedy używasz komendy, która nic nie robi, tylko sprawdza i pokazuje kciuk. Nazywa się test.

test 5 -gt 3; echo "$?"
[ 5 -gt 3 ]; echo "$?"
[ 5 -lt 3 ]; echo "$?"
0
0
1

I tu jest największy sekret Basha. Nawias kwadratowy [ to komenda. Dokładnie ta sama co test, tylko z innym imieniem i z wymogiem, żeby na końcu postawić ]. Jak urzędnik, który przyjmie wniosek tylko z pieczątką na końcu.

type test [ [[
ls -l /usr/bin/[
test is a shell builtin
[ is a shell builtin
[[ is a shell keyword
-rwxr-xr-x 1 root root 55744 Aug 25 17:09 /usr/bin/[

[ jest wbudowany w Basha, a dla innych powłok leży nawet jako osobny program w /usr/bin. Plik, który nazywa się nawias. Witaj w Linuksie.

Skoro [ to komenda, to obowiązują ją zasady komend: po nazwie komendy spacja, między argumentami spacje. Więc [ 5 -gt 3 ], a nie [5 -gt 3]. Każda spacja w tym zapisie jest obowiązkowa. To jest najczęstszy błąd początkujących i wrócimy do niego na końcu.

Porównywanie liczb

Do liczb są skróty z angielskiego, które trzeba po prostu zapamiętać:

-eq równe (equal), -ne różne (not equal), -lt mniejsze (less than), -le mniejsze lub równe, -gt większe (greater than), -ge większe lub równe.

Czemu nie < i >? Bo w Bashu to są przekierowania, nasze dwa głośniki. [ 10 > 9 ] nie porówna niczego, tylko utworzy plik o nazwie 9. Zupełnie jakbyś na pytanie „która liczba większa?” dostał w odpowiedzi nowy plik. Bardzo kreatywne. Bardzo bez sensu.

Zróbmy skrypt z prawdziwym wyborem z kilku dróg:

#!/bin/bash
# dysk.sh - ocenia zajętość dysku podaną w procentach
proc=$1

if [ -z "$proc" ]; then
    echo "Użycie: $0 PROCENT" >&2
    exit 2
fi

if [ "$proc" -ge 90 ]; then
    echo "ALARM: dysk zajęty w $proc%!"
    exit 1
elif [ "$proc" -ge 75 ]; then
    echo "Uwaga: dysk zajęty w $proc%."
else
    echo "OK: $proc% to jeszcze nic."
fi
$ ./dysk.sh 95
ALARM: dysk zajęty w 95%!
$ ./dysk.sh 80
Uwaga: dysk zajęty w 80%.
$ ./dysk.sh 30
OK: 30% to jeszcze nic.
$ ./dysk.sh
Użycie: ./dysk.sh PROCENT

elif to „else if”, czyli „a jeśli nie, to może…”. Bramkarz sprawdza warunki po kolei, od góry, i wpuszcza przez pierwsze pasujące drzwi. Reszty już nie sprawdza. Dlatego kolejność ma znaczenie: gdybyś najpierw sprawdził -ge 75, to 95 też by tam wpadło i alarmu nigdy by nie było. elif może być ile chcesz, else najwyżej jeden, na końcu.

A co, jeśli użytkownik wpisze bzdurę?

$ ./dysk.sh dużo
./dysk.sh: line 10: [: dużo: integer expression expected
./dysk.sh: line 13: [: dużo: integer expression expected
OK: dużo% to jeszcze nic.

[ wypluwa błąd, zwraca kod 2, czyli „nie”, więc bramkarz wpuszcza przez else. Skrypt twierdzi, że „dużo” to jeszcze nic. Optymista. Sprawdzanie, czy argument jest liczbą, zrobimy porządnie przy wyrażeniach regularnych.

Porównywanie napisów

= równe, != różne. Do tego dwa testy na pustkę:

-z "$x" — prawda, gdy napis jest pusty (zero długości).

-n "$x" — prawda, gdy napis nie jest pusty (non-zero).

[ -z "" ]; echo "$?"
[ -n "" ]; echo "$?"
0
1

I złota zasada z lekcji o cudzysłowach wraca tu ze zdwojoną siłą. Zobacz:

pusty=""
[ $pusty = "tak" ]
bash: [: =: unary operator expected

Bez cudzysłowu pusta zmienna znika całkowicie. Komenda [ dostaje = tak ] i nie wie, co porównać z czym. Z cudzysłowem dostaje pusty napis, porównuje go z „tak” i spokojnie mówi „nie”. A to jeszcze nic:

f=""
[ -f $f ]; echo "$?"
[ -f "$f" ]; echo "$?"
0
1

Bez cudzysłowu „czy pusty napis jest plikiem?” dało odpowiedź „tak”. Bo [ -f ] z jednym słowem sprawdza tylko, czy to słowo nie jest puste, a -f puste nie jest. Teraz wyobraź sobie if [ -f $plik ]; then rm .... Cudzysłowy, proszę państwa. Zawsze.

Pytania o pliki

To jest chleb powszedni admina. Najważniejsze testy:

-e — istnieje cokolwiek o tej nazwie (exists).

-f — istnieje i jest zwykłym plikiem (file).

-d — istnieje i jest folderem (directory).

-r — mogę czytać (readable).

-x — mogę uruchomić, a przy folderze: mogę do niego wejść (executable).

[ -e /etc/hostname ] && echo "coś jest"
[ -f /etc/hostname ] && echo "plik"
[ -d /etc ] && echo "folder"
[ -x /bin/ls ] && echo "można uruchomić"
coś jest
plik
folder
można uruchomić

Pytania -r i -x odpowiadają z punktu widzenia tego, kto pyta. Klucze do pokojów, pamiętasz? Ten sam plik /etc/shadow, w którym są skróty haseł:

# jako root:
[ -r /etc/shadow ]  ->  0
# jako zwykły użytkownik:
[ -r /etc/shadow ]  ->  1

[Ubuntu] /etc/shadow ma prawa -rw-r----- i grupę shadow. [RHEL] ma zwykle prawa ----------, czyli 0000, i tego wymagają też wytyczne bezpieczeństwa STIG. Root i tak przeczyta, bo dla roota zamki są z dykty.

Wzorzec, który będziesz pisać setki razy:

if [ ! -d "$cel" ]; then
    echo "Folder $cel nie istnieje" >&2
    exit 1
fi

Wykrzyknik odwraca test: „jeśli NIE jest folderem”. Najpierw sprawdź, potem działaj. Zwłaszcza przed rm.

Podwójne nawiasy [[ ]]

Bash ma też ulepszoną wersję: [[ ]]. To już nie komenda, tylko część gramatyki Basha, jak if czy fi. Dzięki temu Bash wie, że jest w środku testu, i może być mądrzejszy. Pojedyncze nawiasy to bramkarz z kartką i długopisem. Podwójne to bramkarz z czytnikiem kodów i bazy danych.

Co daje [[ ]]:

Zmienne bez cudzysłowu nie są cięte na słowa. [[ $pusty = "tak" ]] po prostu odpowie „nie”, bez błędu. Cudzysłowy i tak warto dawać z przyzwyczajenia, ale tu wybaczy.

&& i || w środku: [[ -f "$p" && -r "$p" ]]. W pojedynczych trzeba pisać [ -f "$p" ] && [ -r "$p" ].

Wzorce przy ==: [[ $plik == *.txt ]] sprawdza, czy nazwa kończy się na .txt. Gwiazdka działa jak w lekcji piątym, tylko porównuje napis, a nie szuka plików.

< i > porównują napisy alfabetycznie, a nie tworzą plików.

[[ "plik.txt" == *.txt ]] && echo "pasuje"
[ "plik.txt" == *.txt ]; echo "kod: $?"
pasuje
bash: [: too many arguments
kod: 2

W pojedynczych nawiasach gwiazdka została rozwinięta w listę plików .txt z bieżącego folderu i [ dostał za dużo słów.

Które więc wybrać? Piszesz skrypt z #!/bin/bash? Bierz [[ ]]. Piszesz pod #!/bin/sh, żeby działało też w dash na Ubuntu albo w malutkich systemach? Tam [[ ]] nie istnieje, zostają pojedyncze. Pamiętaj tylko, że [[ ]] uważa < za porównanie napisów, więc [[ 10 < 9 ]] jest prawdą, bo „1” jest w alfabecie przed „9”. Liczby zawsze przez -lt, -gt i spółkę.

Jak to działa pod spodem

if nie wie nic o liczbach, plikach ani napisach. Bash wykonuje to, co stoi między if a then, odbiera kod wyjścia, dokładnie tak jak przy &&, i na tej podstawie wybiera gałąź. Cała inteligencja siedzi w komendzie, którą wołasz. Możesz mieć nawet kilka komend przed then, liczy się kciuk ostatniej.

[ to zwykła komenda, więc zanim dostanie argumenty, Bash przepuszcza je przez wszystkie znane Ci kroki: rozwinięcie zmiennych, cięcie na słowa, rozwijanie gwiazdek. Dlatego niecytowana pusta zmienna znika, a gwiazdka zamienia się w listę plików. Komenda [ dostaje już gotową, czasem pokiereszowaną listę słów i liczy je: jedno słowo to test „czy niepuste”, dwa to test jednoargumentowy typu -f plik, trzy to porównanie. Przy złej liczbie słów mówi „too many arguments” albo „unary operator expected”.

[[ ]] Bash rozpoznaje wcześniej, już przy czytaniu linijki, i wewnątrz nie tnie zmiennych na słowa i nie rozwija gwiazdek. Dlatego jest bezpieczniejszy.

Ściąga flag

KomendaFlagaCo robi
[ ] / test-eq / -neliczby równe / różne
[ ] / test-lt / -lemniejsze / mniejsze lub równe
[ ] / test-gt / -gewiększe / większe lub równe
[ ] / test= / !=napisy równe / różne
[ ] / test-znapis pusty
[ ] / test-nnapis niepusty
[ ] / test-eistnieje cokolwiek
[ ] / test-fistnieje zwykły plik
[ ] / test-distnieje folder
[ ] / test-r / -w / -xmogę czytać / pisać / uruchomić
[ ] / test-splik istnieje i nie jest pusty
[ ] / test!odwraca wynik testu
[[ ]]== z *dopasowanie do wzorca
[[ ]]&& / ||„i” / „lub” w środku testu
ifelif / else / fikolejny warunek / w przeciwnym razie / koniec

⚠ Typowy błąd

x=10
[$x -eq 10]
[ "$x" -eq 10]
bash: [10: command not found
bash: [: missing `]'

Pierwsza linijka: bez spacji po nawiasie Bash skleja [ z wartością zmiennej i szuka komendy o nazwie [10. Nie ma takiej. Druga: bez spacji przed ] ostatnim słowem jest 10], a [ czeka na samodzielne ] jak na pieczątkę i jej nie dostaje.

Naprawa: spacje wszędzie. [ "$x" -eq 10 ]. Pomaga myślenie, że [ to komenda jak ls, a ] jej ostatni argument. Nikt nie pisze ls-l, więc nikt nie pisze [$x.

I drugi klasyk, już znany: [ 10 > 9 ] nie porównuje, tylko tworzy plik o nazwie 9. Jeśli w folderze ze skryptami nagle pojawiają się pliki o nazwach 9, 100 albo 0, to wiesz, kto je robi.

✎ Zadanie

Napisz skrypt sprawdz.sh, który dostaje ścieżkę jako argument i mówi, czy to plik, folder, czy nic takiego nie istnieje. Bez argumentu ma wypisać, jak go używać, i zakończyć się kodem 1.

Przetestuj na /etc/hostname, /etc, na czymś, czego nie ma, na ścieżce ze spacją i bez argumentu. Sprawdź kod wyjścia przez echo $? i przepuść skrypt przez ShellCheck.

Pokaż rozwiązanie
#!/bin/bash
# sprawdz.sh — mówi, czym jest podana ścieżka

if [ $# -eq 0 ]; then
    echo "Użycie: $0 ŚCIEŻKA"
    exit 1
fi

if [ -f "$1" ]; then
    echo "$1 to plik"
elif [ -d "$1" ]; then
    echo "$1 to folder"
elif [ -e "$1" ]; then
    echo "$1 istnieje, ale to ani zwykły plik, ani folder"
else
    echo "$1 nie istnieje"
fi

Sprawdzamy:

./sprawdz.sh; echo "kod: $?"
./sprawdz.sh /etc/hostname
./sprawdz.sh /etc
./sprawdz.sh /nie/ma
./sprawdz.sh /dev/null
Użycie: ./sprawdz.sh ŚCIEŻKA
kod: 1
/etc/hostname to plik
/etc to folder
/nie/ma nie istnieje
/dev/null istnieje, ale to ani zwykły plik, ani folder

Najpierw bramkarz sprawdza, czy w ogóle ktoś przyszedł, czyli $#. Potem kolejność ma znaczenie: -f i -d przed -e, bo -e mówi „tak” na wszystko, co istnieje. Trzecia gałąź to bonus dla dociekliwych: /dev/null istnieje, ale nie jest ani plikiem, ani folderem, tylko urządzeniem. Czarna dziura z własnym adresem.

I najważniejsze: "$1" w cudzysłowie. Bez koperty skrypt wyłoży się na pliku moje notatki, bo spacja rozetnie nazwę na dwa kawałki. Jeśli Twój skrypt na to nie wpadł, sprawdź go teraz. Ja poczekam.

Co dalej

Bramkarz z if i elif jest świetny, ale kiedy drzwi jest dziesięcioro, robi się z tego drabinka nie do czytania. W następnej lekcji poznasz case, czyli wybór z menu po wzorcach, i select, który sam narysuje użytkownikowi ponumerowane menu.