Środowisko programisty/Bash - pisanie skryptów

From Studia Informatyczne

Spis treści

Atrybuty plików

W systemie typu Unix jest podział na użytkowników i grupy. Każdy użytkownik może przynależeć do kilku grup. Do wyświetlania przynależności do grup służy polecenie groups.

bashtest@host:~$ groups 
users
bashtest@host:~$ groups bashtest root kubus
bashtest : users
root : root
kubus : users cdrom floppy audio src video staff
bashtest@host:~$ 

Bez argumentów wyświetla przynależność do grup aktualnego użytkownika. Z argumentami przynależność do grup podanych użytkowników. Na przykład użytkownik kubus przynależy do większej ilości grup, co daje mu większe prawa w systemie.

Każdy plik/katalog należy do dokładnie jednego użytkownika i grupy. Z każdym plikiem/katalogiem związane są trzy rodzaje praw dostępu:

r prawo do odczytu,
w prawo do modyfikacji (czyli do zapisu, bądź usunięcia),
x prawo do uruchomienia; w przypadku katalogu oznacza to prawo do zmiany bieżącego katalogu na ten katalog.

Prawa dostępu przydzielane są trzem kategoriom użytkowników:

  1. użytkownicy, do których należy dany plik,
  2. inni użytkownicy z grupy, do której należy dany plik,
  3. wszyscy pozostali użytkownicy.

Aby wyświetlić informacje o właścicielach i prawach dostępu możemy użyć polecenia ls z opcją -l:

bashtest@host:~$ ls -l
razem 128
drwx------ 2 bashtest users   4096 2006-07-08 09:37 Mail
d-wx--x--x 2 bashtest users   4096 2006-08-07 15:28 niedostępny_katalog
----rw---- 1 bashtest users      5 2006-08-07 15:30 plik_dla_pozostałych_userów
-rwxr-xr-x 1 root     root  109552 2006-08-07 15:32 program
-rw-r--r-- 1 bashtest users     13 2006-08-01 15:18 test.txt
bashtest@host:~$

Po lewej stronie są prawa dostępu. Literka po lewej mówi o typie pliku, kolejne trzy literki pokazują prawa dostępu dla pierwszej kategorii użytkowników, kolejne trzy o drugiej kategorii użytkowników i ostatnie trzy literki o ostatniej kategorii. W trzeciej i czwartej kolumnie pokazany jest użytkownik i grupa do której należy dany plik/katalog.

Mail i niedostępny_katalog są katalogami (literka d po lewej). Katalog Mail jest dostępny tylko dla użytkownika bashtest (2-4 literki rwx oznaczają ustawione wszystkie prawa dostępu: do odczytu, zapisu i uruchamiania). Katalog niedostępny_katalog nie ma ustawionych praw do odczytu, zatem nie można wyświetlić jego zawartości, ale można zmienić na niego bieżący katalog, gdyż ma ustawione prawa do uruchomienia. Plik plik_dla_pozostałych_userów mogą odczytywać i modyfikować tylko użytkownicy inni niż bashtest należący do grupy users. Plik program jest programem i można go uruchamiać.

Do zmiany właściciela służą polecenia chown i chgrp. Do zmiany praw dostępu służy polecenie chmod.

Pierwszy skrypt

Przygotujmy plik hello_world.sh o następującej zawartości:

#!/bin/sh
echo "Hello world"

Rozszerzenie sh jest standardowym rozszerzeniem skryptów napisanych w bashu. Nie jest ono konieczne, ale dobrze by było, żeby już sama nazwa pliku mówiła nam o jego typie. Pierwsza linijka jest podpowiedzią dla systemu, jak ma być uruchomiony ten plik. System użyje polecenia /bin/sh do interpretacji tego pliku.

Spróbujmy uruchomić ten plik.

bashtest@host:~$ hello_world.sh
bash: hello_world.sh: command not found
bashtest@host:~$

Takie polecenie nie zostało znalezione. System szuka danego polecenia wśród wszystkich katalogów zapamiętanych na zmiennej środowiskowej PATH. Zmienna środowiskowa jest to taka zmienna, która została zdefiniowana zanim jeszcze uruchomiliśmy interpreter. Własne zmienne środowiskowe, które zostaną przekazane programom przez nas uruchomionych można definiować za pomocą komendy export. Zobaczmy, co zawiera zmienna PATH.

