Przejdź do treści
Lekcja 38 z 117

Kurs › Moduł 3 › Lekcja 38

Bash umie liczyć (trochę)

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

Po tej lekcji: kursant liczy w skrypcie przez $(( )) i (( )), wie, że Bash zna tylko liczby całkowite, używa bc do ułamków i losuje liczby przez RANDOM.

Wstęp

terminal, wpisuję echo $(( 7 / 2 )), wynik 3

Siedem podzielić na dwa. Każde dziecko w trzeciej klasie powie: trzy i pół. Bash mówi: trzy. Bez przecinka, bez przeprosin, bez mrugnięcia.

To nie jest błąd. To jest decyzja. Bash to powłoka, a nie kalkulator naukowy, i liczy tylko na liczbach całkowitych. Do tego, czego potrzebuje admin, czyli liczenia plików, prób, procentów zajętości dysku i sekund, w zupełności to wystarcza. A jak potrzebujesz ułamków, zawołasz kolegi z innego działu. Dziś poznasz obu.

$(( )), czyli kalkulator w okienku

Wyobraź sobie, że urzędnik w okienku ma pod ladą prosty kalkulator. Taki z bazaru, bez przecinka. Jeśli w podaniu napiszesz działanie w podwójnych nawiasach z dolarem, urzędnik je policzy i wpisze wynik w to miejsce.

echo $(( 2 + 3 * 4 ))
echo $(( (2 + 3) * 4 ))
echo $(( 7 / 2 ))
echo $(( 7 % 2 ))
echo $(( 2 ** 10 ))
14
20
3
1
1024

Kolejność działań jak w szkole: mnożenie przed dodawaniem, nawiasy najpierw. / to dzielenie bez reszty, czyli obcina wszystko po przecinku. % to reszta z dzielenia, czyli to, co zostało. ** to potęga.

Ta reszta z dzielenia wygląda na ciekawostkę z lekcji matematyki, a jest jednym z najprzydatniejszych narzędzi. Liczba parzysta? x % 2 daje zero. Co dziesiąty plik? i % 10 równe zero. Rzut kostką? Zaraz zobaczysz.

W środku podwójnych nawiasów zmienne możesz pisać bez dolara:

a=10
b=3
echo $(( a * b ))
30

Z dolarem też zadziała, ale ShellCheck grzecznie zauważy, że jest zbędny. W kalkulatorze każde słowo i tak jest traktowane jak nazwa pudełka z liczbą.

I ważny drobiazg: całe $(( ... )) zamienia się na wynik. Możesz go wstawić gdziekolwiek, w echo, w przypisanie, w środek tekstu:

zajete=37
calosc=120
echo "Zajęte: $(( zajete * 100 / calosc ))%"
Zajęte: 30%

(( )), czyli kalkulator bez dolara

Te same podwójne nawiasy, ale bez dolara z przodu, to nie jest „wstaw wynik”, tylko „zrób to działanie”. Służą do dwóch rzeczy.

Pierwsza: zmiana zmiennej w miejscu.

licznik=0
(( licznik++ ))
(( licznik += 5 ))
echo "$licznik"
6

++ dodaje jeden, -- odejmuje jeden, += 5 dodaje pięć. Jak klikacz, którym bramkarz w klubie liczy wchodzących.

Druga, ważniejsza: warunek. Pamiętasz bramkarza [ "$a" -gt "$b" ] z -gt, -lt, -eq? W podwójnych nawiasach możesz napisać to po ludzku:

if (( a > b )); then
    echo "a większe"
fi

>, <, >=, <=, ==, !=. Jak w szkole, a nie jak w szyfrze. Pod jednym warunkiem: to działa tylko dla liczb. Do porównywania tekstów dalej używasz [ ] albo [[ ]].

Jak (( )) mówi „prawda” albo „fałsz”? Bardzo prosto: wynik zero to kciuk w dół, wynik inny niż zero to kciuk w górę.

(( 0 )); echo "kod po ((0)): $?"
(( 5 )); echo "kod po ((5)): $?"
kod po ((0)): 1
kod po ((5)): 0

Tak, to wygląda odwrotnie. W świecie kodów wyjścia zero to sukces, a w świecie matematyki zero to fałsz. (( )) tłumaczy jedno na drugie, jak tłumacz przysięgły na granicy dwóch krajów, w których kiwanie głową znaczy co innego.

let, czyli starość

Zobaczysz też w starych skryptach taką postać:

let "wynik = a + b"
echo "$wynik"
13

let robi to samo co (( )), tylko wygląda jak z innej epoki. ShellCheck nawet podpowiada: „Instead of let expr, prefer (( expr ))”. Rozpoznawaj, ale sam pisz (( )). Jak z czekami w banku: wiesz, że istnieją, ale nie płacisz nimi za bułki.

Tylko liczby całkowite

Spróbujmy ułamka:

echo $(( 1.5 + 1 ))
bash: 1.5 + 1 : syntax error: invalid arithmetic operator (error token is ".5 + 1 ")

