Skomplikowańsze wyrażenia w powłoce

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).


Poprzednia część kursu
Jadłospis