Uwaga:
Oryginalna wersja tego artykułu została opublikowana w IBM developerWorks i
jest własnością Westtech Information Services. Poniższy dokument jest
poprawioną przez zespół GDP wersją oryginalnego tekstu i nie jest już
aktualizowany.
|
Bash w przykładach - część druga
1.
Więcej podstaw programowania w bashu
Przyjmowanie argumentów
Zaczniemy od przedstawienia sposobu obsługi argumentów podanych skryptowi w
linii komend. Potem zajmiemy się podstawowymi sposobami kontroli przebiegu
programu.
Przykadowy skrypt z pierwszej części kursu
wykorzystywał zmienną środowiskową $1, która odpowiadała pierwszemu
argumentowi przekazanemu do skryptu w linii komend. Analogicznie można korzystać
ze zmiennych $2, $3 itd., odwołujących się kolejnych argumentów
jakie przekażemy skryptowi. Oto przykład:
Listing 1.1: Odwoływanie się do argumentów z linii komend - przykładowy skrypt |
#!/usr/bin/env bash
echo nazwa skryptu to $0
echo pierwszy argument to $1
echo drugi argument to $2
echo siedemnasty argument to $17
echo ilość argumentów to $#
|
Przykład sam wyjaśnia zastosowane w nim odwołania do zmiennych. Zwróćmy jednak
szczególną uwagę na dwa elementy. Po pierwsze, zmienna $0 odwołuje się do
nazwy skryptu jaka została użyta do jego wywołania w linii poleceń. Po drugie,
zmienna $# zawiera ilość argumentów jakie zostały przekazane w czasie
wywołania skryptu. Warto zmodyfikować kilkukrotnie powyższy skrypt i
poeksperymentować wywołując go z różnymi argumentami, aby zrozumieć jak
dokładnie działają odwołania do argumentów wywołania.
Czasami istnieje potrzeba odwołania się do wszystkich argumentów wywołania na
raz. Aby było to możliwe bash dostarcza zmienną $@, która zwróci
wszystkie argumenty wywołania rozdzielone spacjami. Przykład jej wykorzystania
znajduje się nieco dalej, przy okazji omówienia pętli for.
Kontrola przebiegu programu w bashu
W czasie programowania w językach proceduralnych jak C, Pascal, Python czy Perl,
nie jest niczym niezwykłym korzystania z konstrukcji if, pętli for itp. Bash ma
własne wersje większości z tych standardowych konstrukcji. W kolejnych
paragrafach poznamy je i odkryjemy różnice pomiędzy nimi i innymi konstrukcjami,
jakie być może znamy z innych języków programowania. Nawet jeśli nie posiadamy
doświadczenia w programowaniu, zrozumienie tych informacji nie powinno stanowić
problemu - opisy są szczegółowe i opatrzone przykładami, które pozwolą na
śledzenie teoretycznych opisów.
Konstrukcje warunkowe
Jeśli pisaliśmy programy operujące na plikach w języku C, wiemy że sprawdzenie
czy jakiś plik jest nowszy od innego wymaga tam poświęcenia sporo uwagi. Jest
tak, ponieważ C nie posiada żadnej wbudowanej składni dla przeprowadzenia takich
porównań. Zamiast tego trzeba użyć dwukrotnie funkcji stat() i porównać
wyniki ręcznie. Bash, w przeciwieństwie do C, posiada wbudowane operatory
sprawdzania właściwości plików (wszystkich właściwości -a nie tylko użytej w
przykładzie daty modyfikacji). Tak więc sprawdzenie czy plik
/tmp/myfile jest odczytywalny jest tak proste jak spawdzenie czy
wartość zmiennej $myvar jest większa niż 4.
W dalszej części artukułu pojawiają się najczęściej używane operatory porównań,
a także przykłady właściwego użycia poszczególnych opcji. Operatory porównań
znajdują się bezpośrednio po słowie kluczowyn if. Oto przykład:
Listing 1.2: Przykładowy operator porównania |
if [ -z "$myvar" ]
then
echo "zmienna myvar nie jest zdefiniowana"
fi
|
Czasami istnieje kilka możliwości wykonania tego samego porównania. Przykładowo
dwie poniższe konstrukcje if działąją identycznie:
Listing 1.3: Dwa wersje tego samego porównania |
if [ "$myvar" -eq 3 ]
then
echo "myvar równa się 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar równa się 3"
fi
|
Dwie powyższe konstrukcje if wykonują to samo działanie, jednak w pierwszym
przypadku zmienna jest traktowana jako liczba, podczas gdy w drugim zostaje
wykorzystane porównywanie łańcuchów.
Pułapki przy porównywaniu łańcuchów
Zazwyczaj można pomijać podwójne cudzysłowy obejmujące łańcuchy i zmienne
łańcuchowe, jednak nie jest to zalecana praktyka. Dlaczego? Kod będzie działał
doskonale, ale jedynie do momentu gdy w zmiennej środowiskowej pojawi się jakiś
biały znak (spacja lub znak tabulacji), bo wtedy bash nie będzie w stanie
poprawnie zinterpretować otrzymanych poleceń. Poniżej znajduje się przykład
podatny na działanie tego błędu:
Listing 1.4: Niepoprawne porównywanie łańcuchów |
if [ $myvar = "foo bar oni" ]
then
echo "tak"
fi
|
Jeśli $myvar zawiera łańcuch "foo", porównanie zadziała zgodnie z
oczekiwaniami, nie drukując na wyjściu żadnych informacji. Jednakże, jeśli
$myvar zawiera "foo bar oni", skrypt nie zadziała zwracając błąd:
Listing 1.5: Błąd gdy zmienna zawiera białe znaki |
[: too many arguments
|
W tym przypadku spacje w zmiennej środowiskowej $myvar (zawierającej
łańcuch "foo bar oni") nie pozwalają na poprawną interpretację konstrukcji if.
Gdy bash dokonuje ekspansji zmiennej $myvar, warunek konstrukcji if
zostaje rozwinięty do postaci:
Listing 1.6: Rozwinięcie warunku if z poprzedniego przykładu |
[ foo bar oni = "foo bar oni" ]
|
Ponieważ zmienna środowiskowa nie została umieszczona w podwójych cudzysłowach,
bash uznał, że po lewej stronie znaku równości umieściliśmy zbyt dużo
argumentów. Problem można łatwo wyeliminować poprzez umieszczenie zmiennej w
podwójnych cudzysłowach. Zalecane jest obejmowanie wszystkich zmiennych
łańcuchowych podwójnymi cudzysłowami, gdyż pozwala to na wyeliminowanie wielu
błędów programistycznych. Poniżej właściwy sposób porównywania łańcuchów:
Listing 1.7: Poprawny sposób porównywania łańcuchów |
if [ "$myvar" = "foo bar oni" ]
then
echo "tak"
fi
|
Powyższy kod będzie działał zgodnie z przewidywaniami i nie będzie generował
żadnych nieoczekiwanych błędów.
Uwaga:
Jeśli chcemy poddać zmienną środowiskową ekspansji, musimy umieścić ją w
podwójnych cudzysłowach. Pojedyncze cudzysłowy uniemożliwiają ekspansję
zmiennych (a także ekspansję historii basha).
|
Pętle: for
Opanowaliśmy już instrukcję if. Teraz czas na poznanie pętli basha. Zaczniemy od
standardowej pętli for. Poniżej podstawowy przykład:
Listing 1.8: Pętla for - przykładowy skrypt |
#!/usr/bin/env bash
for x in jeden dwa trzy cztery
do
echo numer $x
done
numer jeden
numer dwa
numer trzy
numer cztery
|
Co dokładnie się stało? Część "for x" odpowiada za definicję nowej zmiennej
środowiskowej (nazywanej również kontrolerem pętli) o nazwie $x, która
następnie przyjmowała wartości "jeden", "dwa", "trzy", "cztery". Po każdyn
przypisaniu nowej wartości ciało pętli (kod pomiędzy do i done)
zostaje raz wykonane. W ciele odwołujemy się do zmiennej kontrolującej pętlę,
przy użyciu standardowej składni ekspansji zmiennych. Pętla for działa zawsze na
podstawie listy słów podanych za operatorem in. W powyższym przykładzie
użyliśmy czerech słów, jednak mogą to być równie dobrze odwołania do plików lub
cokolwiek innego. Poniższy przykład ilustruje wykorzystanie w roli
przypisywanych argumentów nazw plików pasujących do podanego wzorca:
Listing 1.9: Używanie wzorców nazw w roli argumentów pętli for |
#!/usr/bin/env bash
for myfile in /etc/r*
do
if [ -d "$myfile" ]
then
echo "$myfile (dir)"
else
echo "$myfile"
fi
done
/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc
|
Powyższy kod wykonał jeden obrót pętli dla każdego pliku lub katalogu,
zaczynającego się literą "r" z katalogu /etc. Aby było to możliwe,
bash musiał w pierwszej kolejności zamienić nasz wzorzec na to, co jest efektem
jego wykonania, czyli w tym przypadku na łańcuch "/etc/rc.d
/etc/resolv.conf /etc/resolv.conf~
/etc/rpc", a następnie przejść do właściwego wykonywania pętli. W
ciele pętli wykorzystano konstrukcję if z operatorem -d, który
pozwala na rozróżnienie czy zmienna przechowuje w danym obrocie pętli nazwę
pliku czy katalogu. W drugim przypadku wyświetlona zostaje dodatkowa informacja
obok nazwy - " (dir)".
Na liście przypisywanych słów możemy stosować także wielokrotne wzorce nazw, a
nawet zmienne środowiskowe:
Listing 1.10: Wielokrotne wzorce i zmienne środowiskowe jako argumenty pętli for |
for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
cp $x /mnt/mydira
done
|
Bash zastąpi wzorce efektami ich wykonania oraz dokona ekspansji zmiennych i na
tej podstawie stworzy prawdopodobnie dość długą listę argumentów pętli for.
Wszystkie dotychczasowe przykłady wykorzystywały wzorce nazw w bezwzględnych
ścieżkach. Nic nie stoi jednak na przeszkodzie aby używać również ścieżek
względnych:
Listing 1.11: Wykorzystanie względnych ścieżek |
for x in ../* mystuff/*
do
echo $x to plik lub folder
done
|
W powyższym przykładzie bash dokona rozwinięcia wzorca w odniesieniu do
katalogu, w którym skrypt został wywołany, dokładnie jak w przypadku używania
względnych ścieżek w wierszu poleceń. Warto wypróbować różne możliwości
rozwijania wzorców. Jeśli korzystamy z bezwzględnej ścieżki we wzorcu, bash
rozwinie go do postaci z bezwzględnymi ścieżkami. W innym przypadku na liście
argumentów pętli umieszczone zostaną względne ścieżki. Jeśli natomiast
odwołujemy się do plików w katalogu wywołania skryptu (np. używając konstrukcji
for x in *), lista nie będzie zawierać żadnych informacji o ścieżce do
plików. Należy pamiętać, że istnieje możliwość skorzystania z polecenia
basename, w celu uzyskania jedynie nazwy pliku bez ścieżki do niego:
Listing 1.12: Uzyskiwanie nazwy pliku przy użyciu basename |
for x in /var/log/*
do
echo `basename $x` to plik w katalogu /var/log
done
|
Oczywiście bardzo przydatna może okazać się możliwość użycia argumentów
wywołania skryptu w roli argumentów pętli for. Poniżej znajduje się przykład
wykorzystania zmiennej $@, opisanej na początku artykułu:
Listing 1.13: Przykład użycia zmiennej $@ |
#!/usr/bin/env bash
for thing in "$@"
do
echo podana zmienna wywołania: ${thing}.
done
$ allargs hello there you silly
podana zmienna wywołania: hello.
podana zmienna wywołania: there.
podana zmienna wywołania: you.
podana zmienna wywołania: silly.
|
Arytmetyka powłoki
Zanim przyjrzymy się innemu typowi pętli, musimy poznać arytmetykę powłoki. Tak,
to prawda: można wykonywać proste operacje matematyczne przy użyciu konstrukcji
powłoki. Wystarczy objąć wyrażenie arytmetyczne znakami $(( i )),
a bash obliczy jego wartość. Oto kilka przykładów:
Listing 1.14: Arytmetyka w bashu |
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar - $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
|
Gdy znamy już podstawy arytmetyki bash, możemy przyjrzeć się dwóm innym typom
pętli - while i until.
Pętle: while i until
Pętla while będzie wykonywać swoje ciało dopóki warunek jest prawdziwy.
Przyjmuje ona następującą formę:
Listing 1.15: Schemat pętli while |
while [ warunek ]
do
operacje
done
|
Pętla while jest zazwyczaj używana do wykonania czynności określoną ilość razy.
Przykładowa pętla wykona swoje ciało dokładnie 10 razy:
Listing 1.16: Wykonanie pętli 10 razy |
myvar=0
while [ $myvar -ne 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
Przykład prezentuje wykorzystanie arytmetyki basha do doprowadzenia do sytuacji,
w której warunek będzie nieprawdziwy i pętla przerwie wykonywanie.
Pętla until stanowi przeciwieństwo pętli while - wykonuje się ona dopóki warunek
jest fałszywy. Poniższa pętla until działa tak, jak wcześniej zaprezentowany
przykład pętli while:
Listing 1.17: Przykład pętli until |
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
Konstrukcja case
Case jest kolejną przydatną konstrukcją warunkową. Oto przykład jej użycia:
Listing 1.18: Przykład użycia konstrukcji case |
case "${x##*.}" in
gz)
gzunpack ${SROOT}/${x}
;;
bz2)
bz2unpack ${SROOT}/${x}
;;
*)
echo "Format archiwum nie został rozpoznany."
exit
;;
esac
|
Bash na początku dokona ekspansji zmiennej według wzorca ${x##*.}.
Zmienna $x zawiera nazwę pliku, a po wykonaniu ekspansji
${x##.*} obcięty zostanie jej początkowy fragment do ostatniej kropki
łącznie z nią. Następnie bash porównuje otrzymany w ten sposób łańcuch z
wartościami jakie pojawiają się po lewej stronie znaków ). Wynik
ekspansji ${x##.*} zostaje porównany z łańcuchami "gz", "bz2" i "*".
Jeśli któreś z tych porównań da wynik w postaci prawdy, wykonane zostaną
instrukcje po znaku ), aż do znaków ;;, po dojściu do których bash
przejdzie do wykonywania komend po kończącym konstrukcję case esac. Jeśli
wszystkie porównania zwrócą wynik w postaci fałszu, żadne instrukcje w obrębie
case nie zostaną wykonane. W powyższym przykładzie zawsze przynajmniej jeden
blok case zostanie wykonany, ponieważ wszystkie łańcuchy, które nie będą pasować
do "gz" lub "bz2", odpowiadają wzorcowi "*".
Funkcje i przestrzenie nazw
W bashu można także definiować funkcje, podobnie jak w innych językach
proceduralnych (np.: Pascal, C). Funkcje w bashu mogą również przyjmować
argumenty, przy użyciu podobnego systemu jak w przypadku obsługi argumentów
wywołania. Przyjrzyjmy się przykładowej funkcji:
Listing 1.19: Przykładowa funkcja w bashu |
tarview() {
echo -n "Zawartość tarballa $1 "
if [ ${1##*.} = tar ]
then
echo "(uncompressed tar)"
tar tvf $1
elif [ ${1##*.} = gz ]
then
echo "(gzip-compressed tar)"
tar tzvf $1
elif [ ${1##*.} = bz2 ]
then
echo "(bzip2-compressed tar)"
cat $1 | bzip2 -d | tar tvf -
fi
}
|
Uwaga:
Powyższy kod mógłby wykorzystywać konstrukcję case zamiast if. Warto spróbować
przekształcić go do takiej formy.
|
Zdefiniowana powyżej funkcja ma nazwę "tarview" i przyjmuje jeden argument w
postaci tarballa jakiegoś typu. W momencie wywołania funkcji następuje
sprawdzenie jakiego typu tarballem jest argument (nieskompresowany,
skompresowany przy pomocy gzip lub przy pomocy bzip2) i wydrukowanie na wyjściu
informacji o tym oraz zawartości pliku. Oto w jaki sposób powyższa funkcja
powinna być wywoływana (zarówno ze skryptu, jak i z wiersza poleceń, po jej
przepisaniu lub przekopiowaniu i nadaniu praw do wykonywania):
Listing 1.20: Wywoływanie powyższej funkcji |
$ tarview shorten.tar.gz
Zawartość tarballa shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot 0 1999-02-27 16:17 shorten-2.3a/
-rw-r--r-- ajr/abbot 1143 1997-09-04 04:06 shorten-2.3a/Makefile
-rw-r--r-- ajr/abbot 1199 1996-02-04 12:24 shorten-2.3a/INSTALL
-rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE
....
|
Jak widać, można odwoływać się do argumentów wewnątrz funkcji przy użyciu tej
samej składni co w przypadku argumentów wywołaniowych. Dodatkowo makro
$# zawiera liczbę wszystkich przekazanych argumentów. Jedynym
przypadkiem, w którym działanie będzie inne od tego, czego moglibyśmy się
spodziewać, jest użycie zmiennej $0, która zwróci łańcuch "bash" (jeśli
wywołaliśmy funkcję z poziomu konsoli) lub nazwę skryptu, w którym wywołaliśmy
funkcję.
Uwaga:
Wywoływanie funkcji z konsoli: należy pamiętać, że funkcje takie jak powyższa,
mogą zostać umieszczone w ~/.bashrc lub
~/.bash_profile, dzięki czemu będzie można je wywoływać z dowolnego
miejsca.
|
Przestrzenie nazw
Często powstaje konieczność stworzenia zmiennych środowiskowych wewnątrz
funkcji. Jeśli tylko jest to możliwe należy korzystać z techniki jaka zostanie
przedstawiona w tej części artykułu. W wiekszości kompilowanych języków (np. C)
zmienne tworzone wewnątrz funkcji umieszczane są w lokalnej dla niej przestrzeni
nazw. Jeśli więc zdefiniujemy w C funkcję o nazwie myfunction i wewnątrz niej
zmienną x, gobalna zmienna (poza funkcją) o nazwie x nie zostanie z żaden sposób
zmieniona.
Jest tak w C, jednak nie jest tak w bashu. W bashu każda zmienna stworzona
wewnątrz funkcji jest dodawana do globalnej przestrzeni nazw. Oznacza to, że
nadpisze ona zmienną o takiej samej nazwie poza funkcją i będzie istniała nawet
po zakończeniu jej wywołania. Oto dowód:
Listing 1.21: Zasięg zmiennych w bashu |
#!/usr/bin/env bash
myvar="cześć"
myfunc() {
myvar="raz dwa trzy"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
Po uruchomieniu, powyższy skrypt wygeneruje w ostatniej linii wyjście "raz dwa
trzy trzy", prezentując w ten sposób jak zmienna $myvar zdefiniowana
wewnątrz funkcji zasłoniła wartość globalnej zmiennej o tej samej nazwie.
Widzimy tu również, że zmienna $x istnieje po zakończeniu wywołania
funkcji (ona również zasłoniłaby wartość globalnej zmiennej $c, gdyby
taka istniała).
W przykładzie tak prostym jak powyższy, błąd jest łatwy do wykrycia i
naprawienia poprzez wykorzystanie innych nazw zmiennych. Jednakże nie jest to
najlepsze rozwiązanie. Najlepszym rozwiązaniem tego problemu jest zapobieżenie
zasłonieniu zmiennych globalnych przy użyciu słowa kluczowego local. Gdy
definiujemy lub deklarujemy zmienną z użyciem słowa local zostaje ona
zamknięta w lokalnej przestrzeni nazw i nie nadpisze żadnych zmiennych
globalnych. Poniżej znajduje się zmodyfikowany kod funkcji:
Listing 1.22: Zapobieganie nadpisywaniu zmiennych globalnych |
#!/usr/bin/env bash
myvar="cześć"
myfunc() {
local x
local myvar="raz dwa trzy"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
W ostatniej linii powyższego przykładu na wyjście zostanie skierowany napis
"cześć" - globalna zmienna $myvar nie została nadpisana, a lokalna
zmienna $x przestaje istnieć po zakończeniu wywołania funkcji. W
pierwszej linii funkcji zadeklarowaliśmy zmienną o nazwie x do poźniejszego
użytku, w drugiej natomiast zdefiniowaliśmy zmienną myvar przypisująć jej od
razu wartość (local myvar="raz dwa trzy"). Pierwsza z lokalnych zmiennych służy
do wykonywania pętli for. Niepoprawna jest konstrukcja "for local x in $myvar",
więc zmienna x musi zostać oddzielnie zadeklarowana. Powyższa funkcja nie wpływa
na globalne zmienne i warto jest konstruować wszystkie funkcje z użyciem słowa
local. Jedynym przypadkiem kiedy nie należy stosować słowa kluczowego
local jest sytuacja, gdy świadomie chcemy modyfikować zmienne globalne.
Potrafimy coraz więcej
Znamy już esencję basha - jego najważniejsze konstrukcje i sposoby ich użycia.
Teraz nadszedł czas aby zbudować całą aplikację w bashu. Tym właśnie zajmiemy
się w ostatniej części tej serii.
2.
Zasoby informacji
Przydatne linki
|