Praktyczne wykorzystanie skryptów powłoki


Streszczenie

W wykładzie 4 przedstawimy zaawansowane aspekty programowania skryptów interpretera poleceń, takie jak pętle, podrzędne interpretery, podprogramy oraz elementy losowości.


Warunki logiczne

Dotychczas nie wiemy jednak jak zapisać warunek1, warunek2, ..., warunek_else z instrukcji warunkowej. Otóż te warunki są po prostu wywołaniami programów. Jeśli taki program zakończy się powodzeniem (kod powrotu 0), to warunek jest prawdziwy. Jeśli program zakończy się błędem (kod powrotu jest niezerowy), to warunek jest fałszywy. To wyjaśnia, dlaczego za każdym warunkiem występuje średnik. Wskazuje on, gdzie kończą się argumenty programu-warunku. Bez tego średnika słowo kluczowe then zostałoby potraktowane jako argument programu warunku.

Do obliczania wartości wyrażeń logicznych najczęściej używa się programu test. Jego idea jest podobna do konstrukcji programu expr służącego do obliczania wartości wyrażeń arytmetycznych. Argumenty programu test są wyrażeniem logicznym, którego wartość je obliczana. Jeśli jest ono prawdziwe, program test kończy się kodem powrotu 0. Jeśli jest ono fałszywe, program test kończy się kodem powrotu 1. Oto przykład:

bash$ if test 1 == 1; then echo PRAWDA; else echo FAŁSZ; fi
PRAWDA
bash$ i=9
bash$ if test $[3 * 3] == $i; then echo PRAWDA; fi
PRAWDA
bash$: if test $i == 4; then echo PRAWDA; fi
bash$ _ 

Zamiast wywołania programu test można używać nawiasów kwadratowych [...]. Jest to po prostu skrótowy sposób wywołania programu test, np.

bash$ if [ 1 == 1 ] ; then echo PRAWDA; else echo FAŁSZ; fi
PRAWDA
bash$ _

Zadanie dla czytelnika

Napisać skrypt comp, który ma dwa argumenty i wypisuje -1, gdy pierwszy argument jest mniejszy niż drugi, 0 gdy są równe a -1 gdy pierwszy argument jest większy niż drugi. Skrypt ma sprawdzić, czy ma dobrą liczbę argumentów. Skrypt będzie wywoływany następująco:

comp arg1 arg2 
Zamiast operatorów <, >, != w argumentach programu test należy użyć odpowiednio -lt, -gt, -ne. Rozwiązanie.


Fragment skryptu jako dane wejściowe

W punkcie Przekierowanie wejścia-wyjścia poznaliśmy znaczenie symboli <, > i >>. Prawdopodobnie zastanowiło cię, dlaczego nie wymieniono <<. Otóż taka para znaków również ma specjalne znaczenie w interpreterze poleceń, jednak jest ono diametralnie odmienne od znaczenia tych trzech wymienionych na początku symboli. Ciąg znaków << służy do umieszczania w skrypcie danych, które będą podawane na standardowe wejście wykonywanych programów. Ten ciąg znaków ma praktyczne zastosowanie jedynie w skryptach.

Gdy chcemy, aby fragment skryptu stanowił dane wejściowe dla jakiegoś programu, umieszczamy na końcu jego wywołania << a potem dowolne słowo. W kolejnych liniach wpisujemy to, co ma być podane temu programowi na standardowe wejście. Linia złożona tylko ze słowa podanego za << kończy te dane wejściowe. Rozważmy na przykład skrypt:

cat > plik2.txt << KONIEC
ala
ma
kota
KONIEC
Taki skrypt powoduje stworzenie pliku o nazwie plik2.txt, który składa się z trzech linii ze słowami odpowiednio ala, ma i kota. W tym pliku nie ma słowa KONIEC. W skrypcie to słowo było znacznikiem końca danych wejściowych.

Zadanie dla czytelnika

Napisać skrypt wywoływany w następujący sposób:

wyslij plik adresat1 adresat2 ...
Ma on spowodować wysłanie listu o treści zadanej w 'pliku' do 'adresatów'. Na końcu listu ma znaleźć się podpis i data nadania listu. Do wysyłania listów elektronicznych służy program mail, którego argumentami są adresy odbiorców; treść listu jest czytana ze standardowego wejścia. Do wygenerowania podpisu skorzystaj z programu whoami, który wypisuje identyfikator bieżącego użytkownika. Rozwiązanie.