bashtest@host:~$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games
bashtest@host:~$

Jak widzimy, nie zawiera ona bieżącego katalogu, w którym znajduje się nasz skrypt. Przy uruchamianiu polecenia, które nie znajduje katalogu podanym w PATH, trzeba podawać również ścieżkę (względną, bądź bezwzględna) przed nazwą pliku. W tym przypadku musimy podać katalog bieżący, co najprościej można zrobić przy użyciu kropki.

bashtest@host:~$ ./hello_world.sh
bash: ./hello_world.sh: Brak dostępu
bashtest@host:~$ 

Tym razem dostaliśmy komunikat o złych prawach dostępu. Zobaczmy:

bashtest@host:~$ ls -l hello_world.sh 
-rw-r--r-- 1 bashtest users 29 2006-08-07 15:45 hello_world.sh
bashtest@host:~$

Ten plik nie ma ustawionych praw do uruchamiania. Możemy to zrobić używając polecenia chmod. Aby ustawić prawa uruchamiania tylko dla użytkownika bashtest, możemy użyć opcji u+x. Jeśli chcemy ustawić prawa uruchamiania dla wszystkich, używamy opcji a+x. W tym przypadku ustawimy prawa uruchamiania tylko dla nas.

bashtest@host:~$ chmod u+x hello_world.sh 
bashtest@host:~$ ls -l hello_world.sh 
-rwxr--r-- 1 bashtest users 29 2006-08-07 15:45 hello_world.sh
bashtest@host:~$

Teraz wygląda lepiej spróbujmy uruchomić nasz skrypt.

bashtest@host:~$ ./hello_world.sh 
Hello world
bashtest@host:~$

Udało się!

Komentarze

Komentarze zaczynają się od symbolu #. Wszystkie pozostałe znaki aż do końca linii są ignorowane. W pierwszej linii skryptu helo_world.sh mamy już taki komentarz, który jest zarazem informacją dla systemu. Dodajmy jeszcze dwa komentarze.

#!/bin/sh

# Przykładowy skrypt wypisujący napis "Hello world"

echo "Hello world" # Tutaj wypisujemy co trzeba

Argumenty

Skrypty - podobnie jak dowolne programy - możemy uruchamiać podając im argumenty. Następujące zmienne o specjalnych nazwach pozwalają odczytywać argumenty:

$# zwraca liczbę argumentów,
$0 zwraca nazwę pliku bieżącego programu,
$1$2, ... zwraca odpowiednio pierwszy argument, drugi argument, itd.,
$@ rozwija się do listy wszystkich argumentów; przydatne jeśli chcemy przekazać wszystkie argumenty innemu programowi. Jeśli chcemy mieć pewność, że każdy argument będzie osobnym słowem, należy użyć cudzysłowów: "$@"; ma to znaczenie na przykład wtedy, gdy istnieje argument, który zawiera spację.

Aby operować na dalszych argumentach pomocne jest polecenie shift, które usuwa pierwszy argument, a pozostałe przesuwa o jeden w lewo. Aby n-krotnie wywołać polecenie shift wystarczy podać mu to n jako argument: shift n.

Na przykład dla skryptu test_arg.sh o zawartości

#!/bin/sh

# Testowanie argumentów

echo "Uruchomiłeś program `basename $0`"
echo Wszystkie: $@
echo "Pierwsze trzy: '$1', '$2', '$3'"
shift 2
echo "shift 2"
echo "Wszystkie: $@"
echo "Pierwsze trzy: '$1', '$2', '$3'"

mamy efekt

bashtest@host:~$ ./test_arg.sh Raz Dwa    "To jest  zdanie" Cztery
Uruchomiłeś program test_arg.sh
Wszystkie: Raz Dwa To jest zdanie Cztery
Pierwsze trzy: 'Raz', 'Dwa', 'To jest  zdanie'
shift 2
Wszystkie: To jest  zdanie Cztery
Pierwsze trzy: 'To jest  zdanie', 'Cztery', ''
bashtest@host:~$

Wyrażenia

Jak w każdym liczącym się języku, w bashu możemy wyliczać wartości wyrażeń arytmetycznych. Możemy zrobić to na kilka sposobów.

