Системное программирование в UNIX средствами Free Pascal

       

Командный интерпретатор 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.


Содержание раздела