Listy (tablice)
Po tej lekcji: kursant tworzy tablice w Bashu, dokłada do nich elementy, liczy je, przechodzi po nich pętlą bez gubienia spacji i wczytuje plik do tablicy przez mapfile.
Wstęp
skrypt, który ma serwer1=web01, serwer2=web02, serwer3=db01 i trzy prawie identyczne bloki ping
Znasz takie skrypty. Trzy serwery, trzy zmienne, trzy kopie tego samego kodu. A potem szef mówi: „doszło nam dwanaście maszyn”. I zaczyna się kopiuj-wklej, w którym zawsze gdzieś zostaje serwer7 zamiast serwer8.
Dziś zrobimy z tego jedną listę, jedną pętlę i jeden plik z nazwami serwerów. Doszedł serwer? Dopisujesz linijkę do pliku. Skryptu nie ruszasz.
Tablica, czyli szuflada z przegródkami
Zwykła zmienna to pudełko z napisem. Jedno pudełko, jedna rzecz. Tablica to szuflada z przegródkami, jak ta na sztućce. Jedna nazwa na froncie szuflady, a w środku ponumerowane przegródki: zero, jeden, dwa…
Tak, od zera. Programiści liczą od zera i nie ma od tego odwołania.
Tablicę tworzysz nawiasami okrągłymi, elementy oddzielasz spacją:
serwery=(web01 web02 db01)
echo "${serwery[0]}"
echo "${serwery[2]}"web01
db01Numer przegródki, czyli indeks, piszesz w nawiasie kwadratowym. I klamry są tu obowiązkowe. Zaraz w błędach zobaczysz, co się dzieje bez nich.
Cała szuflada naraz to @, a liczba elementów to znany już krzyżyk:
echo "${serwery[@]}"
echo "${#serwery[@]}"web01 web02 db01
3Dokładanie i wyjmowanie
Dokładanie na koniec to += z nawiasem. Nawias jest ważny, zaraz powiem dlaczego.
serwery+=(backup01)
serwery+=("mail 01")
echo "${#serwery[@]}"
echo "${serwery[-1]}"5
mail 01[-1] to ostatni element, [-2] przedostatni. [macOS] Ujemne indeksy działają od Basha 4.2, na systemowym Bashu z Maca nie.
A bez nawiasu? serwery+=backup01 nie dokłada nowego elementu, tylko dokleja tekst do pierwszego. Wychodzi web01backup01. Serwer, którego nikt nigdy nie kupił.
Kawałek tablicy, czyli plaster, robisz tak jak przy tekście, od którego i ile:
echo "${serwery[@]:1:2}"web02 db01A wyrzucenie elementu to unset. I tu pierwsza niespodzianka:
unset 'serwery[1]'
echo "${#serwery[@]}"
echo "${!serwery[@]}"4
0 2 3 4Element zszedł, ale numeracja się nie przesunęła. Przegródka numer 1 jest teraz pusta. ${!serwery[@]}, z wykrzyknikiem, pokazuje, które numery w ogóle są zajęte. To jak bloki na osiedlu po rozbiórce: jest blok 0, 2, 3 i 4, a jedynki już nie ma. Listonosz musi o tym wiedzieć. Twoja pętla też.
Pętla po tablicy: taśma przy kasie
To jest serce lekcji. Pętla to taśma przy kasie. Tablica to zakupy w koszyku. Wykładasz wszystko na taśmę i kasjerka bierze po kolei.
for s in "${serwery[@]}"; do
echo "Sprawdzam: $s"
doneSprawdzam: web01
Sprawdzam: web02
Sprawdzam: db01
Sprawdzam: backup01
Sprawdzam: mail 01Zobacz ostatnią linijkę. mail 01 ze spacją przeszedł w całości. Bo "${serwery[@]}" w cudzysłowie ma specjalną moc: każdy element dostaje własną kopertę.
A teraz to samo bez cudzysłowu:
for s in ${serwery[@]}; do
echo "Sprawdzam: $s"
done...
Sprawdzam: backup01
Sprawdzam: mail
Sprawdzam: 01Nagle mamy serwer mail i serwer 01. Bez koperty Bash rozciął element na spacji. Kasjerka przepuściła ser osobno i dziury z sera osobno. Zasada na całe życie: "${tablica[@]}", zawsze w cudzysłowie.
A kiedy potrzebujesz numeru? Pętla po indeksach:
for i in "${!serwery[@]}"; do
echo "$i: ${serwery[i]}"
done0: web01
1: web02
2: db01
3: backup01
4: mail 01I to działa także po unset, bo idziesz po numerach, które naprawdę istnieją. Wewnątrz nawiasu kwadratowego nie musisz pisać $i, Bash liczy tam jak w (( )).
Nożyczki działają na całej szufladzie
Pamiętasz nożyczki z poprzedniej lekcji? One działają też na tablicy, na każdym elemencie naraz:
pliki=(access.log error.log syslog.log)
echo "${pliki[@]%.log}"
echo "${pliki[@]^^}"access error syslog
ACCESS.LOG ERROR.LOG SYSLOG.LOGJeden ruch, trzy pliki bez rozszerzenia. Jak krajalnica do jajek.
Jest jeszcze ${tablica[*]}, z gwiazdką. W cudzysłowie skleja wszystko w JEDEN tekst. Przydaje się do wypisania listy w jednej linijce:
echo "Padnięte: ${padniete[*]}"Zasada: @ do pętli, * do wypisywania.
Skąd brać tablice: pliki, wyniki komend, mapfile
Tablicę rzadko wpisujesz ręcznie. Zwykle bierzesz ją skądś.
Z listy plików:
logi=(logi/*.log)
echo "${#logi[@]}"
printf '%s\n' "${logi[@]}"3
logi/a.log
logi/b.log
logi/stary log.logGwiazdkę rozwija sam Bash, a każdy plik ląduje w osobnej przegródce, nawet ze spacją w nazwie.
A z pliku tekstowego? Tu wchodzi mapfile. Ma też drugą nazwę, readarray, to ta sama komenda. Mamy plik z serwerami:
web01
web02
db01
backup01mapfile -t serwery < serwery.txt
echo "Wczytano: ${#serwery[@]}"
declare -p serweryWczytano: 4
declare -a serwery=([0]="web01" [1]="web02" [2]="db01" [3]="backup01")Każda linijka pliku to jedna przegródka. Prosto jak składanie ubrań do szuflady, jeden sweter na przegródkę.
Po co -t? Bez niego każdy element ma na końcu znak nowej linii:
declare -a bez=([0]=$'web01\n' [1]=$'web02\n' ...)I potem ping szuka serwera o nazwie „web01 plus Enter”. Nie znajdzie. -t to „obetnij końcówkę”. Używaj zawsze.
A z wyniku komendy? Przez podstawienie procesu < <( ), które znałeś z lekcji 39:
mapfile -t webowe < <(grep web serwery.txt)
echo "${#webowe[@]}"2Jest też sposób na podzielenie jednej linijki na kawałki, read -a:
IFS=: read -r -a pola <<< "root:x:0:0:root:/root:/bin/bash"
echo "${pola[0]} ${pola[5]} ${pola[-1]}"root /root /bin/bashIFS=: mówi, że separatorem jest dwukropek. Linijka z /etc/passwd rozpada się na siedem przegródek. Te trzy strzałki <<< to here-string, jeszcze o nich porozmawiamy za dwa lekcje.
[macOS] mapfile i readarray weszły w Bashu 4.0. Na systemowym Bashu 3.2 z Maca ich nie ma.
Wszystko razem: ping.sh
#!/bin/bash
# ping.sh – sprawdza listę serwerów z pliku
plik=${1:-serwery.txt}
mapfile -t serwery < "$plik"
echo "Serwerów do sprawdzenia: ${#serwery[@]}"
padniete=()
for s in "${serwery[@]}"; do
if ping -c 1 -W 1 "$s" > /dev/null 2>&1; then
echo "OK $s"
else
echo "BRAK $s"
padniete+=("$s")
fi
done
echo "Nie odpowiada: ${#padniete[@]}"
if (( ${#padniete[@]} > 0 )); then
echo "Lista: ${padniete[*]}"
fi./ping.sh lista.txtSerwerów do sprawdzenia: 3
OK localhost
OK 127.0.0.1
BRAK web99.nieistnieje.invalid
Nie odpowiada: 1
Lista: web99.nieistnieje.invalidZobacz, co tu się dzieje. Pierwsza tablica przychodzi z pliku. Druga, padniete, startuje pusta, (), i rośnie w pętli. Na końcu liczymy, ile urósło. ping -c 1 wysyła jedno zapytanie, a -W 1 czeka najwyżej sekundę. [macOS] Na Macu ping ma inne opcje, tam -W liczy się w milisekundach, więc ten skrypt odpalaj na Linuksie.
Doszedł serwer? Dopisujesz linijkę do pliku. Skrypt zostaje, jak był. To jest cała idea.
Jak to działa pod spodem
Tablica w Bashu to nie jest blok pamięci z równymi przegródkami, jak w języku C. To lista par: numer i tekst. Dlatego może mieć dziury i dlatego unset niczego nie przesuwa. Bash po prostu wyrzuca jedną parę z listy.
Podpatrzysz to przez declare -p:
serwery[10]=nowy
declare -p serwerydeclare -a serwery=([0]="web01" [2]="db01" [3]="backup01" [4]="mail 01" [10]="nowy")Pięć elementów, a najwyższy numer to 10. Dlatego ${#serwery[@]} to nie to samo co „ostatni indeks plus jeden”.
I ważna rzecz: w Bashu wszystko jest tekstem. W tablicy nie ma liczb, są napisy, które wyglądają jak liczby. Bash zamienia je na liczby dopiero w (( )).
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
a=(x y z) | nawiasy | tworzy tablicę |
${a[0]} | [n] | element numer n (od zera) |
${a[-1]} | ujemny indeks | ostatni element (Bash 4.2+) |
"${a[@]}" | @ w cudzysłowie | wszystkie elementy, każdy osobno |
"${a[*]}" | * w cudzysłowie | wszystkie elementy sklejone w jeden tekst |
${#a[@]} | # | liczba elementów |
${!a[@]} | ! | lista zajętych indeksów |
${a[@]:1:2} | :od:ile | plaster tablicy |
a+=(x) | +=( ) | dokłada element na koniec |
unset 'a[1]' | usuwa element (bez przesuwania!) | |
declare -p a | -p | pokazuje tablicę z numerami |
mapfile -t a < plik | -t | wczytuje plik, linijka = element, bez znaków nowej linii |
mapfile -t a < <(cmd) | < <( ) | wczytuje wynik komendy |
read -r -a a | -a | dzieli jedną linijkę na elementy według IFS |
⚠ Typowy błąd
Wydaje się oczywiste: wynik komendy przez rurę prosto do mapfile.
cat serwery.txt | mapfile -t z_rury
echo "Z rury: ${#z_rury[@]}"Z rury: 0Zero. Żadnego błędu, tablica pusta. Dlaczego? Każdy kawałek rury działa w osobnym procesie, w podpowłoce. mapfile grzecznie wypełnił tablicę, tylko że w swoim procesie. Jak ten się skończył, tablica poszła razem z nim do kosza. To jak zostawienie zakupów w sąsiednim sklepie.
ShellCheck to wyłapie:
SC2030 (info): Modification of z_rury is local (to subshell caused by pipeline).Naprawa: zamiast rury podstawienie procesu.
mapfile -t serwery < <(cat serwery.txt)A najlepiej, gdy to plik, po prostu mapfile -t serwery < serwery.txt.
Drugi klasyk, krótko: brak [@].
serwery=(web01 web02 db01)
echo "$serwery"
echo "${#serwery}"web01
5Sama nazwa tablicy to tylko pierwsza przegródka. A ${#serwery} to nie liczba serwerów, tylko długość słowa web01. Pięć liter. Raport powie, że masz pięć serwerów, i będzie się mylił do pierwszego audytu.
✎ Zadanie
- Wczytaj
mapfile-em do tablicy nazwy wszystkich kont z/etc/passwd(pierwsza kolumna, podpowiedź:cut -d: -f1). Wypisz, ile jest kont, które jest pierwsze, a które ostatnie. - W pętli wypisz tylko konta, których nazwa zaczyna się na „s”, razem z numerem porządkowym liczonym od 1.
Rozwiązanie znajdziesz pod spodem.
Pokaż rozwiązanie
Zadanie: wczytać konta z /etc/passwd do tablicy, podać ich liczbę, pierwsze i ostatnie, a potem wypisać z numerem te na „s”.
#!/bin/bash
mapfile -t konta < <(cut -d: -f1 /etc/passwd)
echo "Kont: ${#konta[@]}"
echo "Pierwsze: ${konta[0]}"
echo "Ostatnie: ${konta[-1]}"
for i in "${!konta[@]}"; do
if [[ ${konta[i]} == s* ]]; then
echo "$((i + 1)). ${konta[i]}"
fi
doneKont: 34
Pierwsze: root
Ostatnie: eryk
4. sys
5. sync
19. systemd-network
20. systemd-timesync
23. systemd-resolve
26. syslog
33. sshd
...U mnie, na świeżym Ubuntu Server, wyszły 34 konta. U Ciebie liczba i numery mogą być inne, bo prawie każdy zainstalowany pakiet usługi dokłada własne konto. Pierwsze zawsze będzie root, bo on wszedł do budynku pierwszy i nikt go nie przesunął.
Dwie rzeczy. cut -d: -f1 bierze pierwszą kolumnę, a < <( ) wlewa wynik do mapfile bez rury, więc tablica nie ginie w podpowłoce. A numer porządkowy to i + 1, bo ludzie liczą od jedynki, nawet jeśli Bash nie. [na ekranie: „konto numer 0” przekreślone, „konto numer 1”]
Co dalej
Tablica numeruje przegródki: zero, jeden, dwa. A co, jeśli chcesz je podpisać nazwami? „web01 → 10.0.0.11”, „db01 → 10.0.0.21”. W następnej lekcji szatnia z nazwiskami, czyli słowniki. I tu użytkownicy Maca muszą już naprawdę zainstalować nowego Basha.