case
jest instrukcją wyboru, która jest krótszą alternatywą dla instrukcji if
, gdy testujemy jedną wartość. Jej składnia to:
case wartość in wzorzec1) instrukcje1 ;; wzorzec2) instrukcje2 ;; ... esac
Wzorce mają formę podobną do wzorców plików, tzn. może to być konkretna wartość, a może też zawierać znaki * i ?. case dopasowuje wartość do jednego z wzorców. Dla pierwszego dopasowanego wzorca wykonywane są odpowiadające mu instrukcje. Jeśli nie uda się dopasować do żadnego z wzorców, to nie są wykonywane żadne instrukcje.
#!/bin/sh case $1 in "") echo "Prawidłowe wywołanie to: $0 plik" exit 1 ;; *.txt) # jeśli plik tekstowy, to go uruchamiamy edytor pico $1 ;; *.sh) # jeśli skrypt to go uruchamiamy ./$1 ;; *) # to oznacza wszystkie wartości; dalsze wzorce nie mają już sensu echo "Nieznany rodzaj pliku '$1'" ;; esac
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
Pętla until jest bardzo podobna do pętli while:
until warunek; do instrukcje done
Różnica polega na tym, że 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
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 z języka C.
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 używać wzorców nazw plików do podania wielu nazw plików naraz. Instrukcje w bloku są wykonywane dla każdej wartości znajdującej 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ładającą się z elementu 0 oraz z wszystkich argumentów skryptu.
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 modyfikowanie 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.
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
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 }
Zadeklarowana funkcja dostępna jest dla potrzeb skryptu jak nowe polecenie. Wywołujemy ją używając jej nazwy. Możemy przekazywać argumenty w ten sam sposób, w jaki przekazujemy je 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 "$@" wypisz_argumenty raz dwa trzy cztery pięć
Deklaracja funkcji jest instrukcją, w wyniku której 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
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
Uruchamianie skryptów wiąże się z tworzeniem nowego procesu. Wszystko, co dostaje skrypt, to ś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 (chyba, że je wyeksportowaliśmy do środowiska). Po powrocie z wywołania innego skryptu bądź programu mamy pewność, że zadeklarowane zmienne nie zostały zmienione, podobnie jak argumenty skryptu. Wiemy też, że katalog bieżący jest niezmieniony.
Tego typu własności mogą być pożą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ń lub 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. 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 polecenie 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:~$
W jaki sposób dobrać się do zmiennej, której nazwa znajduje się na innej zmiennej. Otóż możemy użyć do tego polecenia eval
:
eval argumenty
Działa ono tak, że tworzona jest instrukcja składająca się z podanych argumentów. Niby nic w tym specjalnego, ale jest to w pewien sposób użyteczne. Na przykład, gdy w argumentach znajduje się wywołanie do zmiennej (np. $zmienna
), wtedy wpierw wstawiana jest wartość zmiennej, potem tworzona jest instrukcja i dopiero na końcu jest wykonywana. Pozwala nam to tworzyć polecenia, w których skład wchodzą wartości zmiennych.
bashtest@host:~$ a=wartość bashtest@host:~$ echo $a wartość bashtest@host:~$ b=a bashtest@host:~$ echo $b a bashtest@host:~$ eval echo \$$b wartość bashtest@host:~$
Taki sposób przechowywania informacji o zmiennej (tzn. pamiętanie tylko nazwy zmiennej) nazywamy referencjami. W ten sposób możemy przekazywać zmienne funkcjom.
dopisz_y () { for z in "$@"; do eval $z=\$\{$z\}y done } a=dom b=kąt dopisz_y a b echo "a=$a" # wypisze a=domy echo "b=$b" # wypisze b=kąty
Widzimy, że tutaj odwołanie do zmiennej wymaga nawet użycia formy ${...}. Zauważmy, że gdybyśmy mieli w czwartej linii taką instrukcję
eval $z=\$${z}y
to nie byłoby to poprawne. Otóż dla z=a wartością $z jaki i ${z} było by a, więc polecenie utworzone przez eval do wykonania brzmiałoby a=$ay.
W systemie Linux dostępne są tak zwane pliki specjalne. Na przykład /dev/null
. Jest to taki plik, który zawsze, jak z niego czytamy, jest plikiem pustym, a wszystko, co do niego wpiszemy, znika. Ten plik jest przydatny gdy chcemy, aby wyjście polecenia nie zostało nigdzie wyświetlone.
polecenie 2>/dev/null >/dev/null
Ignorowanie wyjścia jest przydatne, gdy interesuje nas tylko status wyjścia.
Innym plikiem specjalnym jest /dev/zero
. Wszystko co się wpisze do tego pliku, również znika, ale gdy czytamy z tego pliku otrzymujemy zawsze bajty, których wartość binarna wynosi 0. Ten plik można wykorzystać do utworzenia pliku o określonej wielkości. Na przykład, aby utworzyć plik 1m.dat
składający się dokładnie z jednego megabajta znaków możemy użyć polecenia
dd if=/dev/zero of=1m.dat bs=1M count=1
Polecenie dd
służy do kopiowania z jednego pliku do drugiego określonej liczby bajtów.
Inne pliki specjalne znajdują się w katalogu /proc
. W katalogu tym są najróżniejsze informacje o systemie, o procesach. Podamy tu dla przykładu dwa pliki: /proc/cpuinfo, /proc/meminfo
. Pierwszy wyświetla informacje o procesorach znajdujących się w danym komputerze, a drugi wyświetla informacje o dostępnej pamięci operacyjnej.