Home Dokumentacje Bash w przykładach - część druga
14 | 12 | 2019
Bash w przykładach - część druga
Wpisany przez Daniel Robbins   

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

Daniel Robbins Autor
Waldemar Korłub Tłumacz

Zaktualizowano 9 października 2005

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

Wyjście skryptu:
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

Wyjście skryptu:

/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

Wyjście skryptu:

$ 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

 

 
Linki sponsorowane

W celu realizacji usług i funkcji na witrynach internetowych ZUI "ELPRO" stosujemy pliki cookies. Korzystanie z witryny bez zmiany ustawień dotyczących plików cookies oznacza, że będą one zapisywane w urządzeniu wyświetlającym stronę internetową. Więcej szczegółów w Polityce plików cookies.

Akceptuję pliki cookies z tej witryny.