Słowniki
Po tej lekcji: kursant tworzy słownik w Bashu przez declare -A, wkłada i wyjmuje wartości po nazwie, przechodzi po kluczach, sprawdza, czy klucz istnieje, i liczy nim wystąpienia, wiedząc, że na Macu potrzebuje do tego nowego Basha.
Wstęp
W poprzedniej lekcji mieliśmy szufladę z przegródkami numer 0, 1, 2. Świetna, dopóki nie zapytasz: „jaki adres ma db01?”. Wtedy musisz pamiętać, że db01 siedzi w przegródce numer 2, a jego IP w drugiej tablicy, też pod numerem 2. Dwie tablice trzymane równo jak dwa końce prześcieradła przy składaniu. Puszczasz jeden i wszystko się rozjeżdża.
Dziś numerki wyrzucamy. Przegródki dostają nazwy. Pytasz „db01?” i dostajesz adres. Bez liczenia na palcach.
Słownik, czyli szatnia z nazwiskami
Wyobraź sobie szatnię w teatrze. Zwykle dostajesz numerek. Ale w tej szatni nie ma numerków. Na każdym wieszaku jest nazwisko. Przychodzisz, mówisz „Kowalski” i pani podaje Ci płaszcz Kowalskiego.
To jest słownik. Po angielsku associative array, tablica asocjacyjna. Nazwisko na wieszaku to klucz, płaszcz to wartość. W innych językach zwie się to dictionary, hash albo mapa, ale chodzi o to samo: szatnia z nazwiskami.
I jedna rzecz, której nie wolno pominąć. Słownik trzeba ogłosić, zanim go użyjesz:
declare -A ipdeclare -A, wielkie A. To jak zawieszenie tabliczki „szatnia imienna”. Bez niej Bash założy, że to zwykła szuflada z numerami. Co się wtedy dzieje, zobaczysz w błędach i obiecuję, że zapamiętasz.
Teraz wieszamy płaszcze:
ip[web01]=10.0.0.11
ip[web02]=10.0.0.12
ip[db01]=10.0.0.21
echo "${ip[db01]}"10.0.0.21Nawias kwadratowy jak w tablicy, tylko w środku zamiast numeru jest nazwa. I tyle. Pytasz po nazwie, dostajesz wartość.
Możesz też zawiesić wszystko naraz, przy ogłoszeniu:
declare -A port=([ssh]=22 [http]=80 [https]=443 ["dns server"]=53)
echo "${port[https]} ${port["dns server"]}"443 53Klucz ze spacją? Proszę bardzo, tylko w kopercie, czyli w cudzysłowie.
Ile wieszaków, jakie nazwiska, jakie płaszcze
Znaczki znasz z tablic. Działają tak samo:
echo "${#ip[@]}"
echo "${!ip[@]}"
echo "${ip[@]}"3
db01 web01 web02
10.0.0.21 10.0.0.11 10.0.0.12# to liczba wieszaków. Wykrzyknik ! daje listę kluczy, czyli nazwisk. A samo @ daje wartości, czyli płaszcze.
Pętla po słowniku to prawie zawsze pętla po kluczach. Bierzesz nazwisko, a płaszcz wyciągasz po nazwisku:
for h in "${!ip[@]}"; do
echo "$h -> ${ip[$h]}"
donedb01 -> 10.0.0.21
web01 -> 10.0.0.11
web02 -> 10.0.0.12A teraz uważnie. Kolejność wyszła tu ładna, alfabetyczna. Przypadek. Zobacz słownik portów:
declare -p portdeclare -A port=([ssh]="22" [https]="443" ["dns server"]="53" [http]="80" )Wpisywałem ssh, http, https, dns. Wyszło ssh, https, dns, http. Słownik nie pamięta kolejności. Szatniarka wiesza płaszcze tam, gdzie jej wygodnie, a nie w kolejności przychodzenia. Jeśli kolejność ma znaczenie, sortujesz sam:
for h in $(printf '%s\n' "${!ip[@]}" | sort); do
echo "$h -> ${ip[$h]}"
doneCzy taki wieszak w ogóle istnieje?
Pytasz o kogoś, kogo nie ma:
echo "[${ip[mail01]}]"[]Pusto. Żadnego błędu. Szatniarka nie mówi „nie ma takiego”, tylko podaje Ci powietrze. To podstępne, bo pusty wynik wygląda tak samo jak płaszcz, który ktoś zostawił pusty. Dlatego sprawdzamy jawnie, operatorem -v:
if [[ -v ip[web01] ]]; then echo "web01 jest"; fi
if [[ ! -v ip[mail01] ]]; then echo "mail01 nie ma"; fiweb01 jest
mail01 nie maAlbo, jeśli wystarczy Ci wartość zastępcza, nożyczki z lekcji 41:
echo "${ip[mail01]:-nieznany}"nieznanyZdjęcie płaszcza z wieszaka to unset, a podmiana to po prostu nowe przypisanie:
unset 'ip[web02]'
ip[web01]=10.0.0.99
echo "${!ip[@]}"
echo "${ip[web01]}"db01 web01
10.0.0.99Jeden klucz to jeden wieszak. Nie da się mieć dwóch web01. Drugie przypisanie nadpisuje pierwsze.
Licznik: słownik w roli kasownika
Teraz najczęstsze zastosowanie słownika u admina. Liczenie. Ile razy pojawił się dany adres w logu, ile kont ma daną powłokę, ile plików ma dane rozszerzenie.
Obraz: liczenie głosów w wyborach samorządowych. Wyciągasz kartkę, czytasz nazwisko, stawiasz kreskę przy tym nazwisku. Nowe nazwisko? Nowa linijka i pierwsza kreska.
#!/bin/bash
# powloki.sh – ile kont używa której powłoki
declare -A licznik
while IFS=: read -r _ _ _ _ _ _ powloka; do
(( licznik[$powloka]++ ))
done < /etc/passwd
for p in "${!licznik[@]}"; do
echo "${licznik[$p]} $p"
done | sort -rn37 /usr/sbin/nologin
32 /sbin/nologin
11 /bin/bash
4 /bin/false
1 /bin/sync
1 /bin/shRozbierzmy to. IFS=: tnie linijkę z /etc/passwd na dwukropkach. Pierwsze sześć pól nas nie interesuje, więc wrzucamy je do _, czyli do kosza. Siódme to powłoka. (( licznik[$powloka]++ )) stawia kreskę. Jak klucza jeszcze nie było, Bash traktuje go jak zero i robi z niego jedynkę. Na koniec wypisujemy „liczba nazwa” i sortujemy liczbowo od największej.
Ta sama technika to już połowa analizy logów: kto się najczęściej dobija, który adres wali w /admin. Zrobisz to w zadaniu.
Słownik jako konfiguracja
Drugie zastosowanie: wczytanie pliku ustawień w stylu klucz=wartość. Mamy plik ustawienia.conf:
# ustawienia kopii
host=db01
port=5432
uzytkownik=backup#!/bin/bash
declare -A konfig
while IFS='=' read -r klucz wartosc; do
[[ -z $klucz || $klucz == '#'* ]] && continue
konfig[$klucz]=$wartosc
done < ustawienia.conf
echo "Łączę się z ${konfig[host]}:${konfig[port]} jako ${konfig[uzytkownik]}"
echo "Hasło: ${konfig[haslo]:-BRAK, ustaw w pliku}"Łączę się z db01:5432 jako backup
Hasło: BRAK, ustaw w plikuPuste linijki i komentarze z krzyżykiem pomijamy przez continue. Reszta ląduje w szatni. I zamiast pięciu zmiennych o pięciu nazwach masz jedną szatnię, z której bierzesz, co trzeba.
[macOS] Słowniki wymagają nowego Basha
Tu już się nie wymigasz. Słowniki weszły do Basha w wersji 4.0, w 2009 roku. Systemowy Bash na Macu to 3.2. Napiszesz declare -A i dostaniesz:
-bash: declare: -A: invalid optionCo robisz? brew install bash, a skrypty zaczynasz od #!/usr/bin/env bash. Ta linijka mówi „użyj pierwszego Basha, jakiego znajdziesz na ścieżkach”, więc weźmie tego z Homebrew. #!/bin/bash na Macu zawsze wskazuje na stary.
I uwaga na pułapkę, w którą wpada pełno ludzi. bash --version pokazuje wersję pierwszego bash na ścieżkach, a nie tej powłoki, w której właśnie siedzisz. Prawdę mówi zmienna:
echo "$BASH_VERSION"5.2.21(1)-releaseNa serwerach z Linuksem masz Basha 4 albo 5 od lat, więc tam ten problem nie istnieje. A jeśli piszesz skrypt, który może trafić na Maca, daj mu bramkarza na samej górze:
if (( BASH_VERSINFO[0] < 4 )); then
echo "Ten skrypt wymaga Basha 4 lub nowszego, masz $BASH_VERSION" >&2
exit 1
fiLepiej odbić się od bramki z jasnym komunikatem niż wywrócić w połowie sali.
Jak to działa pod spodem
Zwykła tablica w Bashu to lista par numer–tekst. Słownik to tablica mieszająca, po angielsku hash table. Bash bierze klucz, na przykład db01, przepuszcza go przez funkcję mieszającą i wychodzi z tego numer kubełka. W tym kubełku szuka wartości.
To jak szatnia, w której nazwiska na A do F wiszą w pierwszym rzędzie, G do M w drugim i tak dalej. Szatniarka nie przegląda całej szatni, idzie od razu do właściwego rzędu. Dlatego wyszukanie po kluczu jest szybkie nawet przy tysiącach wieszaków. I dlatego kolejność jest taka dziwna: o miejscu decyduje funkcja mieszająca, a nie kolejność wpisywania.
I druga rzecz, pod spodem najważniejsza. W zwykłej tablicy to, co w nawiasie kwadratowym, Bash liczy jak działanie matematyczne. W słowniku traktuje jako tekst. To jedno zdanie wyjaśnia błąd, który zaraz zobaczysz.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
declare -A s | -A | ogłasza słownik (obowiązkowo, Bash 4+) |
declare -A s=([a]=1 [b]=2) | lista w nawiasie | tworzy słownik z wartościami |
s[klucz]=wartość | [klucz] | wstawia albo nadpisuje |
${s[klucz]} | wartość pod kluczem (pusto, gdy brak) | |
${!s[@]} | ! | lista kluczy |
${s[@]} | @ | lista wartości |
${#s[@]} | # | liczba par |
| [[ -v s[klucz] ]] | -v | czy klucz istnieje |
${s[klucz]:-x} | :- | wartość albo x, gdy brak |
unset 's[klucz]' | usuwa jedną parę | |
(( s[$k]++ )) | ++ | licznik: dodaje 1 przy kluczu |
declare -p s | -p | pokazuje cały słownik |
$BASH_VERSION | wersja Basha, w którym jesteś | |
BASH_VERSINFO[0] | główny numer wersji jako liczba |
⚠ Typowy błąd
Zapomniałeś o declare -A:
#!/bin/bash
ip[web01]=10.0.0.11
ip[db01]=10.0.0.21
echo "${ip[web01]}"
declare -p ip10.0.0.21
declare -a ip=([0]="10.0.0.21")Pytasz o web01, dostajesz adres db01. I żadnego błędu.
Co się stało? Bez tabliczki „szatnia imienna” Bash zrobił zwykłą tablicę z numerami. A w zwykłej tablicy nawias kwadratowy to matematyka. web01 potraktował jako nazwę zmiennej w działaniu. Takiej zmiennej nie ma, więc wyszło zero. db01 też zero. Oba adresy poleciały do przegródki numer 0, drugi nadpisał pierwszy. A pytanie o ip[web01] to też pytanie o zero. Szatnia, w której wszyscy dostają ten sam płaszcz.
Zobacz declare -a, małe a. To jest dowód. ShellCheck też coś czuje:
SC2154 (warning): web01 is referenced but not assigned.Czyli: „używasz zmiennej web01, której nigdzie nie ustawiłeś”. Jeśli widzisz takie ostrzeżenie przy nawiasie kwadratowym, prawie na pewno brakuje declare -A. Naprawa: dopisz je na początku. I jeszcze jedno: declare -A w funkcji robi słownik lokalny, jak local. Jeśli chcesz słownik widoczny dla całego skryptu, ogłoś go poza funkcją albo przez declare -gA.
✎ Zadanie
Napisz skrypt rozszerzenia.sh, który dostaje folder jako argument (bez argumentu bierze /etc) i liczy słownikiem, ile jest w nim plików z każdym rozszerzeniem. Pliki bez kropki w nazwie licz jako „(bez rozszerzenia)”. Wypisz pięć najczęstszych, od największej liczby.
Podpowiedź: nożyczki z lekcji 41 wyciągną rozszerzenie z nazwy pliku.
Rozwiązanie znajdziesz pod spodem.
Pokaż rozwiązanie
Zadanie: policzyć słownikiem rozszerzenia plików w folderze i wypisać pięć najczęstszych.
#!/bin/bash
# rozszerzenia.sh – ile plików jakiego rodzaju leży w folderze
folder=${1:-/etc}
declare -A ile
for f in "$folder"/*; do
[[ -f $f ]] || continue
nazwa=${f##*/}
if [[ $nazwa == *.* ]]; then
roz=${nazwa##*.}
else
roz="(bez rozszerzenia)"
fi
(( ile[$roz]++ ))
done
for r in "${!ile[@]}"; do
echo "${ile[$r]} $r"
done | sort -rn | head -555 (bez rozszerzenia)
39 conf
2 rc
2 net
2 denyW tym jednym skrypcie są cztery ostatnie lekcje naraz. Wartość domyślna ${1:-/etc}. Nożyczki ##*/ i ##*.. Słownik z licznikiem. I bramkarz [[ -f $f ]] || continue, który wpuszcza tylko pliki, a foldery odsyła do domu.
Wynik też mówi coś o Linuksie. Większość plików w /etc nie ma rozszerzenia wcale, bo Linux, w przeciwieństwie do Windowsa, nie potrzebuje kropki, żeby wiedzieć, co to za plik. Z tych, które mają, króluje .conf. Zero niespodzianek, ale teraz masz na to dowód.
I zauważ, jak brzydko ten wynik wygląda. Liczba, spacja, nazwa. Jak paragon wydrukowany na drukarce, której skończył się toner. Dziś to naprawimy.
Co dalej
Nasze wyniki wyglądają na razie jak notatki na serwetce: liczba, spacja, nazwa, wszystko krzywo. W następnej lekcji robimy z tego równe tabelki przez printf, a długie teksty, maile i pliki konfiguracyjne wpisujemy here-dokiem.