Bash umie liczyć (trochę)
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
1024Kolejność 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 ))30Z 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)): 0Tak, 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"13let 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' | bc3
3.50
2.5
3.33333333333333333333
1267650600228229401496703205376Niespodzianka 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" | bc30.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 17388Sama 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 ))1Kalkulator 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
| Komenda | Flaga / element | Co robi |
|---|---|---|
$(( )) | + - * / | dodawanie, odejmowanie, mnożenie, dzielenie bez ułamka |
$(( )) | % | reszta z dzielenia |
$(( )) | ** | potęga |
(( )) | x++ / x-- | dodaj / odejmij jeden |
(( )) | x += 5 | dodaj 5 do zmiennej |
(( )) | > < >= <= == != | porównanie liczb w warunku |
let | let "x = 1 + 2" | stara postać (( )) |
bc | scale=N | N miejsc po przecinku |
bc | -l | biblioteka matematyczna, dużo miejsc po przecinku |
bc | ^ | potęga w bc |
RANDOM | $(( RANDOM % N + 1 )) | losowa liczba od 1 do N |
RANDOM | RANDOM=42 | zasianie, 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 ))910# 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
doneTwoja 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.