Środowisko programisty/Bash - pisanie skryptów
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:
- użytkownicy, do których należy dany plik,
- inni użytkownicy z grupy, do której należy dany plik,
- 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 ))
są true
i false
, co przydaje się na przykład w pętlach.