expr

Najprostszym sposobem jest użycie polecenia expr. Trzeba przy tym pamiętać, żeby osobne tokeny (tzn. liczby i operatory arytmetyczne) były podawane w osobnych argumentach. Wynika to stąd, że expr potrafi też operować na łańcuchach znakowych (czym się nie będziemy w tej chwili zajmować), więc musi jakoś te łańcuchy dostawać, a jedyną droga to przez argumenty.

Dostępnych jest pięć operatorów arytmetycznych:

  • dodawanie (+),
  • odejmowanie (-),
  • mnożenie (*),
  • dzielenie (/),
  • modulo - reszta z dzielenia (%).

Ponadto możemy wykonywać porównania <, <=, =, == (synonim =), !=, >=, >. W wyniku mamy 1, gdy relacja jest spełniona i 0 w przeciwnym przypadku.

Trzeba też pamiętać by znaki specjalne poprzedzać backslashem lub brać w cudzysłowy. Przykłady:

bashtest@host:~$ expr 2\*3
2*3
bashtest@host:~$ expr 2 \* 3
6
bashtest@host:~$ expr '2 * 3'
2 * 3
bashtest@host:~$ expr 2 \* \(7 - 1\)
expr: argument nieliczbowy
bashtest@host:~$ expr 2 \* \( 7 - 1 \)
12
bashtest@host:~$ a=5
bashtest@host:~$ a=`expr $a + 1`
bashtest@host:~$ echo $a
6
bashtest@host:~$ expr 3 \<= 4
1
bashtest@host:~$ expr 3 '<=' 1
0
bashtest@host:~$

$(( ... )) i (( ... ))

Znacznie wygodniejszą formą pisania wyrażeń jest forma $(( wyrażenie )). W stosunku do expr w ciapkach ma prawie same zalety. Jest jeden problem, ta składnia może nie działać w innych shellach, czy w starszych wersjach Basha (ale kto teraz używa czegoś innego niż Bash). Pierwszą zaletą jest szybkość, tzn. użycie tej składni nie powoduje tworzenia nowego procesu (co ma miejsce w przypadku `expr ...`) i jest interpretowane bezpośrednio przez Basha. Po drugie przy odwoływaniu się do zmiennych nie musimy poprzedzać ich znakiem $, gdyż każdy identyfikator wewnątrz podwójnych nawiasów jest traktowany jak zmienna. Nie musimy także dbać o używanie odstępów i backslashowania znaków specjalnych. Trzecią zaletą jest bogatsza paleta operatorów arytmetycznych. Otóż wyrażenia arytmetyczne mogą tu zawierać dowolne operatory, które można znaleźć w języku C, np. inkrementacje/dekrementacje zmiennych (ID++, --ID), operacje bitowe (<<, &, ~), przypisania arytmetyczne (=, +=, *=), itp. Więcej o znaczeniu tych operacji i dozwolonych działaniach można znaleźć w kursie języka C lub w dokumentacji Basha.

Składni (( wyrażenie )) używamy wtedy, gdy nie potrzebujemy wyniku, czyli wtedy, gdy wyrażenie nie jest częścią instrukcji, tylko jest sama w sobie instrukcją. Najlepiej będzie, jak przyjrzymy się przykładom.

Kilka sposobów na zwiększenie zmiennej o 1:

bashtest@host:~$ a=0
bashtest@host:~$ a=$((a + 1))
bashtest@host:~$ ((a=a+1))
bashtest@host:~$ ((a++))
bashtest@host:~$ ((a += 1))
bashtest@host:~$ echo $a
4
bashtest@host:~$ 

Inne przykłady:

bashtest@host:~$ echo "1 + ... + $x = $((x * (x + 1) >> 1))"
1 + ... + 5 = 15
bashtest@host:~$ echo $((x++))
5
bashtest@host:~$ echo $((++x))
7
bashtest@host:~$ echo $((x += x > 0))
8
bashtest@host:~$ echo "x = $x"
x = 8
bashtest@host:~$

let

let jest wbudowanym poleceniem Basha i używamy go, podając mu jako argumenty wyrażenia do przetworzenia.

let wyrażenie1 wyrażenie2 ...

