Bash jest dzialajacym w srodowisku Unixowym interpretatorem polecen. Nazwa jest akronimem z ang. Bourne Again SHell (Steve Bourne to autor poprzednika Basha - programu sh).
Bash obsluguje standardowe konstrukcje sh, takie jak for, while,
case, czy if.. Umozliwia wykonywanie skryptow w taki sam
sposob jak zwyklych polecen. Dostepne sa wczesniej wpisane polecenia (tzw.
historia) i rozbudowane funkcje edycji linii polecen. Zawiera tez liczne
wbudowane polecenia (ang. builtins), pozwalajace m.in. na kontrole
procesow, obliczanie wartosci wyrazen arytmetycznych, definiowanie aliasow
itd.
Oto jak w najwiekszym skrocie wyglada dzialanie jednego egzemplarza interpretatora. Algorytm jest niemal identyczny dla dowolnego zrodla polecen, niezaleznie czy jest nim skrypt, argument wywolania, czy tez palce uzytkownika. Niewielkie modyfikacje obejmuja glownie sposob wczytywania i wykonywania polecen.
Ogolne algorytmy wiekszosci wyroznionych procedur opisane sa ponizej.
{ wykonaj wstepna inicjalizacje; obsluz parametry wywolania; inicjalizuj; wczytaj startowe pliki inicjalizacyjne; while (nie koniec) { if (wczytaj polecenie) if (jest polecenie do wykonania) { wykonaj polecenie; posprzataj; } else koniec; } zakoncz dzialanie; }
Oto schemat czynnosci podejmowanych przez interpretator zaraz po uruchomieniu. Wiekszosc podejmowanych tu akcji ma na celu dostosowanie sie Basha do otoczenia, w ktorym dziala.
{ zapamietaj id uzytkownika; if (uid uzytkownika != euid uzytkownika || gid uzytkownika != egid uzytkownika) ustaw tryb uprzywilejowany; if (zdefiniowano zmienna srodowiskowa POSIXLY_CORRECT lub POSIX_PEDANTIC) ustaw tryb zgodnosci ze standardem Posix; inicjalizuj lokalne zmienne robocze; if (nazwa wywolania jest "sh") ustaw tryb maksymalnej zgodnosci z sh; }
Po dokonaniu wstepnych czynnosci inicjalizacyjnych Bash przystepuje do analizy parametrow wywolania. Uwzgledniane sa zarowno pelne nazwy opcji, jak i flagi.
{ arg_index = 1; while (arg_index != argc && argv[arg_index] zaczyna sie od '-') { for (i=0; i<liczba dlugich nazw opcji; i++) if (argv[arg_index] rowny dlugiej nazwie[i]) if (typ opcji==Int) ustaw wartosc opcji na 1; else ustaw wartosc opcji na nastepny parametr; else koniec dlugich nazw opcji; arg_index++; } ustaw opcje jednoliterowe; if (ustawiono opcje wykonania polecenia podanego jako parametr) ustaw odpowiedni parametr jako zrodlo polecen; if (spelnione warunki interakcji) ustaw flage interakcji z uzytkownikiem; }
Tutaj wykonywana jest rzeczywista, ciezka praca inicjalizacyjna. Tworzone i/lub inicjalizowane sa struktury do przechowywania najrozniejszych informacji. Ostatecznie ustalane jest zrodlo wykonywanych pozniej polecen. Ustawiane sa zmienne srodowiskowe - np. dla znaku zachety (ang. prompt), badane mozliwosci edycyjne terminala (koncowki?) itd.
{ ustaw buforowanie stdout i stderr; inicjalizuj i sortuj tablice wbudowanych polecen; ustaw przechwytywanie i obsluge sygnalow; wypelnij strukture informacji o uzytkowniku i komputerze; wlacz funkcje obslugujaca tylde (katalog domowy) w sciezkach; inicjalizuj srodowisko; utworz struktury do przechowywania plikow; rozpocznij obsluge procesow; utworz ogolne struktury do wczytywania polecen; if (terminal == emacs) ustaw brak edycji linii; ustaw znaki zachety; wczytaj startowe pliki inicjalizacyjne; if (jest juz polecenie do wykonania) { wykonaj polecenie; zakoncz dzialanie; } if (tryb interakcji z uzytkownikiem) ustaw obsluge poczty; wczytaj historie; if (podano plik jako parametr) if (plik istnieje && jest skryptem) ustaw plik jako zrodlo polecen; else ustaw buforowane wejscie jako zrodlo polecen; przypisz reszte parametrow do zmiennych srodowiskowych $1...$n; }
Analiza wpisywanych przez uzytkownika polecen zajmuje sie parser wygenerowany na podstawie odpowiedniej gramatyki przez FLEXa i Bisona. Nie bedziemy sie tu zajmowac dokladna analiza parsera, zwlaszcza, ze ma on ponad 4000 linii, a spora jego czesc to automat skonczony i mnostwo wypelnionych liczbami tablic. Sami autorzy Basha przyznaja, ze parser jest bardzo duzy i malo efektywny, w zwiazku z czym zapowiadaja, ze w wolnej chwili napisza go recznie. Parser Basha jest na tyle skomplikowany, ze na jednej z poswieconych Unixowi konferencji Tom Duff stwierdzil: Nikt nie wie, jaka naprawde jest gramatyka Basha. Niewiele daje nawet analiza samych zrodel.
Parser jest slabo odgraniczony od reszty programu. Wywolywany jest w wielu miejscach, on sam rowniez wielokrotnie korzysta z procedur Basha (przykladami moze byc chociazby czesc operacji na napisach czy tez obliczanie wyrazen arytmetycznych). Komunikacja miedzy parserem a reszta interpretatora odbywa sie glownie przez zmienne globalne - wiekszosc funkcji parsera nie zwraca zadnych istotnych parametrow.
Glownym zadaniem parsera jest odczytanie polecen uzytkownika z wejscia i utworzenie z nich struktury COMMAND (opisanej ponizej). Na zmiennej global_command (typu COMMAND) parser zapamietuje strukture analizowanych polecen.
Wejscie dla parsera jest dostarczane przez Basha i jego obsluga jest niemal niezalezna od rzeczywistego zrodla danych. Po odpowiedniej dla danego typu wejscia inicjalizacji (with_input_from_stdin(), with_input_from_buffered_stream(), with_input_from_string()) dalej parser uzywa wylacznie ogolnych funkcji odczytu danych.
Do odczytu danych ze standardowego wejscia uzywana jest biblioteka readline
umozliwiajaca edycje linii danych i obslugujaca wiele klawiszy edycyjnych
w roznych standardach (m.in. emacs i vi), a takze zapewniajaca obsluge
historii.
Wczytywanie polecen Basha na najnizszym poziomie odbywa sie poprzez buforowany strumien z synchronizacja. Mechanizm ten moze dzialac w polaczeniu z plikami, mozna tez go uzywac do emulowania czytania znakow z wejscia i zwracania ich poprzez odpowiedniki getc() i ungetc(). Funkcje i struktury danych obslugujace strumienie zdefiniowane sa w plikach INPUT.C oraz INPUT.H.
Dwie istotne struktury to BUFFERED_STREAM oraz BASH_INPUT korzystajaca z unii INPUT_STREAM. Pierwsza jest nieco podobna do standardowej struktury FILE, ale ma swoje wlasne buforowanie i synchronizacje. Zdefiniowana jest rowniez tablica strumieni buffers[] skladajaca sie z BUFFERED_STREAM.
typedef struct BSTREAM { int b_fd; char *b_buffer; /* bufor przechowujacy otrzymane znaki */ int b_size; /* wielkosc bufora (maksymalnie 8kB) */ int b_used; /* ile bufora zajete */ int b_flag; /* flagi - B_EOF, B_ERROR lub B_UNBUF */ int b_inputp; /* wskaznik bufora - indeks */ } BUFFERED_STREAM;
Druga przechowuje dane potrzebne bezposrednio do obslugi wejscia. Zawiera m.in. dwa wskazniki do funkcji (typ Function to funkcja bez parametru zwracajaca znak) - pierwsza z nich ma sluzyc do pobierania znaku, druga do zwracania. Jak widac z ponizszej unii, miejscem, z ktorego Bash pobiera znaki, moze byc plik, zwykly strumien znakow lub wlasnie strumien buforowany.
typedef union { FILE *file; /* odczyt z pliku */ char *string; /* odczyt z ciagu znakow */ int buffered_fd; /* odczyt z buforowanego strumienia, zdefiniowanego wyzej */ } INPUT_STREAM; typedef struct { int type; char *name; INPUT_STREAM location; Function *getter; Function *ungetter; } BASH_INPUT;
Do utworzenia nowego strumienia na najnizszym poziomie sluzy funkcja make_buffered_stream(), ktora przydziela pamiec dla strumienia i inicjuje pola jego struktury. Nie powinna byc wywolywana z zewnatrz, jest raczej przeznaczona do wykorzystania przez inne funkcje. Jej argumentami sa: deskryptor dla tworzonego strumienia, bufor i jego rozmiar, przy czym pamiec dla bufora musi byc zaalokowana wczesniej. Funkcja zwraca wskaznik do utworzonej struktury.
static BUFFERED_STREAM* make_buffered_stream (int fd, char *buffer, int bufsize) { BUFFERED_STREAM *bp; zaalokuj pamiec; zniszcz stary i wstaw nowy element do buffers[fd]; bp->b_fd = fd; bp->b_buffer = buffer; bp->b_size = bufsize; bp->b_used = 0; bp->b_inputp = 0; bp->b_flag = 0; if (bufsize == 1) bp->b_flag |= B_UNBUFF; return (bp); }
Utworzone strumienie mozna kopiowac funkcja copy_buffered_stream() o nastepujacym naglowku:
static BUFFERED_STREAM * copy_buffered_stream (BUFFERED_STREAM *bp);
Przydziela ona tylko pamiec dla nowej struktury, kopiuje pola i zwraca wskaznik do nowo utworzonej kopii, albo NULL jezeli bp jest pustym wskaznikiem.
Mozliwe jest rowniez duplikowanie deskryptorow podobnie, jak robi to funkcja dup2(fd1, fd2). Sluzy do tego funkcja duplicate_buffered_stream(). Jezeli docelowy strumien istnieje, jest niszczony, a nastepnie do nowo utworzonego kopiowany jest strumien zrodlowy.
duplicate_buffered_stream (int fd1, int fd2) { if (fd1 == fd2) return 0; m = max (fd1, fd2); zniszcz stary i wstaw nowy element do buffers[fd]; if (strumien istnieje w tablicy) free_buffered_stream (strumien); buffers[fd2] = copy_buffered_stream (buffers[fd1]); if (buffers[fd2]) buffers[fd2]->b_fd = fd2; if (fd2 uzywane jako wejscie dla Basha) { /* w tablicy moze nie istniec taki element */ if (!buffers[fd2]) fd_to_buffered_stream (fd2); } return (fd2); }
Kolejna funkcja przyporzadkowuje deskryptorowi pliku strumien buforowany. Nazywa sie ona fd_to_buffered_stream() i w razie potrzeby tworzy oraz zwraca strumien dla zadanego deskryptora, albo NULL w przypadku niemoznosci utworzenia go.
BUFFERED_STREAM* fd_to_buffered_stream (int fd) { if (i-wezel dla fd nie istnieje) { close (fd); return ((BUFFERED_STREAM *)NULL); } if (nie mozna wykonac seek na strumieniu) rozmiar = 1; else ustaw rozmiar bufora; przydziel pamiec dla bufora wewnetrznego; return (make_buffered_stream (fd, bufor, rozmiar)); }
Wykorzystujac powyzsze funkcje mozna otworzyc strumien podobnie jak plik, podajac jego nazwe. Sluzy do tego funkcja open_buffered_stream(), zwracajaca wskaznik do strumienia:
BUFFERED_STREAM* open_buffered_stream (char *file) { fd = open (file, O_RDONLY); if (fd == -1) return ((BUFFERED_STREAM *)NULL); return (fd_to_buffered_stream (fd)); }
Otwiera ona plik (ktory musi wczesniej istniec) do czytania i tworzy strukture strumienia powiazana z deskryptorem tego pliku.
Z kolei do zamykania strumienia sluzy odpowiednik bibliotecznej funkcji close() funkcja o nazwie close_buffered_fd(), zwracajaca wynik wykonania close(), jezeli strumien dla danego deskryptora nie istnieje, a wynik otrzymany z wywolania close_buffered_stream() w przeciwnym przypadku.
int close_buffered_fd (int fd) { if (nie ma strumienia odpowiadajacego fd) return (close (fd)); return (close_buffered_stream (buffers[fd])); }
Funkcja close_buffered_stream() zwalnia pamiec przydzielona strumieniowi oraz zamyka zwiazany z nim deskryptor pliku:
int close_buffered_stream (BUFFERED_STREAM *bp) { if (!bp) return (0); fd = bp->b_fd; zwolnij pamiec zajeta przez strumien; return (close (fd)); }
Funkcja b_fill_buffer() czyta znaki az do zapelnienia bufora podanego jako parametr i zwraca pierwszy znak z bufora albo koniec pliku:
static int b_fill_buffer (BUFFERED_STREAM *bp) { do { bp->b_used = read (bp->b_fd, bp->b_buffer, bp->b_size); } while (bp->b_used < 0 && errno == EINTR); if (bp->b_used <= 0) { /* w buforze nic nie ma */ bp->b_buffer[0] = 0; if (bp->b_used == 0) /* nic nie przeczytano */ bp->b_flag |= B_EOF; else /* read() zwrocil blad */ bp->b_flag |= B_ERROR; return (EOF); } ustaw poczatek bufora; return (pierwszy znak z bufora); }
Do wczytania jednego znaku sluzy odpowiednik getc() - udajace funkcje dosc nieladnie zdefiniowane makro z parametrem bedacym strumieniem, ktore zwraca pierwszy znak z bufora i przesuwa wskaznik lub, jezeli bufor jest pusty, wywoluje funkcje b_fill_buffer():
#define bufstream_getc(bp) \ (bp->b_inputp == bp->b_used || !bp->b_used) \ ? b_fill_buffer (bp) \ : bp->b_buffer[bp->b_inputp++]
Istnieje oczywiscie rowniez odpowiednik ungetc() wykonujacy odwrotna operacje: wpisujacy znak z powrotem do bufora i przesuwajacy wskaznik bufora w druga strone. Jest to funkcja bufstream_ungetc(), ktorej parametry to znak do zwrocenia i strumien:
static int bufstream_ungetc(int c, BUFFERED_STREAM *bp) { if (c == EOF || bp->b_inputp == 0) return (EOF); bp->b_buffer[--bp->b_inputp] = c; return (c); }
Funkcja sync_buffered_stream() przesuwa w tyl wskaznik w pliku o deskryptorze bfd, aby zsynchronizowac jego pozycje w pliku z tym, co dotad zostalo przeczytane:
int sync_buffered_stream (int bfd) { BUFFERED_STREAM *bp; bp = buffers[bfd]; if (!bp) return (-1); chars_left = bp->b_used - bp->b_inputp; /* ile znakow do odczytania */ if (chars_left) /* o tyle cofany jest wskaznik w pliku */ lseek (bp->b_fd, -chars_left, SEEK_CUR); /* co oznacza, ze w buforze z powrotem nic nie ma */ bp->b_used = bp->b_inputp = 0; return (0); }
Uzywana przez Basha funkcja with_input_from_buffered_stream() wiaze wejscie z plikiem o deskryptorze bedacym parametrem, czytanie odbywa sie przez buforowany strumien. Wywoluje inicjujaca wejscie/wyjscie funkcje parsera init_yy_io() podajac jej w miejsce wskaznikow do funkcji pobierajacych i zwracajacych znak funkcje bedace nieco obudowanymi (nawiasami { } i inna nazwa) wywolaniami bufstream_getc() i bufstream_ungetc():
void with_input_from_buffered_stream (int bfd, char *name) { INPUT_STREAM location; location.buffered_fd = bfd; /* upewnij sie, ze strumien istnieje */ fd_to_buffered_stream (bfd); init_yy_io (buffered_getchar, buffered_ungetchar, st_bstream, name, location); }
Podstawowa struktura przechowujaca informacje o pojedynczym poleceniu jest struktura COMMAND. Jej definicja wyglada nastepujaco:
typedef struct command { enum command_type type; /* typ polecenia (por. tabela ponizej) */ int flags; /* dodatkowe flagi */ int line; /* numer linii, w ktorej rozpoczyna sie polecenie */ REDIRECT *redirects; /* przeadresowania dla niektorych polecen */ union { /* dodatkowe dane dla poszczegolnych polecen */ struct for_com *For; struct case_com *Case; struct while_com *While; struct if_com *If; struct connection *Connection; struct simple_com *Simple; struct function_def *Function_def; struct group_com *Group; struct select_com *Select; } value; } COMMAND;
Pole redirects to struktura zawierajaca informacje o przeadresowaniach. Okresla ona: typ przeadresowania (wejscie, wyjscie, wejscie/wyjscie, dolaczenie do pliku itd.), przeadresowywany deskryptor oraz nazwe pliku lub deskryptor, na ktory ma sie odbyc przeadresowanie.
Najciekawszym i najwazniejszym polem struktury COMMAND jest unia value, przechowujaca dane odpowiednie dla danego polecenia. Ponizsza tabela przedstawia wszystkie obslugiwane przez Basha rodzaje polecen i odpowiadajace im parametry.
Rodzaj polecenia |
Parametr |
Lista parametrow |
Polecenie |
CONNECTION | connector - polaczenie |
first, second - polaczone polecenia |
|
CASE | word -- warunek |
clauses - lista kolejnych warunkow i polecen do wykonania |
action (w clauses) - do wykonania gdy warunek spelniony |
FOR, SELECT | name - zmienna |
maplist - lista parametrow do podstawienia na zmienna |
action - do wykonania dla kazdego podstawienia |
IF | test - warunek |
true_case - gdy warunek spelniony, false_case - gdy nie spelniony |
|
WHILE | test - warunek |
action - do wykonywania dopoki warunek spelniony |
|
SIMPLE | words - argumenty dla funkcji exec |
||
FUNCTION_DEF | name - nazwa |
command - struktura typu COMMAND |
|
GROUP | command - struktura typu COMMAND |
Niektore z opisanych w tabeli typow polecen wymagaja krotkiego komentarza:
Sam proces wykonywania polecenia przebiega w nastepujacy sposob: po wczytaniu linii polecenia w petli reader_loop() wywolywana jest funkcja execute_command() z parametrem typu COMMAND. Funkcja ta nie jest zbyt skomplikowana: inicjalizuje zmienne, przydzielajac w razie potrzeby pamiec, a nastepnie wola funkcje execute_command_internal(), zapamietuje wartosc zwrocona przez nia i zwraca ja, zwolniwszy najpierw zaalokowana pamiec.
Funkcja execute_command_internal() wykonuje wiekszosc prac zwiazanych z ogolna obsluga wykonania polecen. Jej argumenty sa nastepujace:
Zwracana jest jedna z dwoch wartosci : EXECUTION_FAILURE lub EXECUTION_SUCCESS (ta druga rowniez po wykonaniu pustego polecenia).
Sam algorytm wyglada nastepujaco :
int execute_command_internal (COMMAND* command, int asynchronous, int pipe_in, int pipe_out, struct fd_bitmap *fds_to_close) { if (parametr command pusty lub wykonujemy break albo continue) return (EXECUTION_SUCCESS); if (polecenie ma byc jawnie wykonane przez kopie interpretatora albo stdin/stdout zostalo przeadresowane i poleceniem jest instrukcja for..., while... itp. badz grupa instrukcji) { make_child(); if (proces potomny) { ustaw obsluge sygnalow; wykonaj ewentualne przeadresowanie; zamknij deskryptory fds_to_close; if (polecenie jest poleceniem prostym) ustaw odpowiednie flagi; /* byc moze nie trzeba bedzie robic kolejnego forka */ exec_result = execute_command_internal (command, asynchronous, NO_PIPE, NO_PIPE, fds_to_close); exit (exec_result); } else { zamknij lacza pipe_in, pipe_out; if (jestesmy czescia lacza) return (EXECUTION_SUCCESS); if (nie ma lacza lub jestesmy ostatnim elementem) { czekaj na potomka; return (wartosc zwrocona przez potomka); } } } wykonaj ewentualne przeadresowanie; switch (typ polecenia) { case cm_for: exec_result = execute_for_command (command->value.For); break; case cm_case: exec_result = execute_case_command (command->value.Case); break; case cm_while: exec_result = execute_while_command (command->value.While); break; case cm_until: exec_result = execute_until_command (command->value.While); break; case cm_if: exec_result = execute_if_command (command->value.If); break; case cm_group: /* przypadek "{...}" */ if (wykonanie asynchroniczne) { ustaw flage jawnego wywolania kopii interpretatora; exec_result = execute_command_internal (command, 1, pipe_in, pipe_out, fds_to_close); } else exec_result = execute_command_internal (command->value.Group->command, asynchronous, pipe_in, pipe_out, fds_to_close); case cm_simple_command: if (byl potrzebny fork() do tego polecenia) czekaj na potomka; exec_result = execute_simple_command (command->value.Simple, pipe_in, pipe_out, asynchronous, fds_to_close); case cm_connection: switch (command->value.Connection->connector) { case '&': wykonaj konieczne przeadresowanie; /* pierwsze polecenie jawnie asynchroniczne */ exec_result = execute_command_internal (command->value.Connection->first, 1, pipe_in, pipe_out, fds_to_close); usun inf. o przeadresowaniu; exec_result = execute_command_internal (command->value.Connection->second, asynchronous, pipe_in, pipe_out, fds_to_close); case ';': execute_command (command->value.Connection->first); exec_result = execute_command_internal (command->value.Connection->second, asynchronous, pipe_in, pipe_out, fds_to_close); case '|': zainicjuj lacze dla procesow; execute_command_internal (command->value.Connection->first, asynchronous, prev, fildes[1], fd_bitmap); /* fildes[1] i prev to nowe deskryptory we/wy dla polecenia */ prev = fildes[0]; /* wykonaj to, co po prawej stronie lacza */ exec_result = execute_command_internal (command->value.Connection->second, asynchronous, prev, pipe_out, fds_to_close); case AND_AND: /* "&&" */ case OR_OR: /* "||" */ if (asynchronicznie) { /* tym razem wymuszane jest utworzenie kopii interpretatora */ exec_result = execute_command_internal (command, 1, pipe_in, pipe_out, fds_to_close); } exec_result = execute_command (command->value.Connection->first); if (exec_result == 0 dla || lub 1 dla &&) exec_result = execute_command (command->value.Connection->second); } } wykonaj porzadki w strukturach i deskryptorach; return (ostatnia wartosc exec_result); }
Jak widac, algorytm ten jest dosyc skomplikowany; tworcy Basha umiescili w nim kilka sprytnych rozwiazan, ktore wymagaja nieco dluzszego komentarza. Otoz po pierwsze tworzenie procesu potomnego realizowane jest przez specjalna funkcje make_child(), ktora tworzy oczywiscie proces potomny wywolujac fork(), ale przedtem wykonuje kilka innych czynnosci. Najpierw ustawia obsluge sygnalow SIGCHLD i SIGINT, potem wola funkcje making_children(), ta zas tworzy lacze, aby usekwencyjnic fork(). Potem nastepuje samo wywolanie fork() i rodzic ustawia potomkowi swoje sygnaly, przechwytuje od niego informacje o bledach i ustawia procesom potomnym te sama grupe, aby mogly korzystac z lacza. Na koniec dodaje utworzone procesy do tablicy dzialajacych procesow (job_array), ktora stanowi czesc aparatu Bashowego zarzadzania procesami. Obsluga konstrukcji skladniowych typu FOR, IF itp. odbywa sie w odpowiadajacych im funkcjach. Czesc z nich jest dosyc podobna i malo interesujaca, wiec ponizej nie opisujemy ich wszystkich.
Funkcja execute_for_command() wykonuje petle FOR przebiegajac kolejne wartosci zmiennej sterujacej i wykonujac dla kazdej z nich zawartosc petli.
Algorytm jest nastepujacy:
int execute_for_command (FOR_COM *for_command) { sprawdz poprawnosc nazwy zmiennej sterujacej; loop_level++; rozwin liste wartosci dla zmiennej sterujacej; while (lista niepusta) { przypisz wartosc z listy na zmienna sterujaca; retval = execute_command (for_command->action); sprawdz, czy nie trzeba wykonac "break" lub "continue"; wez nastepny element z listy; } if (zdefiniowana odpowiednia flaga) przywroc poprzednia wartosc zmiennej; loop_level--; return (retval); }
Polecenia WHILE i UNTIL obslugiwane sa w zasadzie jedna funkcja wolana przez execute_command_while() i execute_command_until(). Ta funkcja to execute_while_or_until (WHILE_COM *while_command, int type), gdzie drugi parametr to flaga: while albo until.
int execute_while_or_until (WHILE_COM *while_command, int type) { loop_level++; body_status = EXECUTION_SUCCESS; while (1) { return_value = execute_command (while_command->test); if (while i zwrocono wartosc falsz) break; if (until i zwrocono wartosc prawda) break; body_status = execute_command (while_command->action); sprawdz, czy nie trzeba wykonac "break" lub "continue"; } loop_level--; return (body_status); }
Obsluga polecenia IF...THEN...ELSE jest bardzo prosta:
execute_if_command (IF_COM *if_command) { return_value = execute_command (if_command->test); if (zwrocono wartosc prawda) return (execute_command (if_command->true_case)); else return (execute_command (if_command->false_case)); }
Godna uwagi jest funkcja execute_simple_command() wykonujaca polecenia proste, gdyz to ona dopiero moze wywolac jakies konkretne polecenie: wbudowane, z dysku lub zdefiniowana funkcje. Oto algorytm:
execute_simple_command (SIMPLE_COM *simple_command, int pipe_in, int pipe_out, int async, struct fd_bitmap *fds_to_close) { if (sa jakies polecenia) { if (polecenie zwiazane z kontrola zadan) { if (asynchronicznie) wykonaj w tle; else wykonaj pierwszoplanowo; return (wynik wykonania); } if (zadanie do wznowienia) { wznow zadanie; return (wynik wznowienia); } /* w takim razie polecenie wbudowane lub funkcja uzytkownika */ /* albo tez polecenie zewnetrzne */ if (funkcja albo polecenie wbudowane) { if (przeadresowanie lub asynchronicznie) { make_child(); if (proces potomny) wykonaj polecenie w kopii interpretatora; zwroc wartosc rodzicowi; else { zamknij lacza; return (zwrocona wartosc); } } else { wykonaj funkcje lub polecenie wbudowane; return (zwrocona wartosc); } } / * jest to polecenie zewnetrzne */ execute_disk_command(); return (zwrocona wartosc); } else if (konieczne przeadresowanie lub wykonanie asynchroniczne) { /* polecenie po rozwinieciu puste, wiec wykonujemy tylko przeadresowanie i konczymy */ make_child(); if (proces potomny) { wykonaj przeadresowanie; exit (EXECUTION_SUCCESS); } else { zamknij lacza; return (EXECUTION_SUCCESS); } } else { /* jesli polecenie po rozwinieciu puste, chcemy mimo to wykonac przeadresowanie, gdyz uzytkownik moze oczekiwac efektow ubocznych */ if (nie udalo sie wykonac przeadresowania) return (EXECUTION_FAILURE); else return (EXECUTION_SUCCESS); } }
Jezeli ostatecznie polecenie okazuje sie byc funkcja, to wolana jest znowu execute_command_internal(), ktora tym razem wykona zawartosc funkcji, natomiast wykonywanie polecen wbudowanych omowione jest w innym miejscu. Dlatego tu ograniczymy sie do opisania funkcji execute_disk_command(), ktorej zadaniem jest (prawie) ostateczne wykonanie polecenia :
static void execute_disk_command (WORD_LIST *words, REDIRECT *redirects, char *command_line, int pipe_in, int pipe_out, int async, struct fd_bitmap *fds_to_close, int nofork) /* nofork oznacza, ze nie ma lacz i wystarczy sam exec, bez dodatkowego fork() */ { if (nieustawiona zm. PATH i sciezka nie jest absolutna) znajdz polecenie w tablicy mieszajacej; if (nie znaleziono i sciezka nie jest absolutna) { wyszukaj polecenie w miejscach wskazanych przez PATH; if (znalezione) dodaj do tablicy mieszajacej; } if (nie trzeba robic fork() ani przeadresowan) pid = 0; else pid = make_child (polecenie, asynchronicznie); if (pid == 0) { ustaw obsluge sygnalow; wykonaj ewentualne przeadresowanie; przygotuj argumenty; zamknij deskryptory wymagajace zamkniecia; if (nie znaleziono wczesniej polecenia) { wypisz komunikat o bledzie; exit (kod bledu); } exit (shell_execve (polecenie, argumenty, srodowisko)) } else { zamknij lacza od strony rodzica; zwolnij pamiec zajeta przez polecenie; } }
Nawet w wypadku, gdy polecenia nie znaleziono, wykonywany jest w tej funkcji fork(). Ma to na celu przeadresowanie ewentualnych komunikatow o bledach.
Ostatnia funkcja wywolujaca juz bezposrednio
execve() jest shell_execve():
int shell_execve (char *command, char **args, char **env) { if (nazwa nie jest plikiem wykonywalnym) if (nazwa oznacza katalog) { wypisz komunikat o bledzie; return (kod bledu); } else blad wykonania; else { if (plik jest pusty) return (EXECUTION_SUCCESS); if (plik jest skryptem) return (execute_shell_script()); } za arg[0] podstaw nazwe interpretatora; execve (nazwa interpretatora, argumenty, srodowisko); }
Jako ciekawostke warto zauwazyc fakt, ze sprawdzenie, czy plik jest binarny, odbywa sie przez zbadanie jego pierwszych 30 znakow. Jesli znaki sa kodami ASCII, to jezeli dwoma pierwszymi znakami sa "#!", mamy do czynienia z wykonywalnym skryptem.
Ostatnia funkcja zwiazana z wykonywaniem
polecen to execute_shell_script(). Jej pierwszy i drugi argument
moze byc niezrozumialy - sa to kolejno linia pobrana wczesniej przy sprawdzaniu
rodzaju pliku w shell_execve() i jej
dlugosc, ktora nie moze przekroczyc 80 znakow. Format polecenia powinien
byc nastepujacy : "#! interpretator [argument]".
static int execute_shell_script (unsigned char *sample, int sample_len, char *command, char **args, char **env) { odczytaj nazwe interpretatora; utworz argumenty, arg[0] = nazwa interpretatora; return (shell_execve (nazwa interpretatora, argumenty, srodowisko)); }
Pojedyncze zmienne i funkcje zdefiniowane w srodowisku Basha przechowywane sa w strukturze variable, ktorej definicja wyglada nastepujaco:
typedef struct variable *DYNAMIC_FUNC (); typedef struct variable { char *name; /* Nazwa zmiennej lub funkcji. */ char *value; /* Wartosc zmiennej lub wartosc zwracana przez funkcje */ DYNAMIC_FUNC *dynamic_value; /* Funkcja obliczajaca wartosc dla zmiennych dynamicznych */ DYNAMIC_FUNC *assign_func; /* Funkcja wywolywana przy przypisaniu wartosci na zmienna */ int attributes; /* Atrybuty (eksportowana, niewidoczna itd.) */ int context; /* Zasieg zmiennej lub funkcji */ struct variable *prev_context; /* Wartosc w poprzednim zasiegu */ } SHELL_VAR;
Oto nieco szerszy opis znaczenia niektorych pol tej struktury:
Zmienne srodowiskowe przechowywane sa w tablicy mieszajacej z metoda lancuchowa (ang. hash with chaining). Tablice mieszajace uzywane sa zreszta do przechowywania wielu roznych struktur Basha.
Zmienne srodowiskowe sa inicjalizowane przy inicjalizacji samego Basha. Srodowisko pobierane jest z otoczenia przez funkcje main i jako jedyny parametr przekazywane funkcji initialize_shell_variables(char** env), ktora wykonuje inicjalizacje. Przekazywane srodowisko ma postac tablicy napisow postaci nazwa_zmiennej=wartosc lub nazwa_funkcji=() {definicja_funkcji}.
Inicjalizacja srodowiska rozpoczyna sie od utworzenia odpowiednich tablic mieszajacych (jednej dla zmiennych i jednej dla funkcji) i zdekodowania przekazanego srodowiska wedlug nastepujacego algorytmu:
{ for (i=0; i<liczba napisow w przekazanym srodowisku; i++) { rozdziel napis[i] na nazwe i wartosc; if (wartosc zawiera fragment "() {") { /* definicja funkcji */ przepisz nazwe i definicje do nowego napisu; wywolaj parser dla napisu tak, jak dla polecenia uzytkownika; if (funkcja o tej nazwie nie zostala zdefiniowana) zglos blad; } else przypisz wartosc do zmiennej nazwa } }
Nastepnie sprawdzane jest istnienie niektorych zmiennych srodowiskowych. Jesli nie sa zdefiniowane, Bash tworzy je ustawiajac im domyslne wartosci. Zmienne te to:
Kolejna czynnoscia jest ustawienie kilku zmiennych okreslajacych parametry samego Basha. Ustawiane sa:
Wiele funkcji do obslugi srodowiska jest bardzo podobnych do siebie i najprawdopodobniej zostaly zdefiniowane w kilku wersjach dla oszczedzenia autorom pisania kilku linijek w innych miejscach kodu, opisane wiec zostana tu tylko te najwazniejsze.
Do wyszukiwania zmiennych i funkcji sluzy caly zestaw funkcji zaczynajacych
sie od FIND_. Wszystkie one przeszukuja odpowiednia tablice mieszajaca,
sprawdzajac, czy jest w niej zmienna lub funkcja o poszukiwanej nazwie.
Prawie wszystkie funkcje FIND_ wywoluja funkcje FIND_VARIABLE
lub FIND_FUNCTION, zas wszystkie bez wyjatku posrednio lub
bezposrednio korzystaja z funkcji LOOKUP, ktora dostaje poszukiwana
nazwe, a zwraca odpowiedni element tablicy mieszajacej (lub NULL
gdy nazwy nie ma).
Tworzenie nowych zmiennych i zmienianie wartosci istniejacych tez opiera
sie na funkcji LOOKUP. Przy ustawianiu wartosci sprawdzane jest,
czy nazwa juz jest zdefiniowana, i wowczas modyfikowana jest odpowiednia
wartosc, zas w przeciwnym razie do tablicy mieszajacej dodawany jest odpowiedni
element. W wypadku tworzenia/zmieniania funkcji przekazywana wartoscia
jest struktura COMMAND - taka jak przy wykonywaniu polecen. Do
policzenia wartosci zmiennej wolana jest funkcja evalexp, obliczajaca
wartosc wyrazenia arytmetycznego i opisana szczegolowo ponizej.
Co ciekawe, przy kazdej zmiennej pamietane jest, ile razy byla ona znajdowana
w tablicy mieszajacej, informacja ta nie jest jednak nigdzie wykorzystywana
(mozna by np. na jej podstawie dynamicznie modyfikowac listy w tablicy
mieszajacej, przesuwajac czesciej uzywane zmienne na poczatek).
Funkcje sluzace do obliczania wartosci wyrazen zawarte sa w module EXPR.C. Zaimplementowano tam rekurencyjny parser, ktory jednoczesnie wykonuje samo obliczanie. Tworcy Basha tym razem nie skorzystali z Bisona i parser zostal napisany recznie.
Przy obliczaniu wartosci wyrazen Bash posluguje sie arytmetyka long
int bez sprawdzania nadmiaru. Obslugiwane sa nastepujace operatory,
uporzadkowane malejaco wedlug priorytetu:
-, + (jako operatory unarne)
!, ~
*, /, %
+, -
<<, >>
<=, >=, <, >
==, !=
&
^
|
&&
||
=
Oczywiscie podwyrazenia zawarte w nawiasach ( ) maja wyzszy priorytet
od wszystkich wymienionych operatorow. Obliczanie wartosci odbywa sie od
lewej, z wyjatkiem operatora przypisania "=". W tym przypadku,
tak jak w C, wartosc jest obliczana od prawej strony.
W pliku EXPR.C zdefiniowana jest nastepujaca struktura, sluzaca
do
przechowywania informacji o wyrazeniu :
typedef struct { int curtok, lasttok; /* biezacy i poprzedni leksem (ang. token) */ char *expression, *tp; /* wyrazenie i pozycja leksemu w jego tekscie */ int tokval; /* wartosc leksemu ... */ char *tokstr; /* ... oraz jego reprezentacja tekstowa */ } EXPR_CONTEXT;
Elementy typu EXPR_CONTEXT sa przechowywane na stosie, zdefiniowanym
tak :
static EXPR_CONTEXT **expr_stack;
Do operacji na stosie sluza funkcje pushexp()
i popexp(). Dwie zmienne informujace o polozeniu wyrazenia i wielkosci
stosu to :
static int expr_depth = 0;
static int expr_stack_size = 0;
Ograniczenie na glebokosc stosu jest standardowo ustawione na 10.
Kilka innych istotnych zmiennych globalnych, uzywanych dalej w algorytmach
:
static int curtok = 0; /* aktualny leksem */
static int lasttok = 0; /* poprzedni leksem */
static int tokval = 0; /* wartosc akt. leksemu */
Za samo obliczenie wartosci jest odpowiedzialna
funkcja evalexp (char* expr). Wywoluje ja (miedzy innymi) posrednio
poprzez funkcje sluzace do zastepowania i rozwijania ciagow tekstowych
parser wygenerowany przez Bisona. Jej ogolny algorytm wyglada nastepujaco:
long evalexp (char* expr) { if (blad w obsludze stosu) { wyczysc stos; zwolnij przydzielona pamiec; } pushexp(); /* za pierwszym razem zapamietywana jest losowa wartosc */ pobierz nastepny leksem; val = expassign(); if (zostal jeszcze leksem) wypisz komunikat o bledzie skladni; popexp(); return (val); }
Do pobierania leksemu sluzy funkcja readtok():
static void readtok() { usun biale znaki i wez nastepny znak z aktualnej pozycji; if (poczatek identyfikatora) { tokval = wartosc identyfikatora; lasttok = curtok; curtok = identyfikator; } else if (poczatek liczby) { tokval = wartosc liczbowa; lasttok = curtok; curtok = liczba; } else { /* w takim razie operator typu "==", "=", "+=" itp. */ odczytaj operator; lasttok = curtok; curtok = odpowiedni leksem dla odczytanego operatora; } }
Jak juz zostalo to powiedziane wczesniej, obliczanie wartosci wyrazenia odbywa sie w sposob rekurencyjny. evalexp() jest pierwsza funkcja w ciagu wywolan. Wola ona funkcje expassign(), ktora z kolei wchodzi glebiej korzystajac z explor(). Na kolejnych poziomach zaglebienia sa wywolywane funkcje odpowiadajace uporzadkowanym wedlug rosnacego priorytetu operatorom. Ich hierarchia wyglada dalej tak:
explor() (dla "||") ---> expland() ("&&") ---> expbor() ("|") - --> expbxor()("^") ---> expband() ("&") ---> exp5() ("==", "!=") ---> exp4() (">" itd.) ---> expshift() (">>", "<<") ---> exp3() ("+", "-") ---> exp2() ("*" itd.) ---> exp1() ("!", "~") ---> exp0() ("-", "+" unarne i nawiasy).
Z wyjatkiem expassign() i exp0() funkcje te sa bardzo podobne, dlatego oprocz tych wymienionych przed chwila opiszemy dokladnie tylko dwie z pozostalych - exp5() obslugujaca operatory porownania oraz exp1(), ktora obsluguje dwa operatory unarne: negacji i negacji bitowej. Przedstawiony mechanizm zostal zastosowany rowniez we wszystkich pozostalych funkcjach.
Istotna uwaga : przed wywolaniem funkcji bedacej nizej w hierarchii odczytywany jest leksem przy uzyciu readtok(). Dzieje sie tak, gdyz te funkcje zakladaja, ze aktualny leksem jest juz odczytanym pierwszym skladnikiem wyrazenia.
Na poczatek funkcja obslugujaca operatory przypisania
zwyklego oraz przypisan typu "+=", "*=" itd.:
static long expassign () { value = explor(); /* wartosc dla lewej strony */ if (aktualny leksem to operator "=" lub "op=") { if (poprzedni leksem != zmienna) komunikat o bledzie; if (operator typu "op=") { zapamietaj op; lvalue = value; } zapamietaj nazwe zmiennej, na ktora przypisujemy; readtok(); value = expassign(); /* wartosc prawej strony */ if (operator typu "op=") { switch (op) { case mnozenie: lvalue *= value; break; case dzielenie: lvalue /= value; break; case modulo: lvalue %= value; break; case plus: lvalue += value; break; case minus: lvalue -= value; break; case przes. bitowe w lewo: lvalue <<= value break; case przes. bitowe w prawo: lvalue >>= value break; case bitowy AND: lvalue &= value; break; case bitowy OR: lvalue |= value; break; default: komunikat o bledzie; } value = lvalue; } zamien value na string i zapamietaj; } return (value); }
Przy pierwszym wywolaniu expassign() funkcja explor() oblicza wartosc wyrazenia stojacego po lewej stronie ewentualnego przypisania i jezeli rzeczywiscie jest przypisanie, to tym wyrazeniem powinna byc zmienna. Dla prawej strony wolana jest rekurencyjnie funkcja expassign(), ktora znowu wywola na poczatku explor() i jesli nie ma przypisania postaci "a=b=c", to zwroci obliczona wartosc, ktora zostanie przypisana na nasza poczatkowa zmienna.
Oto obiecane wczesniej funkcje wolane pomiedzy expassign()
i exp0():
static long exp5 () { val1 = exp4 (); /* zejdz nizej */ while (aktualny leksem to operator "==" lub "!=") { readtok (); val2 = exp4 (); if (aktualny leksem to operator "==") val1 = (val1 == val2); else if (operator "!=") val1 = (val1 != val2); } return (val1); } static long exp1 () { if (aktualny leksem to operator "!") { readtok (); val = !exp1 (); } else if (aktualny leksem to operator "~") { readtok (); val = ~exp1 (); } else val = exp0 (); /* ta funkcja nie ma nic do roboty, zejdz nizej */ return (val); }
Na samym dole hierarchii wywolan znajduje sie funkcja exp0():
static long exp0 () { if (aktualny leksem to operator unarny "-") { readtok (); val = - exp0 (); } else if (aktualny leksem to operator "+") { readtok (); val = exp0 (); } else if (aktualny leksem to lewy nawias) { readtok (); val = expassign (); /* licz od poczatku dla wnetrza nawiasu */ if (aktualny leksem rozny od prawego nawiasu) komunikat o bledzie: ("missing `)'"); readtok (); /* pomijamy prawy nawias */ } else if ((aktualny leksem to liczba) || (aktualny leksem to zmienna)) { val = tokval; readtok (); } else /* blad w skladni wyrazenia */ komunikat o bledzie; return (val); }
Ta funkcja zwraca dla leksemu oznaczajacego pojedyncza zmienna albo liczbe jego wartosc, oprocz tego obsluguje tez operatory jednoargumentowe "+" i "-". Jezeli napotka nawias, to poniewaz nawias ma najwyzszy priorytet, rekurencyjnie liczy wartosc tego, co jest pomiedzy nawiasami i zwraca te wartosc.
Do przechowywania informacji o wbudowanych poleceniach sluzy struktura builtin, bardzo podobna do definicji funkcji uzytkownika (FUNCTION_DEF) w strukturze COMMAND:
struct builtin { char *name; /* Nazwa polecenia */ Function *function; /* Funkcja realizujaca polecenie */ int flags; /* Jedna z flag ponizej */ char **long_doc; /* Pelny opis polecenia */ char *short_doc; /* Krotki opis polecenia */ }; /* Flagi */ #define BUILTIN_ENABLED 0x1 /* Polecenie jest dostepne */ #define STATIC_BUILTIN 0x2 /* Polecenie nie ladowane dynamicznie */ #define SPECIAL_BUILTIN 0x4 /* Polecenie specjalne */
Polecenia wbudowane przechowywane sa w tablicy shell_builtins. Poniewaz ich liczba i nazwy nie zmieniaja sie w czasie dzialania Basha, mozna je posortowac przy inicjalizacji (o dziwo autorzy nie napisali wlasnej funkcji sortujacej - uzywaja standardowej bibliotecznej funkcji qsort()) i nastepnie wyszukiwac binarnie wedlug nazwy. Nie ma wiec potrzeby stosowania tablicy mieszajacej.
Obsluga polecen wbudowanych jest bardzo prosta - gdy trzeba wykonac jakies polecenie, na podstawie jego nazwy wyszukiwany jest adres odpowiedniej funkcji, ktora nastepnie jest wywolywana. Odpowiednie parametry dla niej sa czytane z wejscia i w razie potrzeby przeksztalcane na liczby (przeznaczone do tego funkcje - np. get_numeric_arg() lub read_octal() znajduja sie w pliku BUILTINS/COMMON.C).
HELP - wyswietl krotki opis wszystkich dostepnych w Bashu polecen pasujacych do podanego wzorca (lub wszystkich, gdy nie podano wzorca).
Algorytm help_builtin (WORD_LIST* wzorzec):
{ if (nie podano wzorca) for (i=0; i<liczba polecen wbudowanych; i++) { if (polecenie wbudowane[i] jest niedostepne) wypisz gwiazdke; wypisz polecenie wbudowane[i] i jego krotki opis; } else { wypisz kolejne slowa wzorca; while (wzorzec niepusty) { for (i=0; i<liczba polecen wbudowanych; i++) if (polecenie wbudowane[i] pasuje do aktualnego wzorca) { wypisz polecenie wbudowane[i] i jego krotki opis; wypisz pelny opis polecenia[i] } nastepny wzorzec; } } }
ENABLE - z opcja -n czyni podane polecenia Basha niedostepnymi,
bez opcji - udostepnia je.
Algorytm enable_builtin (WORD_LIST* argumenty):
{ odczytaj opcje; if (nie podano nazw polecen) wypisz polecenia (podano opcje -n) ? niedostepne : dostepne; else while (nazwa polecenia) { ustaw/wyczysc flage BUILTIN_ENABLED polecenia nazwa; if (blad) wypisz ("To nie jest polecenie wbudowane"); } }
: (DWUKROPEK) - polecenie nic nie robi i zwraca 0
Algorytm colon_builtin (char* ignorowane):
{ return 0; }
ECHO - wypisz podane argumenty na ekranie. Opcja -n blokuje wypisywanie
znaku konca linii, opcja -e powoduje interpretowanie znakow specjalnych
(takich jak \a, \n czy \b).
Algorytm echo_builtin (WORD_LIST* argumenty):
{ odczytaj opcje; if (bez opcji -e) while (argument) { wypisz argument znak po znaku ignorujac znaki specjalne; nastepny argument; } else wypisz liste argumentow; if (bez opcji -n) wypisz znak konca linii; }
W tym rozdziale przedstawiamy odpowiedz na standardowe pytanie: jakie sa najciekawsze rozwiazania i struktury danych?, przy czym przez najciekawsze niekoniecznie rozumiemy warte stosowania.
Wejscie jest calkiem niezle odizolowane od reszty programu - wiekszosc funkcji majacych cos wspolnego z wejsciem dziala na uogolnionej strukturze i wola ogolne funkcji obslugi.
Sprytne uzycie kilkunastu funkcji obslugujacych wyrazenia o kolejnych priorytetach oraz uzycie rekurencji pozwolily na obliczanie wartosci wyrazen bez potrzeby tworzenia dodatkowych rozbudowanych struktur.
Autorzy stosuja wiele metod, trickow, zaklec itd. by uczynic Basha naprawde przenosnym i niezaleznym od wersji Unixa, a nawet od systemu operacyjnyego. Badane sa m.in. kolejnosc bajtow w licznach wielobajtowych (ang. little-endian, big-endian), liczba i nazwy sygnalow, zgodnosc ze standardem POSIX, mozliwosci kontroli procesow, maksymalna dlugosc sciezki itd. itp. Wszystkie te informacje znajdowane sa automatycznie przez Basha, bez potrzeby jakiejkolwiek ingerencji uzytkownika.
Autorzy Basha namietnie stosuja te dwie instrukcje wszedzie gdzie tylko sie da. Cala obsluga bledow i nie tylko implementowana jest za pomoca skakania po calym kodzie funkcji (czesto majacej kilkaset linii). Dosc skutecznie utrudnia to analize kodu. Ale byc moze programowanie duzych programow w Linuxie bez goto jest niemozliwe (samo goto z etykietami tez oczywiscie wystepuje wielokrotnie).
Dosc elegancko przechowywana jest struktura polecen. Drzewo polecen, uzywane nie tylko do natychmiastowego wykonywania zadan uzytkownika, ale takze np. do przechowywania definicji funkcji, jest struktura elastyczna i przejrzysta.
Uzycie tablic mieszajacych do przechowywania wszystkich niemal wiekszych zestawow danych, ktore wymagaja czegos wiecej niz sekwencyjne przeszukiwanie jest godne pochwaly. Tablice mieszajace sa szybkie, implementacja nawet w linuxowym C dosc czytelna, zas rezygnacja ze struktur typu lista cykliczna polaczona z drzewem AVL, ktore i tak sa zazwyczaj przeszukiwane sekwencyjnie na wszelki wypadek niewatpliwie usprawnia program.
Aczkolwiek zagadki w rodzaju Czy ta funkcja powinna znajdowac sie
tutaj? albo Nie wierze, ze ten fragment jest kiedykolwiek wykonywany
sa wspolne dla calego kodu Linuxa, w Bashu wystepuja w duzym natezeniu.
Od czasu do czasu autorzy zaskakuja przyjetym rozwiazaniem: Otworz skrypt,
ale najpierw zmien nasz deskryptor na duzy losowy, z nadzieja, ze w skrypcie
nie znajdzie sie taki sam.