Командный интерпретатор smallsh
В этом разделе создается простой командный интерпретатор smallsh. Этот пример имеет два достоинства. Первое состоит в том, что он развивает понятия, введенные в этой главе. Второе – в том, что подтверждается отсутствие в стандартных командах и утилитах UNIX чего-то особенного. В частности, пример показывает, что оболочка является обычной программой, которая запускается при входе в систему.
Наши требования к программе smallsh просты: она должна транслировать и выполнять команды – на переднем плане и в фоновом режиме – а также обрабатывать строки, состоящие из нескольких команд, разделенных точкой с запятой. Другие средства, такие как перенаправление ввода/вывода и раскрытие имен файлов, могут быть добавлены позднее.
Основная логика понятна:
while не встретится EOF do
begin
получить строку команд от пользователя
оттранслировать аргументы и выполнить
ожидать возврата из дочернего процесса
end;
Дадим имя userin функции, выполняющей «получение командной строки от пользователя». Эта функция должна выводить приглашение, а затем ожидать ввода строки с клавиатуры и помещать введенные символы в буфер программы. Функция userin реализована следующим образом:
uses stdio,linux;
(* Заголовочный файл для примера *)
{$i smallsh.inc}
(* Буферы программы и рабочие указатели *)
var
inpbuf:array [0..MAXBUF-1] of char;
tokbuf:array [0..2*MAXBUF-1] of char;
const
ptr:pchar=@inpbuf[0];
tok:pchar=@tokbuf[0];
(* Вывести приглашение и считать строку *)
function userin(p:pchar):integer;
var
c, count:integer;
begin
(* Инициализация для следующих процедур *)
ptr := inpbuf;
tok := tokbuf;
(* Вывести приглашение *)
write(p);
count := 0;
while true do
begin
c := getchar;
if c = EOF then
begin
userin:=EOF;
exit;
end;
if count < MAXBUF then
begin
inpbuf[count] := char(c);
inc(count);
end;
if (c = $a) and (count < MAXBUF) then
begin
inpbuf[count] := #0;
userin:=count;
exit;
end;
(* Если строка слишком длинная, начать снова *)
if c = $a then
begin
writeln ('smallsh: слишком длинная входная строка');
count := 0;
write (p);
end;
end;
end;
Некоторые детали инициализации можно пока не рассматривать. Главное, что функция userin вначале выводит приглашение ввода команды (передаваемое в качестве параметра), а затем считывает ввод пользователя по одному символу до тех пор, пока не встретится символ перевода строки или конец файла (последний случай обозначается символом
EOF).
Функция getchar содержится в стандартной библиотеке ввода/вывода. Она считывает один символ из стандартного ввода программы, который обычно соответствует клавиатуре. Функция userin помещает каждый новый символ (если это возможно) в массив символов inpbuf. После своего завершения функция userin возвращает либо число считанных символов, либо EOF, обозначающий конец файла. Обратите внимание, что символы перевода строки не отбрасываются, а добавляются в массив inpbuf.
Заголовочный файл smallsh.inc, упоминаемый в функции userin, содержит определения для некоторых полезных постоянных (например,
MAXBUF). В действительности файл содержит следующее:
(* smallsh.inc - определения для интерпретатора smallsh *)
{ifndef SMALL_H}
{define SMALL_H}
const
EOL=1; (* конец строки *)
ARG=2; (* обычные аргументы *)
AMPERSAND=3; (* символ '&' *)
SEMICOLON=4; (* точка с запятой *)
MAXARG=512; (* макс. число аргументов *)
MAXBUF=512; (* макс. длина строки ввода *)
FOREGROUND=0; (* выполнение на переднем плане *)
BACKGROUND=1; (* фоновое выполнение *)
{endif} (* SMALL_H *)
Другие постоянные, не упомянутые в функции userin, встретятся в следующих процедурах.
Рассмотрим следующую процедуру, gettok. Она выделяет лексемы (tokens) из командной строки, созданной функцией userin. (Лексема является минимальной единицей языка, например, имя или аргумент команды.) Процедура gettok вызывается следующим образом:
toktype := gettok(@tptr);
Целочисленная переменная toktype будет содержать значение, обозначающее тип лексемы. Диапазон возможных значений берется из файла smallsh.inc и включает символы EOL (конец строки),
SEMICOLON и так далее. Переменная tptr является символьным указателем, который будет указывать на саму лексему после вызова gettok. Так как процедура gettok сама выделяет пространство под строки лексем, нужно передать адрес переменной tptr, а не ее значение.
Исходный код процедуры gettok приведен ниже. Обратите внимание, что поскольку она ссылается на символьные указатели tok и ptr, то должна быть включена в тот же исходный файл, что и userin. (Теперь должно быть понятно, зачем была нужна инициализация переменных tok и ptr в начале функции userin.)
(* Получить лексему и поместить ее в буфер tokbuf *)
function gettok (outptr:ppchar):integer;
var
_type:integer;
begin
(* Присвоить указателю на строку outptr значение tok *)
outptr^ := tok;
(* Удалить пробелы из буфера, содержащего лексемы *)
while (ptr^ = ' ') or (ptr^ = #9) do
inc(ptr);
(* Установить указатель на первую лексему в буфере *)
tok^ := ptr^;
inc(tok);
(* Установить значение переменной type в соответствии
* с типом лексемы в буфере *)
case ptr^ of
#$a:
begin
_type := EOL;
inc(ptr);
end;
'&':
begin
_type := AMPERSAND;
inc(ptr);
end;
';':
begin
_type := SEMICOLON;
inc(ptr);
end;
else
begin
_type := ARG;
inc(ptr);
(* Продолжить чтение обычных символов *)
while inarg (ptr^) do
begin
tok^ := ptr^;
inc(tok);
inc(ptr);
end;
end;
end;
tok^ := #0;
inc(tok);
gettok:=_type;
end;
Функция inarg используется для определения того, может ли символ быть частью «обычного» аргумента. Пока можно просто проверять, является ли символ особым для командного интерпретатора команд smallsh или нет:
const
special:array [0..5] of char = (' ', #9, '&', ';', #$a, #0);
function inarg(c:char):boolean;
var
wrk:pchar;
begin
wrk := special;
while wrk^<>#0 do
begin
if c = wrk^ then
begin
inarg:=false;
exit;
end;
inc(wrk);
end;
inarg:=true;
end;
Теперь можно составить функцию, которая будет выполнять главную работу нашего интерпретатора. Функция procline будет разбирать командную строку, используя процедуру gettok, создавая тем самым список аргументов процесса. Если встретится символ перевода строки или точка с запятой, то она вызывает для выполнения команды процедуру runcommand. При этом она предполагает, что командная строка уже была считана при помощи функции userin.
{$i smallsh.inc}
function procline:integer; (* обработка строки ввода *)
var
arg:array [0..MAXARG] of pchar; (* массив указателей для runcommand *)
toktype:integer; (* тип лексемы в команде *)
narg:integer; (* число аргументов *)
_type:integer; (* на переднем плане или в фоне *)
begin
narg := 0;
while true do (* бесконечный цикл *)
begin
(* Выполнить действия в зависимости от типа лексемы *)
toktype := gettok (@arg[narg]);
case toktype of
2://ARG
if narg < MAXARG then
inc(narg);
1,3,4://EOL,SEMICOLON, AMPERSAND:
begin
if toktype = AMPERSAND then
_type := BACKGROUND
else
_type := FOREGROUND;
if narg <> 0 then
begin
arg[narg] := nil;
runcommand (arg, _type);
end;
if toktype = EOL then
exit;
narg := 0;
end;
end;
end;
end;
Следующий этап состоит в определении процедуры
runcommand, которая в действительности запускает командные процессы. Процедура runcommand в сущности, является переделанной процедурой docommand, с которой встречались раньше. Она имеет еще один целочисленный параметр
where. Если параметр where принимает значение BACKGROUND, определенное в файле smallsh.inc, то вызов waitpid пропускается, и процедура runcommand просто выводит идентификатор процесса и завершает работу.
{$i smallsh.inc}
(* Выполнить команду, возможно ожидая ее завершения *)
function runcommand(cline:ppchar;where:integer):integer;
var
pid:longint;
status:integer;
begin
pid := fork;
case pid of
-1:
begin
perror ('smallsh');
runcommand:=-1;
exit;
end;
0:
begin
execvp (cline^, cline, envp);
perror (cline^);
halt(1);
end;
end;
(* Код родительского процесса *)
(* Если это фоновый процесс, вывести pid и выйти *)
if where = BACKGROUND then
begin
writeln ('[Идентификатор процесса ',pid,']');
runcommand:=0;
exit;
end;
(* Ожидание завершения процесса с идентификатором pid *)
if waitpid (pid, @status, 0) = -1 then
runcommand:=-1
else
runcommand:=status;
end;
Обратите внимание, что простой вызов wait из функции docommand был заменен вызовом waitpid. Это гарантирует, что выход из процедуры docommand произойдет только после завершения процесса, запущенного в этом вызове docommand, и помогает избавиться от проблем с фоновыми процессами, которые завершаются в это время. (Если это кажется не совсем ясным, следует вспомнить, что вызов wait возвращает идентификатор первого завершающегося дочернего процесса, а не идентификатор последнего запущенного.)
Процедура runcommand также использует системный вызов execvp. Это гарантирует, что при запуске программы, заданной командой, выполняется ее поиск во всех каталогах, указанных в переменной окружения PATH, хотя, в отличие от настоящего командного интерпретатора, в программе smallsh нет никаких средств для работы с переменной PATH.
Последний шаг состоит в написании программы, которая связывает вместе остальные функции. Это простое упражнение:
(* Программа smallsh - простой командный интерпретатор *)
{$i smallsh.inc}
const
prompt = 'Command> '; (* приглашение ввода командной строки *)
begin
while userin (prompt) <> EOF do
procline;
end.
Эта процедура завершает первую версию программы
smallsh. И снова следует отметить, что это только набросок законченного решения. Так же, как в случае процедуры docommand, поведение программы smallsh далеко от идеала, когда пользователь вводит символ прерывания, поскольку это приводит к завершению работы программы smallsh. В следующей главе будет показано, как можно сделать программу smallsh более устойчивой.
Упражнение 5.9. Включите в программу smallsh механизм для выключения с помощью символа \ (escaping) специального значения символов, таких как точка с запятой и символ &, так чтобы они могли входить в список аргументов программы. Программа должна также корректно интерпретировать комментарии, обозначаемые символом # в начале. Что должно произойти с приглашением командной строки, если пользователь выключил таким способом специальное значение символа возврата строки?
Упражнение 5.10. Системный вызов dup2 можно использовать для получения копии дескриптора открытого файла. В этом случае он вызывается следующим образом:
dup2(filedes, reqvalue);
где filedes – это исходный дескриптор открытого файла. Значение переменной reqvalue должно быть небольшим целым числом. Если уже был открыт файл с дескриптором, равным reqvalue, он закрывается. После успешного вызова переменная reqvalue будет содержать дескриптор файла, который ссылается на тот же самый файл, что и дескриптор filedes. Следующий фрагмент программы показывает, как перенаправить стандартный ввод, то есть дескриптор файла со значением 0:
fd := fdopen('somefile', Open_RDONLY);
fdclose (0);
dup2(fd, 0);
Используя этот вызов вместе с системными вызовами fdopen и fdclose, переделайте программу smallsh так, чтобы она поддерживала перенаправление стандартного ввода и стандартного вывода, используя ту же систему обозначений, что и стандартный командный интерпретатор UNIX. Помните, что стандартный ввод и вывод соответствует дескрипторам 0 и 1 соответственно. Обратите внимание, что существует также близкий по смыслу вызов dup.