Musisz być baaaaardzo uparty, jeśli dotarłeś aż tutaj. Albo zdesperowany :]
Zaczniemy teraz konstruować takie cuda, że niech DOS się schowa ze swoimi
[*.bat]-ami.
Przede wszystkim ustalmy, co będziemy uważać za prawdę, a co za fałsz. Prawdą
będzie u nas 0, fałszem zaś cokolwiek innego. Te kryteria będziemy stosować do
kodu błędu zwróconego przez program. Wiesz chyba, że jeśli program zakończył
się w sposób normalny, to nie było błędu, zatem program zwraca 0. To chyba
logiczne, skąd takie kryteria, odwrotne do języka C?
Mając pojęcie, co jest prawdą, możemy przejść do wyrażeń warunkowych.
grep text *.txt > /dev/null || echo "Nie znalazlem nic" - takie
polecenie mówi: "wykonaj grep z parametrami, a potem w zależności
od kodu błędu wykonaj echo". A co to za zależność? Albo prawdą
będzie grep, albo wykonaj echo. Więc jeśli grep zwróci prawdę,
to echo się nie wykona. Odwrotnie do tego, grep text
*.txt > /dev/null && echo "Znalazlem maske" wykona
echo tylko wtedy, gdy grep zwróci
prawdę[*]. Takie coś jest wygodne
przy instalacji programów: ./configure && make && make
install. Jeśli wystąpi błąd przy konfiguracji, żaden z dwóch następnych
etapów się nie wykona. Jeśli wystąpi błąd podczas kompilacji, to kopiowanie
plików nie dojdzie do skutku. Zwróć uwagę, że oddzielenie poleceń średnikiem
nie daje takich możliwości.
Teraz coś bardziej skomplikowanego: programik test, częściej zwany
[. Przede wszystkim: test jest obecnie (czyli w bashu
2.05) poleceniem wbudowanym, więc stuprocentowe informacje możesz znaleźć
w pomocy wbudowanej (help test). Sam program test rzecz
jasna także się gdzieś znajduje (which test), ale for
performance został wbudowany w powłokę. Ma niewyszukaną składnię, co
w zasadzie wychodzi mu na plus. Może najpierw wymienię parę przełączników,
a potem pokażę składnię na przykładach, ogólna składnia nie byłaby tu zbyt
czytelna.
Jak to się je?
test -e /etc/passwd && echo "/etc/passwd istnieje" test -d /etc && echo "/etc jest katalogiem" test -r /etc/shadow && echo "/etc/shadow jest odczytywalny (to niedobrze!)" test 15 -gt 15 && echo "15 > 15" test $UID -eq 0 && echo "jestem root-em" # teraz porownania tekstowe test "$SHELL" ="" /bin/bash && echo "pracuje pod bashem" test "$PWD" !="" "$HOME" && echo "nie jestem w katalogu domowym" test -z "$EDITOR" && echo "zmienna \$EDITOR jest pusta" |
Ostatni przełącznik oznacza "czy ciąg jest pusty?" i ma swoje przeciwieństwo
-n. Ale gdyby nie miał, trzebaby użyć zaprzeczenia własnoręcznego:
test ! -d /etc/fstab && echo "/etc/fstab nie jest
katalogiem". Jak się pewnie domyślasz, istnieją także łączniki
oraz i lub, odpowiednio -a i -o, dzięki
czemu możesz łączyć kilka warunków jednocześnie. To teraz słówko o drugiej
nazwie programu: [. Ta forma wymaga na zakończenie wywołania
nawiasu kwadratowego zamykającego ] (jak to mawiał mój wykładowca
z programowania, "przedwias kwadratowy" wymaga "zawiasu kwadratowego" :-] ).
I to jedyna różnica w wywołaniu. Nawiasem mówiąc, ten nawias się nie pomyli
z nawiasem oznaczającym klasę znaków, bo każdy występuje w innym miejscu, a do
tego przy nawiasie od klasy znaków nie może być "niewyeskejpowanej" spacji,
która z kolei jest wymagana zaraz za przedwiasem od programu test.
Ach, i jeszcze przykładowe wywołanie: [ -z "" ] && echo "Pusty
parametr"
No to już mamy parę sposobów na testowanie warunków. Ale po takim warunku
następowała tylko jedna komenda, a nam przydałby się sposób nieco bardziej
uniwersalny (czy jakoś tak). Do tego posłuży nam if. Składnia jest
na tyle prosta, że może od razu przykład:
if [ "$USER" ="" "root" ]; then echo "Nieladnie uczyc sie z tutoriali jako root!" elif [ "$USER" ="" "dozzie" ]; then echo "Hej, masz takiego samego uzytkownika, jak ja!" else echo "Jakis nieciekawy komunikat" fi # inny sposob formatowania if [ "jakies cos" ="" "jakies cos" ] then echo "Nic nie znaczacy przyklad" fi # jeszcze inne wywolanie if ! grep root /etc/passwd > /dev/null ; then echo "W /etc/passwd nie ma wpisu dotyczacego konta root, dziwne..." else echo "W /etc/passwd jest wpis dotyczacy konta root" fi |
Jak widać z przykładów, nie trzeba stosować bloków elif ani
else, a nawet nie trzeba używać programu test w żadnej
formie. Możesz użyć dowolnego programu, a nawet gdy zwraca 0, to uznać, że to
fałsz (to mówi wykrzyknik z ostatniego przykładu). Zwracam uwagę na średniki
w odpowiednich miejscach, są ważne. I osobiście zachęcam do sposobu
formatowania z pierwszego (i trzeciego) przykładu, moim zdaniem jest
czytelniejszy (a na pewno stosują go ludzie piszący większe ilości skryptów,
z tego co widziałem). No i nie zapominaj o then ani
fi
Instrukcja warunkowa jest, więc przydałyby się pętelki. Proponuję zacząć od
pętli for, chyba częściej się ją stosuje.
for WARTOSC in cos ble uhu fiu hopsasa; do echo "Test petli 'for', wartosc \$WARTOSC ="" $WARTOSC" done |
Najlepiej osobiście wypróbuj, jaki to da efekt.
Teraz przypomnij sobie o rozwijaniu gwiazdek i pytajników przez bash. Jak
chcesz zrobić po jednej operacji na duuużej ilości plików w jednym katalogu,
możesz użyć gwiazdki:
for PLIK in *; do tar zcf "$PLIK.tar.gz" "$PLIK" done # to samo w jednej linijce, nadaje sie do wpisania # bezposrednio w linie polecen for PLIK in *; do tar zcf "$PLIK.tar.gz" "$PLIK"; done |
Znowu zwracam uwagę, gdzie są średniki.
Inne wywołanie pętli for:
for (( LICZBA ="" -10; LICZBA < 5; LICZBA++ )); do echo "$LICZBA" done |
To wypisze liczby od -10 do 4. Składnia identyczna jak w C, nawet operatory ++
i -- są i działają dokładnie tak samo.
No to pora na pętlę while:
while [ "$X" !="" "aaaaaaa" ]; do echo "$X" X="$X"a done |
Przyznaję, że trudno o przykład mniej przydatny w pisaniu skryptów, ale
najlepszy, jaki udało mi się wymyślić bez wykorzystania pewnych rzeczy jeszcze
nie przedstawionych przeze mnie.
Istnieje możliwość "ręcznego" wyjścia z pętli for
i while: instrukcja break. Jak uznasz, że pora zakończyć
pętlę, zwyczajnie wstawiasz break.
Istnieje jeszcze instrukcja bliźniacza do while: until.
Robi dokładnie to samo, tylko wykonuje się dopóki warunek jest fałszywy. To
tak, jak byś zaprzeczył warunek w while.
Do instrukcji if przydałaby się instrukcja, która mogłaby zamienić
spory bloczek if...elif...elif...elif...else...fi.
TEST="Jakas dziwna wartosc" case "$TEST" in cos) echo "Jest cos" ;; jeden | dwa | trzy | cztery | piec) echo "Liczba zapisana slownie" ;; [0-9]) echo "Cyferka" ;; [a-z]*) echo "Cos zaczynajace sie od litery" ;; ??) echo "Dwa znaczki" ;; *) echo "Cokolwiek innego" ;; esac |
Jeśli do $TEST wstawisz słówko cos, to wykona się pierwszy przypadek. Gdy wstawisz jeden lub dwa lub... , wykona się drugi. Gdy wstawisz 0...9, to trzeci. W przykładzie wykona się przypadek czwarty - klasa znaków w case jest case insensensitive, znaczy bez znaczenia jest wielkość liter. No i gwiazdka zachowuje się tak, jak przy rozwijaniu nazw plików. Pytajnik zresztą też, w piątym przypadku pasowane są dokładnie dwa znaczki. Goła gwiazdka, jeśli już powinna się pojawić, musi być na samym końcu. Zauważ, że cos pasuje do przypadków 1, 4 i 6, ale wykona się tylko pierwszy pasujący. Oczywiście w przypadkach obowiązkowy jest zawias okrągły na zakończenie maski i dwa średniki na zakończenie bloku instrukcji. Samo formatowanie to oczywiście moja propozycja, możesz wymyślić własne, ale to wydaje mi się rozsądne. I drobna uwaga: esac to czytane od tyłu case. Ogólnie, instrukcje wyboru kończą się napisaniem ich od końca, a pętle są ograniczone przez do...done
Wiesz już wystarczająco dużo, żeby pisać skomplikowane wyrażenia z palca, jak
na przykład for E in *.tar.gz; do tar zxf $E; done (rozpakowanie
wszystkich tarballi z katalogu, spróbuj to wykonać jednym poleceniem
tar :-) ). Pora na pisanie skryptów.
Nie wierzę, żebyś dotarł do tego miejsca nie wiedząc, co to jest skrypt, ale
zdarzały mi się dziwniejsze rzeczy. Skrypt to plik tekstowy z zestawem
poleceń, w zasadzie jedynie tym się różniący od programu napisanego na
przykład w C czy w Pascalu, że skryptu się nie kompiluje, dzięki czemu
banalnie łatwo można go modyfikować i ewentualna zmiana systemu operacyjnego
nie wymaga ponownego kompilowania skryptu. Jednak skrypty mają wady. Po
pierwsze, skrypt wymaga programu, który będzie mógł go wykonać, mianowicie
interpretera. Po drugie, skrypty zawsze będą wolniejsze od dobrze
napisanego i skompilowanego programu, bo interpreter najpierw musi sprawdzić,
czy nie ma błędów składniowych, a potem na bieżąco przetwarzać treść skryptu
na postać zrozumiałą dla procesora, co w przypadku pełnoprawnych programów
jest wykonywane tylko raz - przy kompilacji. Ale chyba zgodzisz się ze mną, że
do uruchomienia tara osiemnaście razy raczej nie ma sensu pisać
osobnego programu. Dlatego w przypadku, gdy w grę wchodzi rzadkie wykonywanie
nieskomplikowanych czynności, lepszy jest skrypt. Jak wynika z tego tu wyżej,
skrypt to nic innego, jak plik tekstowy (tyle, że z prawami wykonywania).
Potrzebny będzie ci zatem edytor tekstu - ja polecam
Vim-a. No, powymądrzałem się, więc
zasłużyłeś sobie na przejście do konkretów.
Przede wszystkim, skrypt w bashu powinien się zaczynać linijką
#!/bin/bash, czytaną przeze mnie jako hasz bang slasz bin slasz
basz (w oryginale hash bang slash bin slash bash). Wprawdzie nie
musi się tak zaczynać, ale byłoby miło. Jak zabraknie tej linijki, to shell
aktualnie używany będzie próbował to wykonać. A przecież ktoś może używać
csh/tcsh, które mają inną składnię, albo też
ksh/pdksh czy nawet zsh, które wprawdzie są
kompatybilne w składni z sh (z którym to również bash
jest kompatybilny), ale są pewne sztuczki dostępne tylko w bashu
(tak słyszałem :]). A ta linijka nie dość, że mówi na przykład edytorowi
tekstu, że to skrypt basha, to jeszcze każe wykonać dokładnie program
/bin/bash. I owszem, wiem, że ten system się sypie, jeśli bash
jest gdzie indziej, ale przeważnie jest w /bin, a nawet, gdy go
tam nie ma, to można zmienić tę linijkę na
odpowiednią[**]
I jeszcze drobna uwaga: jak każdy porządny język programowania, bash ma
komentarze. Zaczynają się od hasza (#). A teraz do roboty.
Skrypty od poleceń wydawanych bezpośrednio w powłoce w zasadzie się niczym nie
różnią. Jedno co, to skrypty są zapisane w pliku tekstowym i polecenia są
wykonywane jedno po drugim, więc musisz przewidzieć większość możliwych
sytuacji podczas wykonywania skryptu. I może jeszcze wskazówka odnośnie
formatowania: jeśli polecenie nie mieści ci się w linii, złam ją wstawiając na
końcu linii backslash (tak, jak to się robi w tekście w C/C++), a z uwagi na
to, że ilość spacji i tabulatorów nie robi bashowi różnicy (co najwyżej przy
wypisywaniu tekstu na ekran), więc złamaną linię możesz zgrabnie wciąć.
No to pora na przykładowy skrypt (nie wiem, czy uda mi się wymyślić coś
prostego i jednocześnie obrazującego, o co mi chodzi, ale spróbuję...)
#!/bin/bash # Skrypt do dzialania wymaga zainstalowanego ImageMagick. Prawdopodobnie # ten zestaw narzedzi masz zainstalowany, w razie czego odsylam na strone # autora: http://www.imagemagick.org # # Skrypt sluzy jako banalna nakladka na program import, dzieki ktoremu # mozna robic zrzuty ekranu - i to tez jest robione # najpierw robimy zrzut ekranu do pliku XPM import -window root /tmp/zrzut.xpm # teraz plik konwertujemy na PNG - uzyskamy lepsza kompresje niz # w przypadku zrzucania od razu do PNG convert /tmp/zrzut.xpm -interlace line -quality 100 /tmp/zrzut.png # usuwamy plik tymczasowy rm /tmp/zrzut.xpm # informujemy uzyszkodnika, gdzie jest plik ze zrzutem echo "Zrzut zostal wykonany, plik wynikowy: /tmp/zrzut.png" |
W skrypcie więcej komentarzy niż kodu :) Ale to nie jest wcale dziwne.
Obejrzyj sobie moje bardziej skomplikowane skrypty:
murder,
chlink,
remindme.
Przeważnie warto jest, żeby użyszkodnik podał skryptowi, co on chciałby
takiego dokładnie zrobić. Chodzi mi dokładnie o parametry z linii poleceń. Do
tych można się ładnie odwoływać przy pomocy zmiennych środowiskowych
$1, $2 ...
$9, a do samej nazwy pliku wykonywalnego
$0. To przypomina C, prawda? No to jeszcze
ciekawiej: wbudowane polecenie shift kasuje pierwszy parametr, a na
jego miejscu umieszcza drugi, na którego miejscu umieszcza trzeci... Przesuwa
parametry o jedno miejsce w lewo. A żeby było najciekawiej, to zmienna
$@ przechowuje wszystkie parametry (oprócz
$0), dzięki czemu łatwo napisać skrypt dodający do
listy parametrów jakiś dodatkowy na początku albo na końcu, zaś zmienna
$# przechowuje ilość parametrów (od zera wzwyż).
Może przykład wykorzystujący jeden parametr:
#!/bin/bash # Skrypt do dzialania wymaga zainstalowanego ImageMagick. Prawdopodobnie # ten zestaw narzedzi masz zainstalowany, w razie czego odsylam na strone # autora: http://www.imagemagick.org # # Skrypt sluzy jako banalna nakladka na program import, dzieki ktoremu # mozna robic zrzuty ekranu - i to tez jest robione # podstawowa pomoc programu, dobrze jest, gdy to jest zamieszczone if [ $# -eq 0 -o "$1" ="" "--help" -o "$1" ="" "-h" -o "$1" ="" "-?" ]; then echo "Uzycie:" $( basename "$0" ) "<plik ze zrzutem>" exit 0 fi # najpierw robimy zrzut ekranu do pliku XPM import -window root /tmp/zrzut.xpm # teraz plik konwertujemy na PNG - uzyskamy lepsza kompresje niz # w przypadku zrzucania od razu do PNG convert /tmp/zrzut.xpm -interlace line -quality 100 "$1" # usuwamy plik tymczasowy rm /tmp/zrzut.xpm # informujemy uzyszkodnika, gdzie jest plik ze zrzutem echo "Zrzut zostal wykonany, plik wynikowy: $1" |
Pojawiły się dwie nowe komendy: exit 0 i basename.
Jak sama nazwa wskazuje, exit kończy działanie skryptu, tutaj
z kodem błędu równym zero. Jeśli skrypt wykrzaczyłby się na czymś innym (na
przykład brak pliku), mógłbyś użyć na przykład exit 1.
basename z kolei jest jednym z dwóch programów do analizy ścieżki
(drugi to dirname). basename odcina całą ścieżkę
i wypisuje samą nazwę pliku z zadanego przez parametr wyrażenia. Nawias
$( ) jest ci już znany z poprzedniej strony - przechwytuje wyjście
programu.
Ciekawostka: w zmiennej $? znajduje się status
ostatniej komendy. Jest to użyteczne, gdy chcesz na przykład przekierować
wyjście jednej komendy potokiem do innej, a jednocześnie użyć całości jako
wyrażenia do ifa albo while'a. Ale bash nie byłby
bashem, gdyby nie było innej metody na takie coś. Nawiasy okrągłe pozwalają na
uruchomienie całej sekwencji poleceń w podpowłoce, czyli otwiera się nowy
bash, tam się wykonują polecenia, a ewentualne zmiany katalogu roboczego
i zmiennych środowiskowych nie wpływają na środowisko wewnątrz skryptu. Takim
zabiegiem można odpalać całe sekwencje poleceń warunkowych w tle:
[dozzie@hans ~]$ ( ./configure && make && echo "Koniec") & [dozzie@hans ~]$ ( grep test * > /dev/null && echo "OK" ) || echo "Zle" |
Fajne, co? Ale nasuwa się spostrzerzenie: skoro zmiana katalogu wewnątrz nawiasów okrągłych (czyli w subshellu) nie powoduje zmiany katalogu na zewnątrz, to czy zmiana katalogu wewnątrz skryptu zmieni katalog bieżący całej powłoki? Odpowiedź brzmi: nie, i nie powinna. Skrypt może dokonywać najdzikszych biegów po systemie plików, a nie chciałbyś się chyba nagle znaleźć w całkiem innym katalogu, nie wiedząc o tym?
No, przeszedłeś właśnie podstawowy kurs pisania skryptów w bashu. Podstawowy, bo w zasadzie nie omówiłem pewnego "sprytnego" rozwijania zmiennych i zabawy na nich (w tym tablic[!]). Ponadto jest wiele programów pozwalających na wysoce zaawansowaną modyfikację tekstu, że wspomnę tylko: ed i sed, awk, perl (choć ten sam nadaje się do pisania skryptów), ex, grep, cat, cut, head i tail i wielu, wielu innych - dalsza nauka dla ciebie to już nie tutoriale, ale manuale, strony info, no i skrypty, twoje własne i cudze. I niech Vim cię prowadzi, a Linus Torvalds pobłogosławi! :]]]]
[*] Chyba o tym nie wspominałem,
grep zwraca 0, jeśli znalazł zadaną maskę, 1 gdy maski nie znalazł,
a 2 w przypadku błędu (to działa zarówno w GNU grepie, jak i pod
Solarisem).
[**] W tej linijce po
#! może się znaleźć dowolna ścieżka bezwzględna, nawet
#!/bin/bc, co da skrypt kalkulatora bc (o którym
koniecznie poczytaj).