Kalkulator z bazaru zobaczył kropkę i się obraził. Bash w ogóle nie zna ułamków. Ani w wyniku, ani na wejściu.

A jak dzielisz, kolejność ma znaczenie:

echo "$(( zajete * 100 / calosc ))%"
echo "$(( zajete / calosc * 100 ))%"
30%
0%

W drugiej linijce Bash najpierw policzył 37 przez 120, wyszło zero z kawałkiem, kawałek wyrzucił, i zero razy sto to zero. Dysk w połowie pełny, a skrypt mówi „0%, luzik”. Zasada: przy procentach najpierw mnóż, potem dziel. ShellCheck też to wyłapie.

I jeszcze dzielenie przez zero:

bash: 10 / 0 : division by 0 (error token is "0 ")

Przynajmniej Bash mówi to wprost, zamiast wymyślać wynik.

bc, czyli kolega z działu księgowości

Kiedy potrzebujesz przecinka, urzędnik wysyła pismo do księgowości. Księgowość nazywa się bc. To osobny program, kalkulator z własnym językiem, który liczy z dowolną dokładnością. Działanie podajesz mu rurą:

echo '7 / 2' | bc
echo 'scale=2; 7 / 2' | bc
echo '1.5 + 1' | bc
echo '10 / 3' | bc -l
echo '2 ^ 100' | bc
3
3.50
2.5
3.33333333333333333333
1267650600228229401496703205376

Niespodzianka w pierwszej linijce: bc też domyślnie dzieli bez ułamków. scale=2 mówi „dwa miejsca po przecinku”. -l włącza bibliotekę matematyczną i od razu ustawia dużo miejsc po przecinku. A 2 ^ 100 pokazuje, że bc nie ma limitu wielkości liczb. Bash przy takiej liczbie dawno by się przekręcił jak licznik w starym aucie.

Uwaga na potęgę: w Bashu **, w bc daszek ^. Dwa działy, dwa formularze.

Na naszym procencie dysku:

echo "scale=1; 37 * 100 / 120" | bc
30.8

[Ubuntu] [RHEL] bc jest w pakiecie o tej samej nazwie, ale na minimalnej instalacji serwera może go nie być. Sprawdzisz przez command -v bc. Brak? sudo apt install bc albo sudo dnf install bc.

RANDOM, czyli kostka do gry

Bash ma wbudowaną maszynę losującą. To zmienna RANDOM, która przy każdym odczytaniu daje inną liczbę od 0 do 32767.

echo "$RANDOM $RANDOM"
16518 17388

Sama w sobie jest mało przydatna, bo kto potrzebuje liczby od zera do trzydziestu dwóch tysięcy? Ale właśnie tu wraca reszta z dzielenia. RANDOM % 6 daje resztę z dzielenia przez sześć, czyli coś od 0 do 5. Dodaj jeden i masz kostkę:

#!/bin/bash
# kostka.sh — rzuca kostką kilka razy i liczy średnią

rzuty=4
suma=0

for (( i = 1; i <= rzuty; i++ )); do
    oczka=$(( RANDOM % 6 + 1 ))
    echo "Rzut $i: $oczka"
    (( suma += oczka ))
done

echo "Suma: $suma, średnio: $(( suma / rzuty ))"
Rzut 1: 4
Rzut 2: 3
Rzut 3: 2
Rzut 4: 6
Suma: 15, średnio: 3

Średnia to naprawdę 3,75, ale Bash obciął do trzech. Wiesz już, dlaczego.

Po co adminowi losowanie? Żeby 300 serwerów nie uderzyło po aktualizacje w tę samą sekundę. Dajesz w skrypcie sleep $(( RANDOM % 60 )) i każdy serwer czeka losowo do minuty. Jak w sklepie, kiedy otwierają nową kasę i nie wszyscy rzucają się naraz. Przynajmniej w teorii.

Uwaga, bardzo ważna: RANDOM nie nadaje się do haseł ani niczego, co ma być tajne. To kostka do gry, a nie sejf. Do haseł służą narzędzia, które poznamy w module o bezpieczeństwie.

Jak to działa pod spodem

$(( )), (( )) i let to wszystko robota samego Basha. Żadnego osobnego programu, cały kalkulator jest wbudowany w urzędnika. Działania i ich kolejność są takie same jak w języku C, bo Bash został napisany w C i po prostu pożyczył zasady.

Kiedy Bash widzi $(( zajete * 100 / calosc )), robi to w kolejności: podstawia wartości zmiennych, zamienia tekst na liczby całkowite, liczy i wstawia wynik z powrotem jako tekst. Bo w Bashu wszystko jest tekstem, nawet liczby. zajete=37 to tak naprawdę pudełko z napisem „37”, które kalkulator na chwilę traktuje jak liczbę.

A co, jeśli w pudełku nie ma liczby?

x=abc
echo $(( x + 1 ))
1

