Środowisko programisty/Bash - skrypty złożone
Instrukcja wyboru
Pętle
while
Pętla while ma składnię
while warunek; do instrukcje done
Interpreter tak długo wykonuje instrukcje jak długo jest spełniony warunek. Podobnie jak przy instrukcji if, warunek jest poleceniem, które jest uruchamiane przy każdym obrocie pętli. Jeśli status wyjścia jest równy zero, to wykonywane są instrukcje podane w bloku pętli.
Przykład.
zm="" while [ "$zm" != koniec ]; do echo -n "Wpisz coś (słowo 'koniec' aby zakończyć): " read zm echo "Wpisałeś '$zm'" done
until
Pętla until jest bardzo podobna do pętli while:
until warunek; do instrukcje done
Różnica polega na tym, ze pętla jest wykonywana tak długo, jak warunek jest nieprawdziwy (przeciwnie do tego jak ma się to w pętli while). Na przykład pętla z poprzedniego przykładu mogła by wyglądać tak:
until [ "$zm" == koniec ]; do ... done
for
Pętla for ma dwie formy. Pierwsza forma służy wykonywania bloku instrukcji dla każdej wartości argumentów z listy, a druga forma ma bardziej złożoną składnię i jest zapożyczona języka C.
for dla list
Ta wersja ma postać:
for zm in lista; do instrukcje done
gdzie lista jest listą wartości. Listę podajemy w analogiczny sposób jak argumenty poleceniu, czyli na przykład możemy uzywać wzorce nazw plików do podania wielu nazw plików naraz. Instrukcję w bloku są wykonywane dla każdej wartości znajdujące się na liście. W danym obrocie wartość z listy przypisywana jest na zmienną zm.
Przykład:
for f in *; do # * rozwija się do listy wszystkich plików/katalogów znajdujących się w bieżącym katalogu if [ -d "$f" ]; then echo "Katalog '$f'" elif [ -f "$f" ]; then echo "Plik '$f'" else echo "Inny typ '$f'" fi done
Listą może być też ciąg wartości. Na przykład, aby wyświetlić kwadraty wybranych liczb możemy te liczby umieścić na liście:
for i in 1 5 100 99; do echo "Kwadrat $i = $((i * i))" done
Listą może też być wynik innego polecenia:
# Wyszukanie wszystkich tych plików o rozszerzeniu txt, # dla których ostatnia linia zawiera napis "Autor: Jan Kowalski" for f in `find . -name "*.txt" -type f`; do if [ "`tail -1 $f`" == "Autor: Jan Kowalski" ]; then echo "Plik '$f' posiada już podpis" fi done
Można także w liście umieścić argumenty skryptu:
# Wypisywanie argumentów skryptu n=1 for arg in "$@"; do echo "Argument $n: '$arg'" let "n++" done
Listę można utworzyć przez połączenie dwóch innych list, na przykład 0 "$@" jest listą skłądającą się z elementu 0 oraz z wszystkich argumentów skryptu.
for w stylu C
Bardziej skomplikowana wersja pętli for w stylu C ma postać:
for ((inicjacja; warunek; post_modyfikacja)); do instrukcje done
inicjacja, warunek i post_modyfikacja są wyrażeniami takimi jak wyrażenia używane w konstrukcjach $(( ... )) i (( ... )). Działanie jest następujące.
Na początku i tylko raz, jest uruchamiane wyrażenie inicjacja. To wyrażenie zazwyczaj ma za zadanie zainicjowanie zmiennych używanych do iterowania pętli.
Następnie przed każdą iteracją wyliczane jest wyrażenie warunek. Jeśli jest ono fałszywe wykonywanie pętli kończy się. Jeśli jest ono prawdziwe wykonywany jest blok instrukcje.
Po każdej iteracji wykonywane jest wyrażenie post_modyfikacja. To wyrażenie zazwyczaj ma za zadanie modyfikacje zmiennych.
Przykład. Kilka sposobów wypisania liczb od 1 do 10:
i=1 while ((i <= 10)); do echo -n "$i " let "i++" done echo for i in 1 2 3 4 5 6 7 8 9 10; do echo -n "$i " done echo for i in `seq 1 10`; do echo -n "$i " done echo for ((i = 1; i <= 10; i++)); do echo -n "$i " done echo
Polecenie seq służy do generowanie ciągów arytmetycznych.
break i continue
Wewnątrz pętli dostępne są dwa dodatkowe polecenia:
- break,
- continue.
break powoduje natychmiastowe przerwanie wykonywanej pętli. continue powoduje zakończenie aktualnej iteracji pętli i przejście do następnej iteracji.
while true; do read a if [ "$a" = "koniec" ]; then break elif [ "$a" = "dalej" ]; then continue fi echo "Wpisałeś '$a'" done
Funkcje
Wewnątrz skryptu można pisać własne funkcje, które spełniają rolę podprogramu. Ma to miejsce na przykład wtedy, gdy pewną czynność chcemy wykonać wielokrotnie w różnych miejscach skryptu i chcemy uniknąć kopiowania kodu. Funkcję deklarujemy w skrypcie w następujący sposób:
nazwa_funkcji () { instrukcje }
Wywoływanie funkcji i argumenty
Zadeklarowaną funkcja dostępna jest dla potrzeb funkcji jak nowe polecenie. Wywyołujemy ją używając jej nazwy. Możemy przekazywać argumenty w ten sam sposób w jaki przekazujemy ja poleceniom.
# Przykład pokazujący deklarację i wywołanie funkcji z parametrami wypisz_argumenty () { echo -n "Jest $# argumentów:" for i in "$@"; do echo -n " '$i'" done echo } wypisz_argumenty wypisz_argumenty raz dwa trzy cztery pięć
Zasięg deklaracji
Deklaracja funkcji jest instrukcją, w wyniku dostępne staje się nowe polecenie. Czyli na przykład można deklarować funkcję wewnątrz bloków, a nie można używać funkcji, których deklaracja następuje później.
f # błąd - funkcja nie jest zadeklarowana if [ "$USER" = bashtest ]; then f () { echo "Pierwsza wersja f" } else f () { echo "Druga wersja f" } fi f # funkcja f może być zadeklarowana na dwa sposoby, zależnie od tego jaki użytkownik uruchomił skrypt
Status wyjścia
Ponieważ funkcja zachowuje się jak polecenie, to może też zwracać status wyjścia. Domyślnie zwracane jest zero. Aby wyjść z funkcji z zadanym statusem służy polecenie return.
pytanie_tak_nie () { while true; do if [ $# -ge 1 ]; then echo -n "$1 (tak/nie)? " fi read odp if [ "$odp" = tak ]; then return 0 elif [ "$odp" = nie ]; then return 1 fi done } if pytanie_tak_nie "Czy chcesz usłyszeć pytanie"; then until pytanie_tak_nie "Czy chcesz zakończyć ten skrypt"; do : # : jest wbudowaną pustą instrukcją done fi
Podshelle
Uruchamianie skryptów wiąże się z tworzeniem nowego procesu. Wszystko co dostaje skrypt dostaje środowisko i argumenty. Tworząc nowy proces możemy mu przekierować wejście i wyjście. Ponadto jeśli uruchamiamy inny skrypt, to nie widzi on zmiennych, które zadeklarowaliśmy (no chyba, że je wyeksportowaliśmy do środowiska). Po powrocie ze wywołania innego skryptu bądź programu, mamy pewność, że zadeklarowane zmienne nie zostały zmienione, czyli argumenty skryptu również. Wiemy też, że katalog bieżący jest niezmieniony.
Tego typu własności mogą być porządane gdy chcemy wykonać ciąg poleceń w skrypcie. Istnieje możliwość zrobienia tego bez pisania pliku z nowym skryptem. Można to zrobić w aktualnym skrypcie używając konstrukcji z nawiasami:
( instrukcje )
Instrukcje między nawiasami są uruchomione w nowym shellu. Dziedziczy po naszym skrypcie wszystkie zmienne:
bashtest@host:~$ a=2; ( echo $a; a=3; echo $a ); echo $a 2 3 2 bashtest@host:~$ ( cd /tmp ); echo $PWD /home/bashtest bashtest@host:~$
Konstrukcja ( ... ) przydaje się też do grupowania poleceń po to, aby używać przekierowań, czy też potoku do całej grupy instrukcji.
( if [ ! -d "$1" ]; then echo "$1 nie jest katalogiem exit 1 # wychodzi tylko z podshella fi cd "$1" for i in *.txt; do if [ -f "$i" ]; then echo "==== Do zrobienia w pliku '$i' ====" grep TODO "$i" fi done ) | less
Powyższy kawałek skryptu wyszuka w katalogu podanym w pierwszym argumencie wszystkie pliki o rozszerzeniu txt i dla każdego z nich wyświetli główkę oraz wszystkie linie zawierające tekst TODO. Wynik będzie można przejrzeć programem less.
Uruchamianie nowych procesów nie jest zbyt szybkie i jeśli zależy nam na szybkości należy unikać tworzenia nowych procesów. Na przykład
cat plik | grep wzorzec
można zastąpić poleceniem
grep wzorzec plik
które uruchamia tylko jeden proces, a nie dwa jak w poprzednim wywołaniu (dodatkowy proces to polcenie cat).
Warto wiedzieć, że wiele poleceń jest wbudowanych w Basha i nie są dla nich uruchamiane nowe procesy. Takie polecenia to na przykład: cd ,echo, test. Jeśli chcemy zgrupować polecenia, to zamiast używać konstrukcji ( ... ) można używać { ... }. Różnica jest taka, że nie jest uruchamiany nowy shell, a co się z tym wiąże wszelkie modyfikacje dokonane wewnątrz tego bloku są widoczne po wyjściu z tego bloku.
bashtest@host:~$ a=2; { echo $a; a=3; echo $a; }; echo $a; 2 3 3 bashtest@host:~$