Pętle
Po tej lekcji: kursant każe skryptowi powtarzać robotę pętlami for, while i until, steruje nimi przez break i continue i bezpiecznie czyta plik linijka po linijce przez while IFS= read -r.
Wstęp
folder z 500 plikami zdjecie_001.JPG … zdjecie_500.JPG
Szef przychodzi z pendrive’em: „Tu jest pięćset zdjęć z imprezy firmowej. Trzeba każde przerzucić do folderu i zmienić końcówkę na małe litery, bo strona ich nie widzi”.
Możesz to zrobić pięćset razy ręcznie. Po trzydziestym zaczniesz się mylić, po setnym zaczniesz kwestionować swoje wybory życiowe.
Albo napiszesz trzy linijki, które powiedzą komputerowi: „dla każdego pliku zrób to samo”. Komputer się nie nudzi, nie myli i nie idzie na kawę. Dziś pętle, czyli najważniejsza rzecz, jaka odróżnia skrypt od listy komend.
for, czyli taśma przy kasie
Wyobraź sobie taśmę przy kasie w Biedronce. Wykładasz zakupy, a kasjerka bierze po jednej rzeczy i robi z każdą to samo: skanuje, odkłada. Mleko, skan. Chleb, skan. Bułki, skan, skan, skan. Kiedy taśma jest pusta, koniec.
Pętla for robi dokładnie to. Dostaje listę, bierze po jednym elemencie, wkłada go do pudełka ze zmienną i wykonuje te same komendy.
for owoc in jabłko gruszka "czerwona porzeczka"; do
echo "Myję: $owoc"
doneMyję: jabłko
Myję: gruszka
Myję: czerwona porzeczkafor owoc in ... to: „po kolei wkładaj do pudełka owoc każdą rzecz z listy”. Między do i done jest to, co kasjerka robi z każdą rzeczą. Zauważ, że „czerwona porzeczka” w kopercie przeszła jako jedna rzecz. Bez cudzysłowu dostałbyś osobno „czerwona” i osobno „porzeczka”. Smacznego.
Najważniejsze zastosowanie dla admina: pętla po plikach.
for plik in *.log; do
echo "Znalazłem: $plik"
doneGwiazdka rozwija się w listę plików, a pętla jedzie po każdym. Tak właśnie załatwisz te pięćset zdjęć z haka.
Zakresy i liczenie
Często nie masz listy, tylko chcesz coś zrobić pięć razy. Pamiętasz nawiasy klamrowe z lekcji o wzorcach?
for i in {1..5}; do echo -n "$i "; done; echo
echo {01..10}
echo {0..20..5}
echo {5..1}1 2 3 4 5
01 02 03 04 05 06 07 08 09 10
0 5 10 15 20
5 4 3 2 1{1..5} to od jednego do pięciu. {01..10} dokłada zera z przodu, żeby pliki kopia_01 i kopia_10 ładnie się sortowały. {0..20..5} liczy co pięć. {5..1} liczy w tył, jak przed startem rakiety.
Jest też trzecia postać, znana programistom z języka C:
for ((i = 1; i <= 3; i++)); do
echo "Okrążenie $i"
doneOkrążenie 1
Okrążenie 2
Okrążenie 3W podwójnych nawiasach są trzy kawałki oddzielone średnikami. Start: i = 1. Warunek: kręć, dopóki i <= 3. Krok: po każdym okrążeniu i++, czyli dodaj jeden. Jak bieganie na stadionie: zaczynasz na linii, biegasz, dopóki trener nie powie stop, i po każdym okrążeniu ktoś dopisuje kreskę na tablicy. Więcej o tych podwójnych nawiasach w następnej lekcji, bo to jest kalkulator Basha.
while, czyli „dopóki”
for jest dobry, kiedy wiesz, ile tego jest. A co, jeśli nie wiesz? Czekasz, aż ktoś skończy, aż plik się pojawi, aż serwer wstanie?
Wyobraź sobie, że stoisz pod drzwiami łazienki rano, w mieszkaniu z czterema współlokatorami. Co chwilę pukasz: zajęte? Zajęte. Czekasz. Pukasz. Zajęte. Dopóki zajęte, czekasz. To jest while.
#!/bin/bash
# czekaj.sh — czeka, aż pojawi się plik /tmp/gotowe
while [ ! -f /tmp/gotowe ]; do
echo "Jeszcze nie ma, czekam..."
sleep 1
done
echo "Jest! Jedziemy dalej."w drugim oknie tmux po trzech sekundach wpisuję touch /tmp/gotowe
Jeszcze nie ma, czekam...
Jeszcze nie ma, czekam...
Jeszcze nie ma, czekam...
Jest! Jedziemy dalej.while przed każdym okrążeniem sprawdza warunek. Warunek to ten sam bramkarz [ ] co w if. Dopóki mówi „prawda”, czyli kciuk w górę, pętla się kręci. Kiedy powie „fałsz”, wychodzimy.
I ważne: sleep 1. Bez niego skrypt pyta tysiące razy na sekundę i grzeje procesor. Jak współlokator, który nie puka, tylko wali w drzwi bez przerwy. Formalnie to też czekanie, ale nikt go potem nie lubi.
Czasem chcesz kręcić się w nieskończoność, na przykład w skrypcie, który co minutę coś sprawdza:
while true; do
date
sleep 60
donetrue to komenda, która zawsze kończy się kciukiem w górę, więc ta pętla nie skończy się sama. Zatrzymasz ją Ctrl+C albo break w środku.
until, czyli „aż”
until to while na odwrót. Kręci się, dopóki warunek jest fałszywy, czyli „aż się uda”. Czyta się to naturalniej, kiedy próbujesz czegoś do skutku. Jak dzwonienie na infolinię: dzwoń, aż ktoś odbierze.
#!/bin/bash
# until.sh — próbuje, aż się uda
until mkdir /tmp/blokada 2>/dev/null; do
echo "Ktoś inny pracuje, czekam..."
sleep 1
done
echo "Mam blokadę, robię swoje"
rmdir /tmp/blokadaKtoś inny pracuje, czekam...
Ktoś inny pracuje, czekam...
Ktoś inny pracuje, czekam...
Mam blokadę, robię swojeTu warunkiem nie jest bramkarz [ ], tylko zwykła komenda. mkdir uda się tylko wtedy, gdy folderu jeszcze nie ma. Jeśli ktoś inny już go założył, dostajesz kciuk w dół i czekasz. To jest stara sztuczka adminów: folder jako kartka „zajęte” na drzwiach. Porządniejszą wersję, flock, poznamy w module o pisaniu solidnych skryptów.
W while i until po słowie kluczowym może stać dowolna komenda. Pętla patrzy tylko na jej kod wyjścia. Kciuk w górę albo w dół, nic więcej jej nie obchodzi.
break i continue: hamulec i „następny proszę”
Kasjerka może zrobić jeszcze dwie rzeczy. Może odsunąć jakiś produkt i powiedzieć „to nie nasze, następny”. To jest continue: pomiń resztę tego okrążenia i weź następny element. I może zamknąć kasę w połowie, bo ogłoszono ewakuację. To jest break: wyjdź z pętli natychmiast.
#!/bin/bash
# wdrozenie.sh — udaje aktualizację kilku serwerów
for serwer in web1 web2 baza1 web3 web4; do
if [ -f /tmp/STOP ]; then
echo "Znalazłem /tmp/STOP, przerywam wdrożenie"
break
fi
if [ "$serwer" = "baza1" ]; then
echo "Pomijam $serwer, bazy nie ruszamy w piątek"
continue
fi
echo "Aktualizuję $serwer"
sleep 1
done
echo "Koniec pętli"w trakcie działania w drugim oknie touch /tmp/STOP
Aktualizuję web1
Aktualizuję web2
Pomijam baza1, bazy nie ruszamy w piątek
Aktualizuję web3
Znalazłem /tmp/STOP, przerywam wdrożenie
Koniec pętliTen plik /tmp/STOP to prawdziwy wzorzec z życia. Skrypt, który robi coś długo i niebezpiecznie, sprawdza przed każdym krokiem, czy ktoś nie wcisnął czerwonego guzika. A czerwony guzik to pusty plik. Tani, prosty i działa nawet wtedy, gdy nie masz dostępu do terminala, w którym skrypt chodzi.
I zauważ: break wyskakuje tylko z pętli. echo "Koniec pętli" wykonało się normalnie. To nie jest exit, który kończy cały skrypt.
while IFS= read -r, czyli czytanie pliku linijka po linijce
To jest najważniejszy wzór w tej lekcji. Admin non stop dostaje listy: serwerów, użytkowników, adresów. I musi coś zrobić z każdą linijką.
Mamy plik uzytkownicy.txt od działu kadr:
# login;imię i nazwisko
anowak;Anna Nowak
jkowalski;Jan Kowalski
pwisniewska;Paula Wiśniewska#!/bin/bash
# konta.sh — pokazuje, jakie konta by założył (na sucho)
while IFS=';' read -r login nazwa; do
case "$login" in
""|\#*) continue ;;
esac
echo "useradd -m -c \"$nazwa\" $login"
done < uzytkownicy.txtuseradd -m -c "Anna Nowak" anowak
useradd -m -c "Jan Kowalski" jkowalski
useradd -m -c "Paula Wiśniewska" pwisniewskaRozbieramy od końca, bo tam jest najważniejsze. done < uzytkownicy.txt podpina plik pod wejście całej pętli. To przekierowanie, które już znasz, tylko na końcu done.
read czyta jedną linijkę i wkłada ją do zmiennych. Kiedy plik się skończy, read daje kciuk w dół i while kończy pętlę. Elegancko, bez liczenia linijek.
IFS=';' mówi, czym są oddzielone kolumny. Pierwsza kolumna ląduje w login, reszta w nazwa. To jak cut -d';', tylko od razu do zmiennych.
-r mówi: nie ruszaj ukośników. Bez niego read traktuje \ jako znak specjalny i po cichu go zjada.
A case z poprzedniej lekcji pomija puste linijki i komentarze z #. Ukośnik przed # jest po to, żeby Bash nie pomyślał, że tu zaczyna się komentarz w samym skrypcie.
I na koniec, uwaga, bo to ważne: skrypt tylko wypisuje komendy, nie zakłada kont. To się nazywa „na sucho”. Zawsze, kiedy pętla ma zrobić coś nieodwracalnego na wielu rzeczach naraz, najpierw uruchom ją z echo przed komendą i przeczytaj, co by zrobiła. Pętla myli się tak samo szybko, jak pracuje. Jeśli chcesz się pobawić naprawdę, to tylko na maszynie wirtualnej.
Dlaczego IFS= w wersji bez kolumn? Porównaj na pliku z wcięciem i ukośnikiem:
while read linia; do echo "[$linia]"; done < dziwne.txt
while IFS= read -r linia; do echo "[$linia]"; done < dziwne.txt[wciętetz ukośnikiem]
[ wcięte\tz ukośnikiem]Pierwsza wersja zjadła spacje z przodu i ukośnik. Druga oddała linijkę co do znaku. Dlatego zapamiętaj całą formułkę, nie kawałki: while IFS= read -r linia. Jak „Dzień dobry, poproszę” w urzędzie. Niby można bez, ale efekty są gorsze.
Jak to działa pod spodem
for, while, until, do, done, break, continue — to wszystko urządza sam Bash, bez uruchamiania osobnych programów. Urzędnik czyta całą pętlę od for do done, zanim wykona cokolwiek. Dlatego błąd składni na końcu pętli zatrzymuje też jej początek.
W for lista jest gotowa, zanim ruszy pierwsze okrążenie. Bash najpierw rozwija gwiazdki i nawiasy, robi z tego listę słów, a dopiero potem jedzie. Jeśli w trakcie pętli ktoś doda nowy plik .log, pętla go nie zobaczy. Kasjerka nie skanuje rzeczy, które ktoś położył na taśmie po tym, jak powiedziała „to wszystko?”.
W while warunek jest sprawdzany na nowo przed każdym okrążeniem. To komenda jak każda inna, uruchamiana od nowa za każdym razem.
A while read ... done < plik? Bash otwiera plik raz, przed pętlą, i podpina go pod wejście numer zero całej pętli. Każde read czyta z niego kolejny kawałek, aż do znaku nowej linii. Plik nie jest wczytywany do pamięci w całości, więc tym samym wzorem przeczytasz i dziesięć linijek, i dziesięć milionów.
Ściąga flag
| Komenda | Flaga / element | Co robi |
|---|---|---|
for | for x in a b c | po kolei każdy element listy |
for | for f in *.log | po kolei każdy pasujący plik |
for | {1..10} / {01..10} / {0..20..5} | zakres / z zerami / co 5 |
for | for ((i=0; i<10; i++)) | licznik w stylu C |
while | while WARUNEK | kręć, dopóki prawda |
while | while true | kręć w nieskończoność |
until | until WARUNEK | kręć, aż będzie prawda |
break | break / break 2 | wyjdź z pętli / z dwóch pętli |
continue | continue | pomiń resztę okrążenia |
read | -r | nie zjadaj ukośników |
read | IFS= | nie obcinaj spacji z brzegów |
read | IFS=';' | podziel linijkę na kolumny po ; |
shopt | -s nullglob | wzorzec bez trafień znika z listy |
done | done < plik | podepnij plik pod wejście pętli |
⚠ Typowy błąd
Chcesz zrobić tyle okrążeń, ile jest w zmiennej:
n=3
for i in {1..$n}; do
echo "$i"
done{1..3}Jedna linijka z dziwnym napisem zamiast trzech liczb. Bez żadnego błędu. Dlaczego? Bo Bash rozwija nawiasy klamrowe jako pierwsze, zanim podstawi zmienne. W chwili, gdy patrzy na nawiasy, widzi {1..$n}, a to nie wygląda na zakres, więc zostawia to w spokoju. Potem podstawia $n i wychodzi {1..3}, ale na nawiasy jest już za późno. Jak kucharz, który najpierw piecze ciasto, a dopiero potem czyta, ile ma być jajek.
Naprawa: pętla w stylu C, która rozumie zmienne.
n=3
for ((i = 1; i <= n; i++)); do
echo "$i"
doneDrugi klasyk: plik, który nie ma znaku nowej linii na końcu, bo ktoś go zapisał w edytorze z Windowsa albo wygenerował programem. W pliku są trzy linijki: „pierwsza”, „druga”, „trzecia”, a pętla wypisuje:
Mam: pierwsza
Mam: drugaTrzecia znikła. read ją przeczytał, ale skoro nie trafił na koniec linii, tylko na koniec pliku, zwrócił kciuk w dół i while zakończył pętlę bez tej linijki. Naprawa:
while IFS= read -r linia || [ -n "$linia" ]; do
echo "Mam: $linia"
done < bez_konca.txt„Kręć, jeśli read się udał, albo jeśli coś jednak przeczytał”. Brzydkie, ale ratuje ostatnią linijkę, która zwykle jest tą najważniejszą, bo Murphy.
✎ Zadanie
- Napisz skrypt
porzadki.sh, który dostaje folder jako argument i dla każdego pliku.logw tym folderze wypisuje nazwę i liczbę linijek (wc -l < plik). Puste pliki ma pomijać przezcontinue. Jeśli w trakcie pracy pojawi się plik/tmp/STOP, ma przerwać przezbreak. Bez argumentu: instrukcja i kod 1. - Zrób plik
serwery.txtz kilkoma nazwami serwerów, pustą linijką i komentarzem zaczynającym się od#. Napisz pętlęwhile IFS= read -r, która dla każdego serwera wypisze „sprawdzam: NAZWA”, a puste linijki i komentarze pominie. - Bonus: odliczanie od 5 do 1 co sekundę pętlą w stylu C, a na końcu „Start!”.
Pokaż rozwiązanie
Część pierwsza:
#!/bin/bash
# porzadki.sh — liczy linijki w plikach .log z podanego folderu
if [ $# -eq 0 ]; then
echo "Użycie: $0 FOLDER"
exit 1
fi
for plik in "$1"/*.log; do
if [ -f /tmp/STOP ]; then
echo "Jest /tmp/STOP, przerywam"
break
fi
if [ ! -s "$plik" ]; then
continue
fi
echo -n "$plik: "
wc -l < "$plik"
donelogi/app.log: 3
logi/web.log: 1W folderze były trzy pliki, ale pusty.log został pominięty. -s znaczy „istnieje i nie jest pusty”, a wykrzyknik odwraca odpowiedź. Bonus za darmo: jeśli folder nie ma żadnego .log, wzorzec zostaje dosłownie logi/*.log, a -s mówi, że takiego pliku nie ma, więc pętla grzecznie go pomija. Przypadkiem napisałeś kuloodporny kod. Zdarza się najlepszym.
Część druga:
#!/bin/bash
# serwery.sh — czyta listę serwerów, pomija puste linijki i komentarze
while IFS= read -r serwer; do
case "$serwer" in
""|\#*) continue ;;
esac
echo "sprawdzam: $serwer"
done < serwery.txtsprawdzam: web1.firma.pl
sprawdzam: baza1.firma.pl
sprawdzam: test1.firma.plI bonus:
#!/bin/bash
for ((i = 5; i >= 1; i--)); do
echo "$i..."
sleep 1
done
echo "Start!"Rakieta poszła. Jeśli Twoja odliczała w górę, to nie rakieta, tylko winda.
Co dalej
W pętli w stylu C pojawiło się i++ w podwójnych nawiasach. W następnej lekcji sprawdzimy, ile Bash umie policzyć: dodawanie, dzielenie, reszta z dzielenia i losowanie liczb. I dlaczego 7 / 2 to w Bashu 3, a matematycy płaczą po raz drugi w tym kursie.