równoważne jest ciągowi poleceń

((wyrażenie1))
((wyrażenie2))
...

Przykład:

bashtest@host:~$ x=0
bashtest@host:~$ let x+=2 "x += 4"
bashtest@host:~$ echo $x
6
bashtest@host:~$ 

Trzeba pamiętać, że wyrażenie zawierające odstępy trzeba ujmować w cudzysłowy, aby formowały jeden argument.

Wczytywanie wejścia

W skryptach czasami jest potrzeba wczytania czegoś ze standardowego wejścia. Możemy chcieć pobrać od użytkownika jakąś informację. Możemy też chcieć wczytywać standardowe wejście i stopniowo je przetwarzać. Do tych celów jest polecenie read.

read wywołane bez argumentów wczytuje jedną linię ze standardowego wejścia na zmienną o nazwie REPLY. Jeśli podamy jeden argument, read wczyta tą linię na zmienną o nazwie takiej samej, jak zawartość argumentu. Jeśli podamy więcej argumentów reprezentujących nazwy zmiennych, read na pierwsze zmienne będzie wczytywał pojedyncze słowa, a na ostatnią wczyta pozostałość bieżącej linii do jej końca. Prześledźmy to na przykładzie.

Dla skryptu

#!/bin/sh  

read
echo $REPLY 
read a
echo $a
read a b c
echo "a='$a', b='$b', c='$c'"
read x
echo "'$x'"

i dla wejścia

Pierwsza linia   (pamiętać o cudzysłowach przy odwoływaniu się do $REPLY)
Druga linia      (teraz pamiętamy - "$a")
Raz Dwa Trzy Cztery

Czwarta linia jest pusta, a to jest piąta linia

otrzymamy wynik

Pierwsza linia (pamiętać o cudzysłowach przy odwoływaniu się do $REPLY)
Druga linia      (teraz pamiętamy - "$a")
a='Raz', b='Dwa', c='Trzy Cztery'

Podawanie wejścia poleceniu w skrypcie

Gdy wykonujemy polecenie, czasami chcemy zadać mu konkretne wejście. Możemy to zrobić na przykład za pomocą komendy echo:

echo "Nasze wejście" | polecenie

Użycie echo dla wejść, które mają składać się z wielu linii jest jednak kłopotliwe. W tym celu w Bashu jest możliwość podania fragmentu skryptu jako wejście do polecenia. Służy do tego symbol specjalny <<. Takie "przekierowanie" << SŁOWO mówi, że wejście dla uruchamianego polecenia ma być czytane z aktualnego wejścia tak długo, aż zostanie napotkany napis SŁOWO. Na przykład wynikiem skryptu

#!/bin/sh
echo "Moje ulubione liczby:"
sort -n << LICZBY
120
10
2006
314159
0
LICZBY
echo "Od najmniejszej do największej, rzecz jasna"

jest

Moje ulubione liczby:
0
10
120
2006
314159
Od najmniejszej do największej, rzecz jasna

Status wyjścia

Każdy program po ukończeniu zwraca swój kod wyjścia. Można go pobrać używając specjalnej zmiennej $?.

bashtest@host:~$ ls *.txt
test.txt
bashtest@host:~$ echo $?
0
bashtest@host:~$ ls *.nieznane
ls: *.nieznane: Nie ma takiego pliku ani katalogu
bashtest@host:~$ echo $?
2
bashtest@host:~$

Zgodnie z konwencją jeśli polecenie wykonało się z sukcesem, kodem wyjścia jest 0, a jeśli w wyniku wykonania pojawiły się błędy lub polecenie skończyło się porażką, zwracany jest kod różny od zera.

Normalnie własny skrypt kończy się ze statusem wyjścia równym zero. Możemy zakończyć skrypt w dowolnym miejscu z wybranym przez nas statusem wyjścia, stosując polecenie exit. Na przykład instrukcja exit 1 powoduje natychmiastowe zakończenie skryptu z kodem wyjścia 1.

Instrukcje warunkowe

if

Instrukcja if w najprostszej postaci ma następującą składnię:

if polecenie_warunek; then
  instrukcje
fi