Pętle

W języku skryptowym interpretera poleceń zdefiniowano również pętle while i for. Oto składnia pętli while:

while warunek; do
  instrukcje
done
Jeśli warunek jest prawdziwy, to wewnętrzne instrukcje są wykonywane. Następnie ponownie sprawdza się prawdziwość warunku i ewentualnie znów wykonuje instrukcje. To postępowanie jest kontynuowane do chwili, w której warunek stanie się fałszywy, np.
bash$ i=4
bash$ while [ $i -gt 0 ] ; do echo $i; i=$[i - 1]; done
4
3
2
1
bash$ _

Pętla for ma bardzo podobną składnię:

for zmienna in lista_słów; do
  instrukcje
done
Na początku wylicza się wyrażenie lista_słów (jeśli jest to na przykład gwiazdka, to jej wyliczenie polega znalezieniu nazw wszystkich plików w bieżącym katalogu). Potem wykonuje się wewnętrzne instrukcje tyle razy, ile było słów na liście, przy czym za każdym wykonaniem na zmienną przypisuje się kolejne słowo z listy, np.
bash$ ls *.txt *.doc
szkic.txt
wyklad.doc
cwicz.doc
bash$ for x in *.txt *.doc ; do echo Plik $x to dokument. ; done
Plik szkic.txt to dokument.
Plik wyklad.doc to dokument.
Plik cwicz.doc to dokument.
bash$ _

Pętla for ma też specjalną, ale bardzo często wykorzystywaną postać, z której można korzystać w skryptach:

for zmienna; do
  instrukcje
done
W takiej pętli zmienna przebiega listę argumentów skryptu. Przyjmuje więc kolejno wartości $1, $2 itd.

Istnieje jeszcze pętla until, która wykonuje instrukcje do chwili, w której warunek stanie się prawdziwy (czyli odwrotnie niż while).

Ciekawym wykorzystaniem pętli może być iteracja po elementach tablicy:
a=0
until [ $a -eq ${#tab[@]} ]
do
    echo tab[$a] = "${tab[$[a++]]}"
done
Powyższy skrypt wypisze wszystkie elementy tablicy tab. Wyrażenie a++, podobnie jak w C i C++, zwraca aktualną wartość zmiennej a, po czym zwiększa ją o 1. Tego rodzaju wyrażenia często stosuje się przy różnego rodzaju iteracjach.

Zadania dla czytelnika

  1. Napisać skrypt, który wypisze swoje argumenty w odwrotnej kolejności. Rozwiązanie.

  2. Napisać skrypt, który poda najdłuższe słowo w plikach podanych jako jego argumenty (długość słowa można łatwo uzyskać składnią: ${#zmienna} ) Rozwiązanie.

  3. Napisać skrypt, który co 10 sekund będzie sprawdzał, czy użytkownik podany jako argument skryptu obecnie pracuje w systemie. Do wypisania listy zarejestrowanych użytkowników służy program who. Do uśpienia skryptu na n sekund służy polecenie sleep n. Do wyszukiwania wierszy, które zawierają podane słowo służy program grep. Zajrzyj do systemowego podręcznika użytkownika, żeby poznać działanie grep (wydaj polecenie man grep). Rozwiązanie.


Instrukcja wyboru

Instrukcja wyboru (case) umożliwia dopasowywanie wartości wyrażenia do kolejnych wzorców i wykonanie instrukcji, które są skojarzone z pierwszym wzorcem, do którego pasuje wyrażenie. Oto składnia:

case wyrażenie in
  wzorzec1) instrukcje1 ;;
  wzorzec2) instrukcje2 ;;
  ...
esac
Wzorce w case tworzy się tak, jak wzorce nazw plików, np. do t*y pasują wszystkie słowa zaczynające się na literę t a kończące się na literę y np. (ty, tygrysy, trytytytyty etc.). Na początku dopasowuje się wyrażenie do wzorca1. Jeśli pasuje, to wykonywane są instruckje1, po czym sterowanie przechodzi za słowo kluczowe esac. Jeśli nie pasuje, to dopasowuje się je do wzorca2 i ewentualnie wykonuje instrukcje2 itd.

Widać więc, że bardziej ogólne wzorce należy umieszczać za bardziej szczegółowymi. Co więcej, najbardziej ogólny wzorzec (gwiazdka) umieszczony na końcu instrukcji odpowiada gałęzi else polecenia if. Zauważmy, że umieszczanie jakiegokolwiek wzorca za wzorcem-gwiazką nie ma sensu, bo wszystko pasuje do gwiazdki.

Wiedząc, że wzorzec ? odpowiada jednemu dowolnemu znakowi (* pasuje do dowolnego, także pustego, ciągu znaków), rozważmy następujący przykładowy skrypt:

case $1 in
    ?) echo $1 ma jeden znak ;;
   ??) echo $1 ma dwa znaki ;;
  ???) echo $1 ma trzy znaki ;;
    *) echo $1 ma więcej niż trzy znaki ;;