Kalkulator potraktował abc jak nazwę kolejnej zmiennej, nie znalazł jej i przyjął zero. Pusta zmienna też daje zero. Wygodne i zdradliwe jednocześnie: literówka w nazwie zmiennej nie daje błędu, tylko zero. Dlatego dane od użytkownika warto najpierw sprawdzić, czy to w ogóle liczba.

bc z kolei to osobny program, który trzeba uruchomić przez rurę. Czyli nowy proces, nowy kucharz w kuchni. Przy jednym działaniu nie ma to znaczenia, ale w pętli na milion okrążeń różnica będzie wyraźna. Liczby całkowite licz w Bashu, ułamki dopiero w bc.

Ściąga flag

KomendaFlaga / elementCo robi
$(( ))+ - * /dodawanie, odejmowanie, mnożenie, dzielenie bez ułamka
$(( ))%reszta z dzielenia
$(( ))**potęga
(( ))x++ / x--dodaj / odejmij jeden
(( ))x += 5dodaj 5 do zmiennej
(( ))> < >= <= == !=porównanie liczb w warunku
letlet "x = 1 + 2"stara postać (( ))
bcscale=NN miejsc po przecinku
bc-lbiblioteka matematyczna, dużo miejsc po przecinku
bc^potęga w bc
RANDOM$(( RANDOM % N + 1 ))losowa liczba od 1 do N
RANDOMRANDOM=42zasianie, powtarzalny ciąg

⚠ Typowy błąd

Skrypt bierze godzinę albo miesiąc z daty i coś z nią liczy. Działał świetnie przez całe lato. A pewnego dnia o ósmej rano:

#!/bin/bash
godzina=08
echo $(( godzina + 1 ))
t3.sh: line 3: 08: value too great for base (error token is "08")

Dlaczego? Bo liczba z zerem z przodu to dla Basha zapis ósemkowy. A w systemie ósemkowym są tylko cyfry od 0 do 7. Ósemka i dziewiątka nie istnieją. 07 przejdzie, 08 i 09 wywalą skrypt. Dlatego ten błąd wychodzi w sierpniu, we wrześniu i o ósmej i dziewiątej rano, czyli dokładnie wtedy, kiedy nikt się go nie spodziewa i wszyscy są na urlopie albo w drodze do pracy.

Naprawa: powiedz kalkulatorowi wprost, że to liczba dziesiętna.

godzina=08
echo $(( 10#$godzina + 1 ))
9

10# znaczy „to jest w systemie dziesiętnym”. Tu dolar przed zmienną jest potrzebny, bo 10# musi stać tuż przed cyframi.

✎ Zadanie

Napisz grę zgadnij.sh. Skrypt losuje liczbę od 1 do 100 i w pętli pyta gracza o liczbę. Po każdej próbie mówi „Więcej” albo „Mniej”. Po trafieniu wypisuje, za którym razem się udało, i kończy grę. Jeśli ktoś wpisze coś, co nie jest liczbą, skrypt ma tego nie liczyć jako próby i zapytać jeszcze raz.

Podpowiedź: RANDOM, while true, read, (( )) i case z wzorcem na „coś, co nie jest cyfrą”.

Pokaż rozwiązanie
#!/bin/bash
# zgadnij.sh — zgadnij liczbę od 1 do 100

sekret=$(( RANDOM % 100 + 1 ))
proby=0

while true; do
    read -r -p "Twoja liczba: " liczba
    case "$liczba" in
        ""|*[!0-9]*)
            echo "To nie jest liczba, spróbuj jeszcze raz"
            continue
            ;;
    esac
    proby=$(( proby + 1 ))
    if (( liczba < sekret )); then
        echo "Więcej"
    elif (( liczba > sekret )); then
        echo "Mniej"
    else
        echo "Trafione za $proby razem!"
        break
    fi
done
Twoja liczba: abc
To nie jest liczba, spróbuj jeszcze raz
Twoja liczba: 50
Mniej
Twoja liczba: 25
Więcej
Twoja liczba: 37
Trafione za 3 razem!

Najciekawszy jest wzorzec *[!0-9]*. Wykrzyknik w nawiasach kwadratowych znaczy „znak, który nie jest cyfrą”. Czyli: jeśli gdziekolwiek w tekście jest coś innego niż cyfra, to nie liczba. Do tego "" dla pustego Entera. I zauważ continue przed liczeniem próby, więc literówki nie psują wyniku.

A strategia na wygraną? Zawsze strzelaj w połowę tego, co zostało: 50, 25, 37 i tak dalej. Przy stu liczbach zawsze trafisz w maksymalnie siedmiu próbach. To się nazywa wyszukiwanie binarne i jest pytaniem na połowie rozmów rekrutacyjnych. Proszę bardzo, właśnie się przygotowałeś, grając w grę.

Co dalej

W kostka.sh wynik działania wpadający do zmiennej był prosty, bo liczył sam Bash. A co, jeśli chcesz w zmiennej zapisać wynik prawdziwej komendy, na przykład dzisiejszą datę albo liczbę zalogowanych użytkowników? O tym następna lekcja: wynik komendy w zmiennej.