Środowisko programisty/Bash - pisanie skryptów

Z Studia Informatyczne
Wersja z dnia 13:07, 11 sie 2006 autorstwa Pan (dyskusja | edycje) (Przesunięcie skomplikowanych ficzerów do modułu "Skrypty złożone")
Przejdź do nawigacjiPrzejdź do wyszukiwania

Atrybuty plików

W systepie 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 rozszerzenie skryptów napisanych w bashu. Nie jest ono konieczne, ale dobrze, gdy już sama nazwa pliku mówi 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:~$ hellow_world.sh
bash: hellow_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. Zobaczmy co zawiera ta zmienna.

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

Na jak widzimy nie zawiera 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:~$

No tak 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, a jeśli chcemy ustawić prawa uruchamiania dla wszystkich, to 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, przy czym jeśli chcemy mieć pewność, że każdy argument będzie osobnym słowem, to 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 każdy szanującym się języku, w bashu możemy wyliczać wartości wyrażeń arytmetycznych. Możemy robić to na kilka sposobów.

expr

Najprostszym sposobem jest użycie polecenia expr. Przy czym trzeba pamiętać, żeby osobne tokeny (tzn. liczby i operatory arytmetyczne) były podawane w osobnych argumentach. Bieże się 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 same zalety oprócz jednego problemu, a mianowicie 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). Pierwsza zaleta to 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 jest traktowany wewnątrz podwójnych nawiasów jak zmienna. Nie musimy także dbać o używanie odstępów i backslashowania znaków specjalnych. Trzecia zaleta to bogatsza paleta operatorów arytmetycznych. Otóż wyrażenia arytmetyczne tutaj mogą zawierać dowolne operatory, które można znaleźć w języku C, np. inkrementacje/dekrementacje zmiennych (ID++, --ID), operacja 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 samo w sobie instrukcją. Najlepiej przyjżyjmy 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, to read wczyta to linię na zmienną o nazwie takiej jaka jest zawartość argumentu. Jeśli podamy więcej argumentów reprezentujących nazwy zmiennych, to 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óra 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. W tym celu służy 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:~$

Konwencja jest taka, że jeśli polecenie wykonało się z sukcesem, to kodem wyjścia jest 0, a jeśli w wyniku wykonania pojawiły się błędy lub polecenie skończyło się porażką, to 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ć sktypt 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 najprostrzej 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, to wynonywane są instrukcje między then, a fi. Jeśli kod wyjścia polecenia był niezerowy, to 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, to wykonywane są instrukcje1, w przeciwynym 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'a 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, to wykonywane są instrukcje_else.

Wyrażenia logiczne

Powstaje pytanie jak tworzyć polecenia, które sprawdzają jakieś sensowne warunki jak na przykład 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, to 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ńcuch 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 polecenie 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 ]

Take 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ć bardzo wygodny sposób pisania porównań, 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:~$

Najprostrzymy poleceniami, które zwracają prawde i fałsz, prostszymi nawet niż (( 1 )) i (( 0 )) są true i false, co przydaje się na przykład w pętlach, o czym za chwilę.