Podstawowym zadaniem Basha jest uruchamianie programów. W najprostszym przypadku nazwa programu i ewentualne argumenty są wpisywane przez użytkownika w linii poleceń (np. w okienku terminala). Bardziej złożone zadania można wykonać za pomocą skryptów, czyli ciągów poleceń zapisanych w pliku tekstowym. Oprócz prostego uruchomienia programu z podanymi argumentami, w Bashu można posługiwać się także
- Strukturowami kontrolnym (pętle i instrukcje warunkowe).
- Zmiennymi środowiskowymi, które pełnią w skryptach taką samą rolę, jak zmienne w dowolnym innym języku programowania. Ich wartości są także dostępne dla uruchamianych programów. Zmienne ustawiane w plikach startowych (np. PATH, JAVA_HOME itp) stanowią też prosty mechanizm konfiguracji systemu.
- Przekierowaniami - poprzez odpowiednią manipulację deskryptorami plików w uruchmanianych procesach, bash może m.in. połączyć wyjścia programu z wejściem innego, albo zapisać je do pliku czy zmiennej.
- Innymi mechanizmami, na których omówienie brakuje tu miejsca.
Ostrzeżenie
Bash jest najbardziej popularnym, lecz nie jedynym interpreterm linii poleceń. Składnia stosowana w innych interpreterach może się znacząco różnić od tej opisanej w poniższych notatkach.
Bash udostępnia uruchomionym programom wartości argumentów z linii polecenia. Jako pierwszy argument (o indeksie 0) przekazywana jest też ścieżka do pliku wykonywalnego z programem (niestety, nie musi to być pełna ścieźka). Sposób odczytania wymienionych wartości ilustruje poniższy program.
program ShowParameters;
{$mode objfpc}{$H+}
var
ParameterIndex : Integer;
Parameter : String;
begin
WriteLn('Number of parameters: ', ParamCount);
WriteLn('Program name: ', ParamStr(0));
for ParameterIndex := 1 to ParamCount do
begin
Parameter := ParamStr(ParameterIndex);
WriteLn('Parameter ', ParameterIndex, ' = ', Parameter);
end
end.
Przykład uruchomienia programu
$ ./showparameters a b c d
Program name: /home/user/showparameters
Parameter 1 = a
Parameter 2 = b
Parameter 3 = c
Parameter 4 = d
Skrypty to po prostu ciągi poleceń (odseparowanych znakami nowej linii lub średnikami). Przy uruchamianiu skryptu również można podać argumenty. W kodzie można uzyskać do nich dostęp przez specjalne zmienne $@ (wszystkie argumenty w formie napisu) oraz “$0, $1, $2, ...`` (dostęp do konkretnego argumentu). Czasami przydaje się też komenda shift, która przesuwa wszystkie argumenty o jeden indeks wstecz (czyli np. $3 będzie się po niej odnosić do czwartego argumentu).
Przykładowy skrypt wypisujący argumenty
#!/bin/bash
echo Script = $0
echo Arguments = $@
Pierwsza linia skryptu to nazwa interpretera. Z punktu widzenia Basha jest to po prostu komentarz. Przy próbie załadowania pliku zaczynającego się od znaków #! w charakterze programu system zinterpretuje pierwszą linię jako nazwę interpretera, uruchomi go i przekaże nazwę pliku jako parametr.
Ostrzeżenie
W przykładach dostępnych w internecie skrypty często zaczyna się od #!/bin/sh. Może to jednak prowadzić do problemów - na przykład na maszynie students plik /bin/sh uruchamia interpreter ksh, którego składnia jest istotnie różna od bashowej.
Skrypt możemy uruchomić poprzez podanie go jako argument do programu bash
$ bash script.sh 1 2 3 4
Script = script.sh
Arguments = 1 2 3 4
Dzięki omówionemu wcześniej mechanizmowi podawnia interpretera w pierwszej linii pliku, możemy też skrypt uruchomić bezpośrednio, jak każdy program. Wymaga to jednak zmiany uprawnień do pliku, w celu zezwolenia na jego wykonywanie. Służy do tego komenda chmod, jak w poniższym przykładzie.
$ chmod +x script.sh $ ./script.sh 1 2 3 4 Script = ./script.sh Arguments = 1 2 3 4
Niekiedy wartość argumentu, którą chcemy przekazać, zawiera spacje bądź inne znaki, które są w jakiś sposób intepretowane przez basha. Mamy wtedy kilka możliwości
Wstawiony przed dowolnym znakiem spowoduje, że bash zignoruje specjalne znaczenie owego znaku i potraktuje go jak zwykłą literę. Wyjątkiem jest znak końca wiersza, który zostanie zignorowany całkowicie.
$ ./showparameters Backslash\ \(\\\)\ \e\x\a\m\p\l\e
Program name: /home/user/showparameters
Parameter 1 = Backslash (\) example
Jeśli ciąg znaków otoczymy apostrofami, bash zignoruje wszelkie zawarte w nim znaki specjalne (w tym backslash).
$ ./showparameters 'This is a single parameter \'
Program name: /home/user/showparameters
Parameter 1 = This is a single parameter \
Wzięcie napisu w cudzysłów również zatrzyma interpretację większości znaków specjalnych. Są jednak wyjątki: $, `, ! i \. Backslash postawiony przed jednym z tych znaków, lub przed znakiem ”, działa w sposób opisany powyżej. Przed innymi znakami nie ma specjalnego znaczenia.
$ ./showparameters "Parameter in quotes (\"), with \$, \\, and \` signs."
Program name: /home/user/showparameters
Parameter 1 = Parameter in quotes ("), with $, \, and ` signs.
$ echo "Backslash before normal character: \x\y\z"
Backslash before normal character: \x\y\z
Parametrami programu często są nazwy plików. Bash pozwala ciągi takich nazw opisywać za pomocą wzorców, czyli napisów zawierająych znaki ‘*’ i ‘?’. Gwiazdka pasuje do dowolnego (być może pustego) ciągu znaków, a ? do dokładnie jednego znaku.
Ostrzeżenie
Jeśli żaden plik nie pasuje do wzorca, bash nie wykona żadnego podstawienia (w szczególności nie zastąpi wzorca pustym ciągiem nazw).
$ echo *.txt
file.txt file1.txt file2.txt file3.txt grades.txt
$ echo file?.*
file1.txt file2.txt file3.txt file4.pas
$ echo no_file*
no_file*
Zmiennymi w Bashu można się posługiwać podobnie, jak w innych językach programowania. Przypisania ma postać ZMIENNA=wartość (trzeba tu uważać na spacje). W dalszej części skryptu mozna się odnosić do wartości zmiennej pisząc ${ZMIENNA} lub $ZMIENNA. Jeśli zmienna ma być widoczna dla uruchamianych programów, trzeba posłużyć się komendą export. Można ją podać przed przypisaniem, jak w poniższym przykładzie, lub w dowolnym innym miejscu napisać export ZMIENNA.
$ export MY_VAR=something
$ echo $MY_VAR
something
Program w Pascalu może uzyskać dostęp do wartości zmiennych w sposób przedstawiony w poniższym przykładzie.
program Env;
uses SysUtils;
{$mode objfpc}{$H+}
var
VarIndex : Integer;
begin
WriteLn('Program search path: ' + GetEnvironmentVariable('PATH'));
for VarIndex := 0 to GetEnvironmentVariableCount - 1 do
WriteLn(GetEnvironmentString(VarIndex));
end.
Przykład uruchomienia
$ export MY_VAR=something
$ ./env
Program search path: /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
HOSTNAME=duch
XDG_MENU_PREFIX=applnk-
TERM=xterm
SHELL=/bin/bash
...
MY_VAR=something
...
Uwaga
env to standardowe polecenie, dostępne w systemach unixowych, które może wypisać wartości i nazwy wszystkich zmiennych środowiskowych lub uruchomić zadany program w zmienionym środowisku.
Do zilustrowania mechanizmów przekierowywania wejścia i wyjścia procesów posłuży poniższy program. Wczytuje on jedną linię ze standardowego wejścia (deskryptor 0) i kopiuje ją na standardowe wyjście (deskryptor 1) oraz do standardowego strumienia diagnostycznego (stderr, deskryptor 2). Przy okazji możemy zobaczyć, jak się do wymienionych strumieni odnosić z poziomu Pascala.
program IO;
{$mode objfpc}{$H+}
var
Msg : String;
begin
ReadLn(Input, Msg);
WriteLn(Output, 'STDOUT: ' + Msg);
WriteLn(ErrOutput, 'STDERR: ' + Msg);
end.
Wyjście programu możemy zapisać do pliku lub przekazać na wejście innego programu. W poniższym przykładzie
- Komenda echo (która wypisuje swoje argumenty na standardowe wyjścia) jest uruchamiana, a jej wyjścia trafia na wejście programu io, opisanego powyżej.
- Standardowe wyjście z io jest zapisywane do pliku.
- Zawartość strumeinai diagnostycznego trafia do innego pliku.
- Komenda cat czyta plik i wypisuje jego zawartość na standardowe wyjście.
$ echo 42 | ./io > io.out 2> io.err
$ cat io.out
STDOUT: 42
$ cat io.err
STDERR: 42
Możemy też przekierować jeden deskryptor na drugi - na przykład połączyć zawartość stderr z stdout (czyli deskryptorem numer 1).
$ echo 42 | ./io > output.txt 2>&1
$ cat output.txt
STDOUT: 42
STDERR: 42
Jeśli wyjście programu chcemy dalej przetwarzać w naszym skrypcie, możemu użyć notacji $(command ...), jak w poniższym przykładzie
$ cp $(find -name '*.pas' -type f) $destdir
Nash wykona polecenie wewnątrz $(...), przeczyta jego standardowe wyjście i wstawi je w miejsce całego wyrażenia.
Uwaga
Komenda find, zastosowana w powyższym przykładzie, służy do znajdowania pliów bądź katalogów spełniających zadane kryteria. W tym przypadku znajdzie i wypisze wszystkie pliki (ale nie katalogi) o rozszerzeniu *.pas umieszczone w bieżącym katalogu bądź jego podkatalogach.
Czasami skrypt musi wyliczyć wartość jakiegoś wyrażenia arytmetycznego. Można się w tym celu posłużyć komendą expr, która intepretuje swoje argumenty jako wyrażenie i wypisuje jego wartość. Trzeba jednak pamiętać o oddzieleniu kolejnych symboli spacjami oraz powstrzymaniu Basha przed zinterpretowaniem specjalnych znaków, takich jak * czy nawiasy.
Wygodniej jest posłużyć się notacją ((...)) lub $((...)). Jak widać na poniższym przykładzie, pozwala ona zapisać wyrażenie w bardziej przejrzysty sposób. Nie trzeba pisać $ przed nazwą zmiennej ani ochraniać znaków specjalnych. ((...)) wypisuje wartość wyrażenia na standardowe wyjście, zaś $((..)) to połączenia $(...) z ((...)) - wartość jest podstawiana w skrypcie w miejsce wyrażenia.
$ expr 6 \* \( 1 + 2 \* 3 \)
42
$ ((x=6 * (2 + 2 * 2), x+=6 ))
$ echo $x
42
$ echo $((6 * (1 + 2 * 3)))
42
Podobnie jak inne jezyki programowania, Bash zawiera instrukcję warunkową. Ma ona postać zilustrowaną poniższym przykładem
if [ -f something.pas ]
then
fpc something.pas
else
echo "Source file not found\!"
fi
Pewnego wyjaśnienie wymaga użyty tutaj warunek ([ -f something.pas ]). W ogołnym przypadku warunkiem jest dowolne wywołanie programu (z argumentami). Warunek jest uważany za spełniony, jeśli kod wyjścia programu jest równy 0. Notacja [...] to skrót dla polecenia test, które pozwala sprawdzać zachodzenie podanych warunków. W szczególności test -f something.pas zwróci 0, jeśli istnieje w bieżącym katalogu plik (ale nie np. podkatalog) o nazwie something.pas.
Oprócz instukcji warunkowych w Bashu są też pętle. Pętla for ZMIENNA in WARTOSCI pozwala iterować po liście wartości oddzielonych spacjami (jak argumenty polecenia). Ilustruje to poniższy przykład.
for file in *.pas
do
fpc $file
done
Lista wartości może być wyjściem z innej komendy, jak w poniższym przykładzie (polecenie seq wypisuje wartości z zadanego zakresu).
for i in $(seq 1 10)
do
./program < test$i.in > test$i.out
done
Warto zawuażyć, że jeśli w katalogu nie będzie żadnego pliku z rozszerzeniem .pas, w pierwszym przykładzie wykona się komenda fpc '*.pas'. Pliki ze spacjami w nazwie również spowodują problemy (for uzna taki plik za dwie różne wartości). Do iterowania po plikach bezpieczniej jest użyć kombinacji poleceń find i xargs, jak w poniższym przykładzie
find -name '*.pas' -type f -print0 | xargs -0 -I file -P 10 fpc file
Wspomniana wcześniej komenda find znajdzie i wypisze ścieżki plików *.pas. Argument -print0 sprawi, że zamiast białych znaków do oddzielenia kolejnych nazw zostanie użyty znak o kodzie równym 0.
Polecenie xargs służy do uruchamiania programów z listą argumentów wczytaną ze standardowego wejścia. Dodatkowo pozwala uniknąć problemów ze zbyt długimi listami argumentów - na przykład wywołanie rm *.tmp w katalogu zawierającym miliony plików *.tmp mogłoby spowodować błąd Basha. xargs potrafi podzielić takie długie listy na krótsze (i wykonać zadane polecenie kilka razy).
W omawianym przykładzie argumenty xargs mają następujące znaczenie
- -0 mówi, że argumenty na wejściu będą oddzielone znakiem o kodzie zero, a nie białymi znakami. To pozwala uniknąc problemów ze spacjami, apostrofami i innymi kłopotliwymi znakami w nazwach plików.
- -I file - w treści komendy, którą ma wywołać xargs, trzeba jakoś odwoływać się do wartości bieżącego argumentu. Domyślnie służą do tego znaki {}, ale opcja -I pozwala to zmienić.
- -P 10 każe xargs wykonać komendy równolegle, używając nie więcej niż dziesięciu procesów.
- Pozostałe argumenty to treść komendy - z tym, że file zostanie zamienione na nazwy kolejnych plików (zgodnie z opcją -I, opisaną powyżej).
Błąd
Chwilowo tylko przykład...
MAIN=program.pas
UNITS=firstmodule.pas secondmodule.pas
TESTS=test1.in test2.in test3.in
FPC=fpc
FPCFLAGS=-gl
EXE=
PPU_FILES=$(UNITS:.pas=.ppu)
OBJ_FILES=$(UNITS:.pas=.o)
TESTS_OUT=$(TESTS:.in=.out)
TESTS_EXPECTED=$(TESTS:.in=.expected)
TESTS_RESULT=$(TESTS:.in=.result)
MAIN_EXE=$(MAIN:.pas=$(EXE))
%.ppu %.o : %.pas
$(FPC) $(FPCFLAGS) $<
%.out : %.in $(MAIN_EXE) %.expected
./$(MAIN_EXE) < $< > $@
$(MAIN_EXE) : $(MAIN) $(PPU_FILES)
$(FPC) -o$@ $(FPCFLAGS) $<
firstmodule.ppu : secondmodule.ppu
.PHONY: clean tests $(TESTS_RESULT)
clean:
-rm $(PPU_FILES) $(OBJ_FILES) $(TESTS_OUT)
test: $(MAIN_EXE) $(TESTS_RESULT)
$(TESTS_RESULT) : %.result : %.out %.expected
@echo -n $(<:.result=) ' '
-@if diff $^ ; then echo OK; else echo FAILED; fi