Moduł 5

Materiały do zajęć z Systemów operacyjnych prowadzonych na Wydziale Matematyki i Informatyki Uniwersytetu im. Adama Mickiewicza w Poznaniu.

« Wróć do spisu materiałów

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:

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:

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.

WarunekZnaczenie
Testy wartości liczbowych
$a -eq $bweryfikuje równość dwóch wartości
$a -ge $bweryfikuje czy $a jest większe lub równe $b
$a -le $bweryfikuje czy $a jest mniejsze lub równe $b
$a -gt $bweryfikuje czy $a jest większe niż $b
$a -lt $bweryfikuje czy $a jest mniejsze niż $b
Testy ciągów znakowych
$a = $bweryfikuje równość dwóch ciągów
$a != $bweryfikuje czy $a jest różne od $b
$a < $bweryfikuje czy $a jest leksykograficznie mniejsze od $b
$a > $bweryfikuje czy $a jest leksykograficznie większe od $b
Testy zmiennych
-z $aweryfikuje czy $a ma zerową długość
-n $aweryfikuje czy $a ma niezerową długość
Testy plików
-e plikweryfikuje, czy plik plik istnieje
-c plikweryfikuje, czy plik plik jest urządzeniem znakowym
-b plikweryfikuje, czy plik plik jest urządzeniem blokowym
-f plikweryfikuje, czy plik plik jest zwykłym plikiem
-d plikweryfikuje, czy plik plik jest katalogiem
-L plikweryfikuje, czy plik plik jest dowiązaniem symbolicznych
-r plikweryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia odczytu do plik
-w plikweryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia zapisu do plik
-x plikweryfikuje, czy użytkownik wykonujący skrypt ma uprawnienia wykonania do plik
-O plikweryfikuje, czy użytkownik wykonujący skrypt jest właścicielem pliku plik
plik1 -ef plik2weryfikuje, czy plik plik1 ma ten sam numer i-węzła, co plik2
plik1 -nt plik2weryfikuje, czy plik plik1 jest nowszy niż plik2
plik1 -ot plik2weryfikuje, czy plik plik1 jest starszy niż plik2
Operatory logiczne
war1 -a war2operator koniunkcji logicznej
war1 -o war2operator alternatywy logicznej
! war1operator 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?

Zobacz odpowiedź

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