Jej działanie jest następujące. Wykonywane jest polecenie polecenie_warunek. Jeśli kod wyjścia tego polecenia jest 0, wykonywane są instrukcje między then, a fi. Jeśli kod wyjścia polecenia był niezerowy, wykonywanie instrukcji if jest zakończone i interpreter przechodzi do wykonywania instrukcji znajdujących się po słowie kluczowym fi.

Widzimy, że rolę warunków logicznych spełniają tu po prostu zwykłe polecenia, a prawda lub fałsz jest to odpowiednio status wyjścia równy zero lub status wyjścia różny od zera.

Składnia if z użyciem else:

if polecenie_warunek; then
  instrukcje1
else
  instrukcje2
fi

Jeśli warunek jest prawdziwy, wykonywane są instrukcje1, w przeciwnym razie wykonywane są instrukcje2. Przykład:

if cd $katalog; then
  echo "Jesteśmy w katalogu $katalog"
else
  echo "Nie udało się wejść do katalogu $katalog"
fi

Pełna składnia if jest następująca:

if warunek1; then
  instrukcje1
elif warunek2; then
  instrukcje2;
...
else
  instrukcje_else;
fi

Część z else jest opcjonalna. instrukcje1 są wykonane, jeśli jest spełniony warunek1, w przeciwnym razie, jeśli jest spełniony warunek2, to wykonywane są instrukcje2, itd. Na końcu, jeśli żaden warunek nie jest spełniony, wykonywane są instrukcje_else.

Wyrażenia logiczne

Powstaje pytanie, jak tworzyć polecenia, które sprawdzają jakieś sensowne warunki np. porównywanie liczb. Do tego celu służy polecenie test. Potrafi ono porównywać łańcuchy znakowe, liczby i sprawdzać istnienie plików.

Jeśli chodzi o porównywanie łańcuchów znakowych, mamy następujące możliwości. -z ŁAŃCUCH sprawdza, czy długość łańcucha jest równa zero, a -n ŁAŃCUCH, sprawdza, czy długość łańcucha jest różna od zera. Ponadto możemy porównywać dwa łańcuchy np. ŁAŃCUCH1 < ŁAŃCUCH2. Porównanie jest leksykograficzne. Możliwe operatory to ==, !=, <, >.

Do porównywania dwóch liczb są inne operatory: -eq, -ne, -lt, -le, -gt, -ge, których odpowiedniki matematyczne to =, <>, <, <=, >, >=.

Na przykład poniższe polecenia zwrócą prawdę (tj. status wyjścia równy 0):

test -z ""
test abc \< def
test 3 \> 17
test 3 -lt 17

Można także sprawdzać istnienie i typ plików, na przykład:

if test -a $plik; then
  echo "$plik istnieje"
  if test -f $plik; then
    echo "$plik jest zwykłym plikiem"
  elif test -d $plik; then
    echo "$plik jest katalogiem"
  fi
fi

Polecenie

test warunek

można też pisać w postaci

[ warunek ]

Taka forma jest po prostu wygodniejsza.

Warto wiedzieć, że instrukcja arytmetyczna (( ... )) też zwraca status. Zwraca 0, jeśli wartość wyrażenia jest niezerowa, i zwraca 1, jeśli wartość wyrażenia wynosi 0. Pozwala to w Bashu stosować w bardzo wygodny sposób porównania, dokładnie tak samo jak w C.

bashtest@host:~$ if (( 0 )); then echo prawda; else echo fałsz; fi
fałsz
bashtest@host:~$ if (( 1 )); then echo prawda; else echo fałsz; fi
prawda
bashtest@host:~$ if (( 3 < 4 )); then echo prawda; else echo fałsz; fi
prawda
bashtest@host:~$ if (( 0 < -1 )); then echo prawda; else echo fałsz; fi
fałsz
bashtest@host:~$ if (( 3 * 6 - 2 * 9 )); then echo prawda; else echo fałsz; fi
fałsz
bashtest@host:~$ if (( 1/0 )); then echo prawda; else echo fałsz; fi
bash: ((: 1/0 : division by 0 (error token is " ")
fałsz
bashtest@host:~$

Najprostszymi poleceniami, które zwracają prawdę i fałsz, prostszymi nawet niż (( 1 )) i (( 0 ))true i false, co przydaje się na przykład w pętlach.