Lab 1 - Bash i make — IPP 2011/12

Spis treści

Poprzedni temat

IPP 2011/12

Następny temat

Lab 2 - Valgrind + styl kodowania

Lab 1 - Bash i make

Bash

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.

Programy i argumenty

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

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

Cudzysłowy, apostrofy itp.

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

Backslash

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

Apostrof

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 \

Cudzysłów

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

Wzorce

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*

Zmienne

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.

Przekierowania

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.

Wyrażenia

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

Instrukcje kontrolne

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

Make

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