esac
Wypisuje on komunikat o ilości znaków w pierwszym argumencie skryptu. Jeśli wzorzec * znalazłby się na pierwszej pozycji, to ten skrypt zawsze wypisywałby komunikat ma więcej niż trzy znaki.

Zadanie dla czytelnika

Napisać skrypt, który w zależności od wartości swojego argumentu wypisuje bieżącą datę, dzień tygodnia i nazwę miesiąca po polsku (gdy pierwszy argument to p) albo po angielsku (gdy pierwszy argument to a). Rozwiązanie.


Wywołanie podrzędnego interpretera

Umieszczenie zestawu instrukcji w zwykłych nawiasach powoduje uruchomienie nowego interpretera poleceń i wykonanie w nim tego zestawu instrukcji. To pozwala na przykład przechwycić standardowe wejście całego zestawu, a nie pojedynczej instrukcji, np.

bash$ (for k in *.txt; do wc -c "$k"; done) | sort -n
Na standardowe wejście programu sort podawany jest wynik instrukcji for. Wynikiem tej instrukcji jest lista rozmiarów plików z rozszerzeniem txt posortowana rosnąco (w porządku numerycznym).


Podprogramy

W skrypcie interpretera można definiować podprogramy. Oto składnia definicji podprogramu:

nazwa () 
{
  instrukcje
}
Podprogram może korzystać z argumentów chociaż ich się nie deklaruje. Argumenty wywołania podprogramu są dostępne poprzez symbole $1, $2, ..., $9 (tak samo odczytuje się argumenty samego skryptu). Analogicznie można też korzystać z symboli $0, $@, $# oraz polecenia shift. Podprogram wywołujemy tak, jakbyśmy wywoływali dowolny program albo skrypt. Należy po prostu podać jego nazwę i argumenty. Oto skrypt wyliczający rekurencyjnie silnię ze swego argumentu.
silnia () {
  # treść funkcji; tu $1 jest argumentem funkcji
  if [ $1 == 0 ] ; then
    wynik=1
  else
    silnia $[$1 - 1]
    wynik=$[wynik * $1]
  fi
}

# wywołanie funkcji; tu $1 jest argumentem skryptu
silnia $1
echo $wynik
Procedury mogą być również rekurencyjne:
silnia () {
  # treść funkcji; tu $1 jest argumentem funkcji
  if [ $1 == 0 ] ; then
    echo 1
  else
    echo $[$1 * `silnia $[$1 - 1]`]
  fi
}

# wywołanie funkcji; tu $1 jest argumentem skryptu
silnia $1

Zadanie dla czytelnika

Napisać skrypt, który wypisze drzewo katalogów i plików. Katalog ma być oznaczony plusem a zwykły plik minusem. Katalogi podrzędne mają być wcięte względem katalogów nadrzędnych. Oto przykładowy wynik działania tego skryptu:

+ bd
   + PL\SQL
      + 20002001
         - index.htmll
         - procs.sql
         - triggers.sql
      + bydlo
         - dane.sql
         - index.htmll
         - procedura.sql
         - tabele.sql
         - wyzwalacz.sql
      + cursor
         - index.htmll
         - usundziury.sql
      + dump_table
         - dump.sql
         - dumpTable.sql
         - index.htmll
Przed przystąpieniem do rozwiązywania tego zadania warto dokładniej zapoznać się z programem test. Zajrzyj do systemowego podręcznika użytkownika (wydaj polecenie man test). Rozwiązanie.


Elementy losowości

