Skrypty powłoki Bash
Kontekst wykonywania skryptów
Pod koniec poprzednich ćwiczeń dowiedzieliśmy się, jak wykonać polecenia zapisane w pliku tekstowym tak, jakbyśmy wpisywali je ręcznie w bieżącej sesji powłoki. Przypomnijmy, że należy w tym celu wykonać polecenie
$ . skrypt
gdzie skrypt
jest ścieżką do pliku zawierającego instrukcje, po jednej w wierszu. Taki plik nazywamy skryptem powłoki. Przyjęło się, że nazwy plików będących skryptami powłoki kończą się znakami .sh
(ang. shell), np. skrypt.sh
i tej notacji będziemy się odtąd trzymać.
Czasem zachodzi potrzeba wykonania poleceń w osobnej instancji powłoki (np. wtedy, gdy skrypt mógłby zmienić wartości zmiennych lokalnych i środowiskowych) i zwrócenia wyjścia do bieżącej instancji. Aby wykonać w ten sposób skrypt skrypt.sh
, należałoby wykonać polecenie:
$ bash skrypt.sh
czyli wywołać program powłoki (bash
) z argumentem będącym nazwą skryptu.
Kiedy wykonywane jest polecenie
$ bash skrypt.sh
bieżąca powłoka uruchamia program bash
, przekazuje do niego wartości zmiennych środowiskowych i pośredniczy w obsłudze wejścia/wyjścia realizowanej przez polecenia wywoływane wewnątrz skryptu. W ten sposób, nawet jeśli wewnątrz skryptu znajdują się polecenia wpływające na środowisko, zakres ich działania ograniczy się do instancji, w której wykonywany jest skrypt i programów uruchamianych z jej poziomu.
Zmienna $$
przechowuje identyfikator procesu bieżącej instancji powłoki. Utwórz skrypt o nazwie pid.sh
i treści
echo "PID: $$"
Następnie wykonaj polecenia:
$ . pid.sh
$ bash pid.sh
$ . pid.sh
$ bash pid.sh
Jakie można stąd wyciągnąć wnioski?
Utwórz w swoim katalogu domowym plik zmienne.sh
o treści
echo "a: $a"
echo "b: $b"
a=10 b=20
Następnie wykonaj polecenia:
$ a=2 b=4
$ export a
$ bash zmienne.sh
$ echo $a $b
$ . zmienne.sh
$ echo $a $b
Zastanów się, co dokładnie się stało. Przypomnij sobie, czym jest zmienna środowiskowa i jak działa polecenie export
.
Skrypty jako polecenia
Nazwę skryptu można też traktować jako nazwę polecenia. Załóżmy, że w bieżącym katalogu znajduje się plik skrypt.sh
. Moglibyśmy chcieć go wykonać, wywołując polecenie
$ skrypt.sh
Najprawdopodobniej skończy się to jednak porażką, bo:
- powyższe wywołanie spowoduje, że powłoka podąży w poszukiwaniu pliku
skrypt.sh
do katalogów określonych w zmiennejPATH
(patrz poprzednie ćwiczenia) – szansa, że katalog, w którym znajduje się skrypt znajduje się na tej liście jest niewielka, - aby wykonać plik (tzn. odwołać się do niego jak do programu), musimy mieć do niego prawa wykonywania.
Aby więc uruchomić skrypt korzystając z tej metody, należy upewnić się, że mamy prawo jego wykonania, a następnie odwołać się do niego korzystając ze ścieżki względnej lub bezwzględnej (która będzie traktowana przez powłokę jak ścieżka, a nie samodzielna nazwa), pisząc choćby
$ ./skrypt.sh
gdy plik skrypt.sh
znajduje się w bieżącym katalogu.
W dalszej części materiałów będziemy odwoływać się do skryptów w ostatni z przedstawionych sposobów. Zanim wykonasz ćwiczenia, upewnij się, że do pliku ze skryptem dodane zostały uprawnienia do wykonania.
Załóżmy, że odwołaliśmy się do skryptu wykonując polecenie
$ ./skrypt.sh
Skąd powłoka wie, że polecenia zawarte w wywołanym pliku powinny być wykonane w podprocesie będącym programem bash
? Zwykle mechanizmy powłoki rozpoznają, czy wywoływany plik jest programem, czy skryptem. Jeśli powłoka ma do czynienia ze skryptem, stara się zdecydować, który program powinien być użyty do jego zinterpretowania. Może się to odbywać arbitralnie (jeśli nie powiedziano inaczej, wykonaj skrypt w powłoce Bash) lub przez próbę dobrania odpowiedniego programu do rozszerzenia pliku, np. powłoki sh
lub bash
dla plików z roszerzeniem .sh
, interpretera języka Python dla plików z roszerzeniem .py
itp. Nie jest to jednak sposób niezawodny – po pierwsze, plik skryptu nie musi mieć żadnego rozszerzenia, a po drugie, możemy chcieć wymusić wykonanie pliku z wykorzystaniem określonego programu. Informację o tym, jaki program ma zostać uruchomiony, aby wykonać polecenia zawarte w skrypcie, możemy zawrzeć w jego pierwszym wierszu, wskazując bezwzględną ścieżkę do interpretera. W przypadku powłoki Bash będzie to
#!/bin/bash
Zbitkę #!
rozpoczynającą wiersz nazywa się shebang lub hashbang.
Stosowanie wprowadzonego tu zapisu jest dobrym zwyczajem, ponieważ zdejmuje z powłoki koniecznośc podejmowania decyzji. Tak opisany skrypt jest więc bardziej uniwersalny i łatwiejszy w przenoszeniu między platformami.
Metaymbol #
jest symbolem komentarza powłoki Bash. Oznacza to, że nic, co znajduje się za tym symbolem, nie zostanie potraktowane jako część polecenia.
Utwórz w swoim katalogu domowym skrypt o nazwie witaj.sh
i treści
#!/bin/bash
echo "Witaj uzytkowniku $USER" # To jest komentarz
Wykonaj go, wykorzystując wszystkie poznane dotąd metody. Dogłębnie zastanów się, jak zachowuje się powłoka w każdym z przypadków.
Budowa skryptu
Parametry pozycyjne
Do skryptu powłoki, podobnie jak do innych poleceń i programów, można przekazywać parametry (argumenty). Odwołać się do nich można korzystając ze specjalnych zmiennych:
$0
– przechowuje nazwę skryptu w postaci, w jakiej został on wywołany,$1
,$2
, ... – przechowują kolejne parametry pozycyjne (argumenty),$#
– przechowuje liczbę parametrów pozycyjnych (argumentów),$*
– przechowuje wszystkie parametry pozycyjne (argumenty) w postaci ciągów znaków oddzielonych znakiem odstępu.
Utwórz w swoim katalogu domowym skrypt o nazwie parametry.sh
i treści
#!/bin/bash
echo "Liczba parametrow: $#"
echo "Nazwa skryptu: $0"
echo "Parametry: $*"
echo "Drugi parametr: $2"
Następnie wykonaj polecenia
$ ./parametry.sh
$ ./parametry.sh 1 2 3 4 5
$ . parametry.sh 1 2 3 4 5
$ ./parametry.sh a "b c" d e
$ ./parametry.sh s "b c" d e
Zainterpretuj wyniki.
Utwórz w swoim katalogu domowym skrypt o nazwie echo.sh
i treści
#!/bin/bash
echo $*
echo "$*"
Następnie wykonaj polecenia
$ ./echo.sh a b c d e
$ ./echo.sh a b c d e
$ ./echo.sh "a b" c "d e"
Dlaczego w ostatnim przypadku skrypt powoduje wyświetlenie dwóch różnych wyników?
Polecenie shift
wywołane wewnątrz skryptu powoduje przesunięcie parametrów pozycyjnych o jedną pozycję w lewo. Oznacza to zmniejszenie liczby tych parametrów o jeden i usunięcie pierwszego z nich.
Utwórz w swoim katalogu domowym skrypt o nazwie shift.sh
i treści
#!/bin/bash
echo "Liczba parametrow: $#"
echo "Parametry: $1 $2 $3 $4"
shift
echo "Liczba parametrow: $#"
echo "Parametry: $1 $2 $3 $4"
Następnie wykonaj polecenia
$ ./shift.sh
$ ./shift.sh 1 2 3
$ ./shift.sh 1 2 3 4
$ ./shift.sh 1 2 3 4 5
Zaobserwuj efekty.
Polecenie read
Polecenie read
pozwala wczytywać dane ze standardowego wejścia do wskazanej zmiennej. Jako argument przyjmuje nazwę zmiennej, do której zostanie wczytany kolejny wiersz danych. Jeśli nie określono, do jakiej zmiennej ma być wczytany wiersz, zostanie wczytany do zmiennej $REPLY
.
Poniższy skrypt realizuje polecenie read
:
#!/bin/bash
read
echo "$REPLY"
read zmienna
echo "$zmienna"
Polecenie read
pozwala wczytać zawartość kilku zmiennych jednocześnie. Nazwy tych zmiennych powinny być kolejnymi argumentami polecenia.
Sprawdź, jak zadziała poniższy skrypt:
#!/bin/bash
read a b c
echo "a: $a"
echo "b: $b"
echo "c: $c"
Na wejściu wprowadź (w kolejnych próbach) następujące ciągi znaków:
1
1 2
1 2 3
1 2 3 4
Jakie wnioski można wyciągnąć z tego eksperymentu?
Polecenie exit
Kiedy skrypt wykonywany jest w osobnej instancji powłoki, w momencie zakończenia zwraca do powłoki macierzystej status wyjścia. Wykorzystując polecenie exit
, możemy jednocześnie zakończyć wykonywanie skryptu (gdyż polecenie to zakończy sesję powłoki) i zwrócić procesowi macierzystemu wybrany status wyjścia. Aby to zrobić, należy umieścić w odpowiednim miejscu skryptu polecenie
exit 0
gdzie 0 można zastąpić dowolną liczbą całkowitą z przedziału od 0 do 255. Jeśli status wyjścia nie zostanie określony, zwrócony zostanie status wyjścia ostatniego polecenia, które go określiło.
Porównaj działanie następujących skryptów w kontekście zwracanego statusu wyjścia:
#!/bin/bash
topolecenienieistnieje
exit
ls
#!/bin/bash
topolecenienieistnieje
exit 0
ls
Polecenie test
Powłoka Bash udostępnia polecenie o nazwie test
, pozwalające weryfikować prawdziwość przekazanego jako argumenty warunku. Polecenie to zwraca status wyjścia równy 0, jeśli warunek jest prawdziwy lub inną wartość, jeśli jest nieprawdziwy. Dzięki temu może być wykorzystywany z powodzeniem w budowie skryptów powłoki. Polecenie to można wywołać dwojako:
$ test warunek
lub, równoważnie
$ [ warunek ]
Zwróć uwagę na odstępy po nawiasie kwadratowym otwierającym i przed nawiasem kwadratowym zamykającym.
Polecenie test
pozwala budować warunki w oparciu o mnogość opcji. Niektóre z nich zostały zebrane w poniższej tabeli.
Warunek | Znaczenie |
---|---|
Testy wartości liczbowych | |
$a -eq $b | weryfikuje równość dwóch wartości |
$a -ge $b | weryfikuje czy $a jest większe lub równe $b |
$a -le $b | weryfikuje czy $a jest mniejsze lub równe $b |
$a -gt $b | weryfikuje czy $a jest większe niż $b |
$a -lt $b | weryfikuje czy $a jest mniejsze niż $b |
Testy ciągów znakowych | |
$a = $b | weryfikuje równość dwóch ciągów |
$a != $b | weryfikuje czy $a jest różne od $b |
$a < $b | weryfikuje czy $a jest leksykograficznie mniejsze od $b |
$a > $b | weryfikuje czy $a jest leksykograficznie większe od $b |
Testy zmiennych | |
-z $a | weryfikuje czy $a ma zerową długość |
-n $a | weryfikuje czy $a ma niezerową długość |
Testy plików | |
-e plik | weryfikuje, czy plik plik istnieje |
-c plik | weryfikuje, czy plik plik jest urządzeniem znakowym |
-b plik | weryfikuje, czy plik plik jest urządzeniem blokowym |
-f plik | weryfikuje, czy plik plik jest zwykłym plikiem |
-d plik | weryfikuje, czy plik plik jest katalogiem |
-L plik | weryfikuje, czy plik plik jest dowiązaniem symbolicznych |
-r plik | weryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia odczytu do plik |
-w plik | weryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia zapisu do plik |
-x plik | weryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia wykonania do plik |
-O plik | weryfikuje, czy użytkownik wykonujący skrypt jest właścicielem pliku plik |
plik1 -ef plik2 | weryfikuje, czy plik plik1 ma ten sam numer i-węzła, co plik2 |
plik1 -nt plik2 | weryfikuje, czy plik plik1 jest nowszy niż plik2 |
plik1 -ot plik2 | weryfikuje, czy plik plik1 jest starszy niż plik2 |
Operatory logiczne | |
war1 -a war2 | operator koniunkcji logicznej |
war1 -o war2 | operator alternatywy logicznej |
! war1 | operator negacji |
( warunek ) | operator grupowania wyrażeń |
Sprawdź, jakie statusy wyjścia generują następujące polecenia:
$ test 5 -eq 6
$ test -d ~/Desktop
$ [ 5 -eq 6 ]
$ [ -d ~/Desktop ]
Samodzielnie zweryfikuj działanie polecenia test
z wykorzystaniem zmiennych.
Instrukcja warunkowa if
W języku powłoki Bash wszystkie warunki logiczne są tak naprawdę poleceniami, które zwracają odpowiednie statusy wyjścia. Wszystkie instrukcje oparte na warunkach, w tym instrukcja if
, działają tak, że warunek uznaje się za spełniony, jeśli wykonywanie związanego z nim polecenia zakończy się zwróceniem statusu wyjścia równego 0. To dość specyficzne podejście, które wymaga przyzwyczajenia i... znajomości poleceń.
Instrukcja warunkowa if
przyjmuje ogólną konstrukcję
if warunek
then
instrukcja
elif warunek
instrukcja
...
elif warunek
instrukcja
else
instrukcja
fi
przy czym bloki elif
oraz else
mogą zostać pominięte.
Poniższe skrypty wykorzystują instrukcję if
.
#!/bin/bash
if [ $# -ge 2 -a $# -le 4 ]
then
echo "Liczba argumentow nalezy do przedzialu [2,4]"
exit 0
elif [ $# -gt 4 -a $# -le 6 ]
then
echo "Liczba argumentow nalezy do przedzialu [5,6]"
exit 0
else
echo "Liczba argumentow nie nalezy do przedzialu [2,6]"
exit 1
fi
#!/bin/bash
if grep "^$1:" /etc/passwd &> /dev/null
then
exit 0
fi
exit 1
Co robi drugi z przedstawionych skryptów?
Drugi ze skryptów weryfikuje, czy użytkownik o loginie przekazanym jako pierwszy parametr istnieje w lokalnym systemie.
Pętla for
Pętla for
przyjmuje w języku powłoki Bash jeden ze wariantów
for zmienna in zbior
do
instrukcja
done
for ((instrukcja poczatkowa;warunek petli;instrukcja iteracyjna))
do
instrukcja
done
Poniższe skrypty wykorzystują pierwszy z możliwych wariantów
#!/bin/bash
for i in Ala ma kota
do
echo $i
done
#!/bin/bash
for litera in {a..z}
do
echo $litera
done
#!/bin/bash
for plik in `ls`
do
echo $plik
done
Przeanalizuj powyższe przykłady. Zastanów się, jak zachowuje się powłoka Bash w momencie wykonywania drugiego wiersza każdego ze skryptów.
Poniższy skrypt wykorzystuje drugi z wariantów pętli for
:
#!/bin/bash
for ((i=1;i<=10;i++))
do
for ((j=1;j<=10;j++))
do
k=$((i*j))
if [ $k -lt 100 ]; then echo -n " "; fi
if [ $k -lt 10 ]; then echo -n " "; fi
echo -n "$k "
done
echo
done
Co robi powyższy skrypt? Zwróć uwagę na zapis instrukcji warunkowej if
wewnątrz pętli for
. Dlaczego taki zapis działa?
Pętla while
Pętla while
przyjmuje w języku powłoki Bash postać
while warunek
do
instrukcja
done
Poniższy skrypt wykorzystuje pętlę while
#!/bin/bash
l=$1
if ([ "$l" -eq "$l" ] && [ $l -gt 0 ]) &> /dev/null
then
while [ $l -gt 0 ]
do
str="$((l%2))$str"
l=$((l/2))
done
echo $str
exit 0
else
echo "Argumentem powinna byc liczba dodatnia" 1>&2
exit 1
fi
Co robi powyższy skrypt?
Zastanów się nad trickami wykorzystanymi w przedstawionym powyżej skrypcie. Uwaga. To nie jest łatwe ćwiczenie. Poświęć mu tyle uwagi, ile potrzeba.
Zmodyfikuj powyższy skrypt tak, aby warunek logiczny instrukcji if
nie wykorzystywał łączenia poleceń na poziomie powłoki, lecz wewnątrz polecenia test
.
Polecenia break
oraz continue
Podobnie jak w znanych językach programowania, tak i w języku skryptowym powłoki Bash można korzystać z poleceń break
oraz continue
wewnątrz pętli for
oraz while
.
Poniższy skrypt pokazuje przykład wykorzystania instrukcji break
oraz continue
w pętli for
.
#!/bin/bash
for i in {1..30}
do
if [ $((i%3)) -eq 1 ]
then
continue
fi
if [ $i -ge 15 ]
then
break
fi
echo $i
done
Selektor case
Selektor case
przyjmuje ogólną postać:
case wartosc in
zakres1) instrukcja ;;
zakres2) instrukcja ;;
...
zakresn) instrukcja ;;
esac
Warto tu zwrócić uwagę na metasymbol ;;
będący terminatorem bloku instrukcji. Jako zakresy można wykorzystywać dopasowywanie wzorów poznane na ćwiczeniach 3.
Poniższy skrypt pokazuje wykorzystanie selektora case
.
#!/bin/bash
case "$1" in
[0-9]) echo "Argument to cyfra" ;;
[[:alpha:]]) echo "Argument to litera" ;;
"napis") echo "Argument to ciag 'napis'" ;;
a*) echo "Argument zaczyna sie od litery 'a'" ;;
*) echo "Argument nie spelnia zadnego kryterium" ;;
esac