10.5 Bash
Do spisu tresci tematu 10
10.5 Interpretator polecen - Bash
Spis tresci
Wprowadzenie
Ogolne algorytmy dzialania Basha: glowna
procedura, inicjalizacja
wstepna, analiza parametrow, inicjalizacja
Parser
Obsluga wejscia: struktury
danych, tworzenie, otwieranie/zamykanie,
operacje
Wykonywanie polecen: struktury
danych, realizacja, FOR,
WHILE, IF, polecenie
proste, polecenie zewnetrzne
Obsluga zmiennych srodowiskowych: struktury
danych, inicjalizacja, funkcje
Obliczanie wyrazen arytmetycznych
Polecenia wbudowane: struktury
danych i obsluga, przyklady
Ciekawe rozwiazania
Bibliografia
Wprowadzenie
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.
Ogolne algorytmy dzialania Basha
Glowna procedura
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;
}
Przebieg inicjalizacji wstepnej
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;
}
Analiza parametrow wywolania
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;
}
Wlasciwa inicjalizacja
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;
}
Parser
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.
Obsluga wejscia
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.
Struktury danych
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;
Tworzenie strumienia
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));
}
Otwieranie / zamykanie strumienia
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));
}
Operacje na strumieniu
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);
}
Wykonywanie polecen
Struktury danych
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:
CONNECTION
struktura umozliwiajaca przechowywanie polecen polaczonych srednikiem
lub spojnikiem logicznym.
FUNCTION_DEF
tutaj przechowywane sa funkcje zdefiniowane przez uzytkownika; po wczytaniu
definicji funkcji Bash analizuje ja jak normalne polecenie i utworzona
w ten sposob strukture COMMAND przechowuje na zmiennej command
GROUP
struktura grupujaca polecenia w strukturze command umozliwiajaca
wspoluzytkowanie laczy (ang. pipes) przez grupe polecen
SIMPLE
podstawowa struktura, zawierajaca informacje o bezposrednio wykonywanych
poleceniach
Realizacja
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:
command jest tym samym, co parametr funkcji execute_command()
asynchronous rozne od zera oznacza wykonanie polecenia w tle
pipe_in i pipe_out to deskryptory plikow informujace
o wejsciu i wyjsciu; wartosc NO_PIPE oznacza, ze bedziemy uzywac
stdin/stdout
fds_to_close to deskryptory plikow, ktore maja zostac zamkniete
po wykonaniu fork()
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.
FOR
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);
}
WHILE i UNTIL
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);
}
IF
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));
}
Polecenie proste
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);
}
}
Polecenie zewnetrzne
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));
}
Obsluga zmiennych srodowiskowych
Struktury danych
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:
dynamic_value
funkcja zwracajaca wartosc zmiennej dynamicznej, tj. takiej, ktora
zmienia sie w czasie nie tylko w wyniku przypisania jej nowej wartosci;
przykladami takich zmiennych sa: $SECONDS i $RANDOM
assign_func
funkcja wywolywana, gdy wartosc zmiennej jest zmieniana w wyniku wywolania
funkcji bind_variable
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.
Inicjalizacja
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:
PWD - biezacy katalog; wartosc ustawiana domyslnie na katalog,
z ktorego wywolano Basha
PATH - przeszukiwana sciezka; domyslnie ustawiana na wartosc
zmiennej DEFAULT_PATH_VALUE
TERM - typ terminala; domyslnie "dumb"
PS1, PS2, PS4 - znaki zachety
HOSTTYPE, OSTYPE - typ komputera i systemu operacyjnego; domyslnie
ustawiane na analogiczne stale Basha
MAILCHECK - czestotliwosc sprawdzania poczty; domyslnie 60
sekund
SHLVL - poziom biezacego interpretatora (przydatny przy kolejnych
wywolaniach); domyslnie 0
PPID - pid rodzica interpretatora
HOME - katalog domowy uzytkownika
Kolejna czynnoscia jest ustawienie kilku zmiennych okreslajacych parametry
samego Basha. Ustawiane sa:
BASH - nazwa interpretatora wraz ze sciezka
BASH_VERSION - wersja Basha
HISTORY_SIZE - liczba pamietanych polecen
HISTFILE - nazwa pliku zawierajacego historie
Funkcje do obslugi srodowiska
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).
Obliczanie wyrazen arytmetycznych
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.
Polecenia wbudowane
Struktury danych i obsluga
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).
Przyklady
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;
}
Ciekawe rozwiazania
W tym rozdziale przedstawiamy odpowiedz na standardowe pytanie: jakie
sa najciekawsze rozwiazania i struktury danych?, przy czym przez najciekawsze
niekoniecznie rozumiemy warte stosowania.
abstrakcja wejscia
Wejscie jest calkiem niezle odizolowane od reszty programu - wiekszosc
funkcji majacych cos wspolnego z wejsciem dziala na uogolnionej strukturze
i wola ogolne funkcji obslugi.
obliczanie wyrazen
Sprytne uzycie kilkunastu funkcji obslugujacych wyrazenia o kolejnych
priorytetach oraz uzycie rekurencji pozwolily na obliczanie wartosci wyrazen
bez potrzeby tworzenia dodatkowych rozbudowanych struktur.
przenosnosc
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.
setjmp i longjmp
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).
struktura polecen
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.
tablice mieszajace
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.
zagadki
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.
Bibliografia
Pliki zrodlowe Basha (dostepne w formie spakowanej pod adresem ftp://ftp.icm.edu.pl./pub/gnu/bash)
Chet Ramey Bash: The GNU shell (plik dolaczony do dokumentacji Basha)
Autorzy: Przemyslaw Kubacki i Michal Rudolf
Wyszukiwarka
Podobne podstrony:
Powłoka BASHBASH Programowanie w powloceBash Prog Intro HOWTOBash Tutorial [EN]Bash Style Guide an Coding Standard [EN]bash help shhet shortcuts [EN]BASH Skryptybash org pl śmiesznebash (3)bashbash Wprowadzenie?shwpBash History Cheat Sheet [EN]BASH Bourne Again SHell2005 11 Discovery Scripts Bash Based Hardware Detection for Pci and Usb2005 11 Discovery Scripts Bash Based Hardware Detection for Pci and Usbwięcej podobnych podstron