poczatek=(ładny "bardzo ładny" śliczny piękny)
koniec=("wystrój łazienki" "widok z okna" "dywan" "widok z klatki schodowej" "dojazd" "trawnik")
i=$[$RANDOM%${#poczatek[@]}]
j=$[$RANDOM%${#koniec[@]}]
echo "${poczatek[$i]} ${koniec[$j]}"

Powyższy program za każdym wykonaniem zwraca dość losowe zdanie zachwalające mieszkanie.

Za każdym razem, gdy następuje odwołanie do zmiennej $RANDOM, generowana jest całkowita liczba losowa z zakresu od 0 do 32767.

UWAGA: powyższa procedura nie daje rozkładu jednostajnego. Jeśli ${#poczatek[@]}=30000, to $RANDOM%${#poczatek[@]} wylosuje elementy od 0 do 2767 dwa wazy częściej niż pozostałe elementy.


Podsumowanie

Za pomocą konstrukcji interpretera można zrobić bardzo wiele. Z czasem, gdy oswoisz się z jego składnią i możliwościami, będziesz za jego pomocą wykonywał większość czynności w swoim systemie operacyjnym. Interfejs graficzny jest wygodny, ale nie jest tak elastyczny, jak interfejs linii poleceń. W graficznym interfejsie użytkownika poruszasz myszą i naciskasz jej guziki, a system na tej podstawie dedukuje, co chciałeś uzyskać, i robi to dla ciebie. Gdy wydajesz polecenia bezpośrednio, to ty jestem panem i władcą, a system robi dokładnie to, co zlecisz. Pisząc skrypty możesz łatwo zautomatyzować rutynowe czynności.


Słownik

argument skryptu
Słowa przekazane przy wywołaniu skryptu; są dostępne w treści skryptu; odczytujemy je za pomocą symboli $1, $2, ..., $9, $@ i $#.
instrukcja warunkowa
Instrukcja języka skryptowego umożliwiająca warunkowe wykonanie instrukcji.
instrukcja wyboru
Instrukcja języka skryptowego umożliwiająca uzależnienie wyboru instrukcji do wykonania od dopasowania pewnego wyrażenia do zbioru wzorców.
interpreter poleceń
Program, który oczekuje na polecenia użytkownika i po ich otrzymaniu je, wykonuje je. Wykonuje także polecenia języka skryptowego.
pętla
Konstrukcja programistyczna umożliwiająca wielokrotne wykonanie tej samej instrukcji.
podprogram
Funkcja albo procedura zadeklarowana i/lub wywoływana w skrypcie.
przekierowanie wejścia-wyjścia
Możliwość skojarzenia strumieni wejścia-wyjścia programu z plikami.
przetwarzanie potokowe
Wykonywanie programów w taki sposób, że standardowe wyjście jednego programu jest przekazywane na standardowe wejście innego.
skrypt
Ciąg poleceń interpretera zapisany w pliku. Zawiera wywołania programów, innych skryptów i instrukcje strukturalne.
standardowe wejście
Strumień, z którego większość programów czyta dane wejściowe.
standardowe wyjście
Strumień, do którego większość programów wysyła dane wyjściowe.
standardowe wyjście diagnostyczne
Strumień, do którego większość programów wysyła informacje o błędach, ostrzeżeniach i innych nieprawidłowościach.
środowisko programu
Składa się ze zmiennych i ich wartości. Jest przekazywane do wszystkich uruchamianych przez ten program programów. Program potomny ma dokładnie takie same środowisko jak program macierzysty.
tło
Miejsce wykonywania programów, które działając nie powstrzymują możliwości wprowadzania następnych poleceń.
zmienna
Nazwane miejsce przechowania wartości w interpreterze poleceń.
zmienna środowiskowa
Zmienna, która należy do środowiska programu.

Zadania

  1. (4p.) Napisać skrypt, który wszystkie pliki z przyrostkiem ~ (np. plik.txt~), skopiuje (jeżeli takie są) do katalogu BACKUP w bieżącym katalogu. Jeżeli katalog BACKUP nie istnieje, skrypt powinien go założyć. Jeżeli jest już plik (lub inny nie-katalog) o nazwie BACKUP, skrypt powinien zgłosić błąd.
  2. (6p.) Napisać skrypt, który obliczy n-tą liczbę Fibbonacciego.

Strona przygotowana przez Marcina Kubicę i Krzysztofa Stencla.