UNIX — универсальная среда программирования

Керниган Брайан Уилсон

Пайк Роб

Глава 6

Программирование с помощью стандартных функций ввода-вывода

 

 

До сих пор мы использовали существующие инструменты, чтобы разрабатывать новые, но сейчас уже достигнут разумный предел в создании новых средств с помощью shell, sed и awk. В этой главе нам предстоит написать простые программы на языке программирования Си. Основополагающая философия конструирования объектов, функционирующих совместно, будет по-прежнему оказывать влияние на построение программ, так как наша цель — подготовить инструменты, с которыми можно работать и на которые можно положиться. В каждом случае мы также попытаемся показать вам приемлемую стратегию реализации таких инструментов: начинать с минимума, обеспечивающего некоторые полезные свойства, а затем добавлять новые средства, если в них возникает необходимость.

Существуют веские причины для того, чтобы писать новые программы "с нуля". Так, может оказаться, что проблема, с которой мы столкнулись, просто не может быть решена с помощью имеющихся программ. Часто приходится иметь дело с нетекстовыми файлами. Например, большинство программ, которые демонстрировались ранее, действительно хорошо работали лишь с текстовой информацией либо слишком трудно достигалась должная ясность или эффективность, если применялись только shell и другие средства общего назначения. В подобных случаях реализация с использованием shell может быть полезна для апробирования программы и ее интерфейса с пользователем. (Если же программа работает достаточно хорошо, нет причины для ее переделки.) Уже знакомая вам программа zap является в этом смысле неплохим примером: требуется всего несколько минут, чтобы написать первую версию на shell, которая имеет адекватный пользовательский интерфейс, но слишком медленна.

Мы будем писать программы на языке Си — стандартном языке системы UNIX (ядро и все пользовательские программы написаны на Си), поскольку нет иного языка, хотя бы отчасти также хорошо поддерживаемого. Вы должны знать этот язык, по крайней мере в такой степени, чтобы свободно разбираться в предлагаемом здесь материале. Если это не так, прочтите книгу "Язык программирования Си" Б. Кернигана и Д. Ритчи (М.: Финансы и статистика, 1985). Мы также воспользуемся "стандартной библиотекой ввода-вывода" — набором функций, обеспечивающих программы на Си эффективными и переносимыми средствами ввода-вывода и системными услугами. Стандартные библиотеки ввода-вывода есть во многих, отличных от UNIX, системах, поддерживающих Си, поэтому программы, взаимодействия с системой которых ограничены возможностями таких библиотек, могут быть легко перенесены.

Примеры, подобранные в настоящей главе, имеют общее свойство: они представляют собой небольшие "инструменты", которые используются регулярно, но не являются частью седьмой версии системы. Если ваша система обладает подобными программами, вам будет легче сравнивать системы. Если же вы с ними еще не знакомы, то они могут оказаться вам чрезвычайно полезными. Эти программы помогут вам понять, что совершенной системы не существует, и для того чтобы добиться улучшения и устранить те или иные дефекты, достаточно приложить лишь небольшие усилия

 

6.1 Стандартные входной и выходной потоки: программа

vis

Многие программы читают только из одного входного потока и пишут в один выходной поток: для таких программ полностью подходят функции ввода-вывода, использующие лишь стандартные входной и выходной потоки, и для того чтобы начать работу, этого почти всегда достаточно.

Проиллюстрируем изложенное с помощью программы vis, которая копирует свой стандартный входной поток в стандартный выходной, изображая при этом все непечатаемые символы в виде \nnn, где nnn — восьмеричное значение символа. Vis полезна для обнаружения "посторонних" или нежелательных символов, которые могут попасть в файлы. Например, vis будет печатать каждый символ "шаг назад" как \010, что является его восьмеричным значением:

$ cat x abc

$ vis < x

abc\010\010\010 ___

$

Чтобы просмотреть несколько файлов с помощью этой элементарной версии vis, вы можете использовать cat для сбора файлов

$ cat файл1 файл2 ... | vis

...

$ cat файл1 файл2 ... | vis | grep '\\'

...

и избежать тем самым выяснения способа доступа к файлам из программы.

Между прочим, может показаться, что подобную работу следует выполнить с привлечением sed, поскольку команда '1' выдает на экран непечатаемые символы в наглядном виде:

$ sed -n 1 x

abc←←←___

$

Результат выполнения программы sed, вероятно, вам покажется яснее, чем результат выполнения vis. Но применение sed к нетекстовым файлам бессмысленно:

$ sed -n 1 /usr/you/bin

$ Ничего в ответ!

(Так получилось на PDP-11; в одной из систем для VAX sed аварийно завершилась, возможно, потому, что ввод был воспринят как очень длинная текстовая строка.) Таким образом, sed нам не подходит, и мы вынуждены писать новую программу.

Простейшие функции ввода и вывода getchar и putchar. При каждом вызове getchar появляется очередной символ из стандартного входного потока, которому может быть поставлен в соответствие файл, конвейер или терминал (последнее принимается по умолчанию). Программа "не знает", что конкретно он собой представляет. Аналогично putchar(c) помещает символ в стандартный выходной поток, который по умолчанию также связан с терминалом.

Функция printf(3) выполняет форматное преобразование при выводе. Вызовы printf и putchar могут следовать в любом порядке; выходной поток отразит порядок этих вызовов. Для форматного преобразования входного потока предусмотрена функция scanf(3); она читает входной поток и разбивает его, как требуется, на строки, числа и т.п. Вызовы scanf и getchar также могут чередоваться.

Приведем первую версию vis:

/* vis: make funny characters visible (version 1) */

#include

#include

main() {

 int c;

 while ((c = getchar()) != EOF)

  if (isascii(c) &&

   (isprint(с) || c=='\n' || c=='\t' || c==' '))

   putchar(c);

  else

   printf("\\%03o", c);

 exit(0);

}

Getchar возвращает из входного потока очередной байт или значение EOF, когда встречает конец файла (или ошибку). Между прочим, EOF не является байтом из файла; вспомните: во второй главе объяснялось, что такое "конец файла". Значение EOF отличается от значения любого байта, поэтому его трудно спутать с реальными данными; переменная с описана как int (целая), а; не как char (символьная), так что она может хранить значение EOF. Строка

#include

должна находиться в начале каждого исходного файла. Это заставляет компилятор Си читать файл макроопределений (/usr/include/stdio.h), в котором специфицированы стандартные функции и имена, в том числе и EOF. Мы будем использовать как краткую запись полного имени файла.

Файл — еще один файл макроопределений в /usr/include, который задает машинно-независимые макрокоманды (макросы) для классификации символов. Чтобы выяснить, принадлежит ли входной символ набору ASCII (т.е. его значение меньше 0200) и печатается ли он, мы использовали здесь isascii и isprint. Остальные макросы перечислены в табл. 6.1. Отметим, что определяет символы "перевод строки", "табуляция" и пробел как непечатаемые.

isalpha(c) Буква принадлежит алфавиту: a-z A-Z
isupper(c) Прописная буква: A-Z
islower(с) Строчная буква: a-z
isdigit(c) Цифра: 0-9
isxdigit(c) Шестнадцатеричная цифра: 0-9 a-f A-F
isalnum(c) Буква или цифра
isspace(c) Пробел, символ табуляции, символ перевода строки, символ вертикальной табуляции, символ перевода страницы, символ возврата
ispunct(c) Не буквенно-цифровой символ, не управляющий, не пробел
isprint(c) Печатаемый: любой графический символ
iscntrl(c) Управляющий символ: 0 <= с < 040 || с == 0177
isascii(c) Символ ASCII: 0 <= с <= 0177

Таблица 6.1: Макросы классификации символов

Вызов exit в конце vis не является необходимым для корректной работы программы, но гарантирует тому, кто эту программу вызвал, получение нормального кода ее завершения (обычно нуля). Другой способ возврата кода завершения выполнить в теле функции main оператор return 0; возвращаемое значение main и есть код завершения программы. Если нет явно указанных return или exit, код завершения не определен.

Для компиляции программы на Си поместите исходный текст в файл, имя которого оканчивается на .с, например vis.с, оттранслируйте его с помощью сс и запустите на выполнение результат, оставляемый компилятором в файле с именем a.out ('а' — ассемблер):

$ сс vis.с

$ a.out

hello worldctl^g

hello world\007

ctl-d

$

a.out можно переименовать после первого запуска или сделать это сразу с помощью флага -о команды сс:

$ сс -о vis vis.с Результат в vis, а не в a.out

Упражнение 6.1

Мы решили, что символы табуляции не следует делать видимыми, изображая их как \011 , → или \t , поскольку главное назначение vis — поиск действительно аномальных символов. Можно принять альтернативное решение и недвусмысленно идентифицировать каждый символ в выходном потоке: символы табуляции, неграфические символы, пробелы в конце строки и т.п. Модифицируйте vis так, чтобы символы табуляции, обратная дробная черта, "шаг назад", перевод страницы и др. печатались в традиционном, принятом в Си представлении: \t , \\ , \b , \f и т.д., причем пробелы в конце строки должны быть помечены. Можете сделать это недвусмысленным образом? Сравните ваш вариант с приведенным ниже:

$ sed -n 1

Упражнение 6.2

Модифицируйте vis так, чтобы она приводила длинные строки к строкам некоторой разумной длины. Как это согласуется с требованием недвусмысленности результата из предыдущего упражнения?

 

6.2 Аргументы программы:

vis

версия 2

Когда выполняется программа на Си, функции main передаются следующие аргументы из командной строки: счетчик argc и массив argv, состоящий из указателей символьных строк, содержащих аргументы. По соглашению argv[0] это имя самой команды, так что argc всегда больше нуля; "полезными" же являются аргументы argv[1]...argv[argc - 1]. Вспомните, что переключение входного или выходного потоков с помощью < и > осуществляется в shell, а не отдельными программами, поэтому такое переключение не влияет на число аргументов, видимых программой.

Для иллюстрации работы с аргументами модифицируем vis, добавив флаг: vis -s удаляет любые непечатаемые символы вместо того, чтобы выделять их. Такое удаление удобно для "чистки" файлов из других систем, например тех, которые используют для завершения строки CRLF (символы возврата каретки и перевода строки) вместо одного символа перевода строки.

/* vis: make funny characters visible (version 2) */

#include

#include

main(argc, argv)

 int argc;

 char *argv[];

{

 int c, strip = 0;

 if (argc > 1 && strcmp(argv[1], "-s") == 0)

  strip = 1;

 while ((c = getchar()) != EOF)

  if (isascii(c) &&

   (isprint(с) || c=='\n' || c=='\t' || c==' '))

   putchar(c);

  else if (!strip)

   printf("\\%03o", c);

 exit(0);

}

Здесь argv — указатель массива, элементы которого служат указателями массивов символов; каждый такой массив заканчивается символом ASCII NUL ('\0'), поэтому массив можно считать строкой. Эта версия vis начинает свою работу с того, что проверяет, есть ли аргумент и является ли он -s. (Неверные аргументы игнорируются.) Функция strcmp(3) сравнивает две строки, возвращая нуль, если они одинаковы.

В табл. 6.2 перечислены некоторые средства работы со строками и ряд полезных функций, одна из которых strcmp. Как правило, лучше воспользоваться этими стандартными функциями, чем писать собственные, так как они отлажены и зачастую выполняются быстрее, чем написанные вами, поскольку были оптимизированы для конкретных машин (нередко благодаря использованию Ассемблера).

strcat(s,t) Добавляет строку t к строке s ; возвращает s
strncat(s,t,n) Добавляет не более n символов t к s
strcpy(s,t) Копирует t в s ; возвращает s
strncpy(s,t,n) Копирует точно n символов; при необходимости добавляет NULL
strcmp(s,t) Сравнивает s и t , возвращает <0, 0, >0 при <, ==, >
strncmp(s,t,n) Сравнивает не более n символов
strlen(s) Возвращает длину s
strchr(s,c) Возвращает указатель на первый символ с в s и NULL , если с отсутствует
strrchr(s,c) Возвращает указатель на последний с в s и NULL , если с отсутствует.
atoi(s) Возвращает целое значение s
atof(s) Возвращает "плавающее" значение s ; необходимо описание double atof()
malloc(n) Возвращает указатель на область памяти в n байт и NULL , если это невозможно
calloc(n,m) Возвращает указатель на n*m обнуленных байтов и NULL , если это невозможно; malloc и calloc возвращают значение типа char*
free(p) Освобождает память, выделенную malloc и calloc

Таблица 6.2: Стандартные функции, выполняемые над строками

Упражнение 6.3

Измените аргумент -s так, чтобы vis -sn печатала только строки из n или более печатаемых символов, опуская непечатаемые символы и короткие последовательности обычных, печатаемых символов. Это полезно при выделении ''текстовых'' частей в нетекстовых файлах, таких, как рабочие программы. Некоторые версии системы содержат для подобных целей программу strings . Что лучше: иметь отдельную программу или пользоваться специальным аргументом vis ?

Упражнение 6.4

Доступность исходной программы на Си — одно из достоинств системы UNIX; такая программа демонстрирует элегантные решения многих программистских проблем. Прокомментируйте баланс между наглядностью программы на Си и встречающимися "оптимизированными" фрагментами, переписанными на Ассемблере.

 

6.3 Доступ к файлам:

vis

версия 3

Две первые версии vis читают из стандартного входного потока и пишут в стандартный выходной поток, причем оба потока наследуются от shell. Следующий шаг модификация vis для работы с файлами по их именам, так что

$ vis файл1 файл2 ...

будет просматривать эти именованные файлы вместо стандартного входного потока. Если же имен файлов в качестве аргументов нет, vis должна читать стандартный входной поток.

Возникает вопрос: как организовать чтение файлов, т.е. как связать имена файлов с операторами ввода вывода, реально читающими данные? Правила просты. Прежде чем быть прочитанным или записанным, файл должен быть открыт стандартной библиотечной функцией fopen. Последняя берет имя файла (например, temp или /etc/passwd), взаимодействует с ядром и возвращает обратно "внутреннее имя", которое используется при последующих операциях с данным файлом.

Внутреннее имя является на самом деле указателем (называемым указателем файла) на структуру, содержащую информацию о файле, такую, как расположение буфера, текущую позицию символа в буфере, режим чтения или записи и т.п. Эта структура определяется в файле и имеет имя FILE. Описание указателя файла таково:

FILE *fp;

Оно означает, что fp — указатель на FILE, fopen возвращает указатель на FILE; в имеется описание типа для fopen. Реальный вызов функции fopen:

char *name, *mode;

fr = fopen(name, mode);

Первый аргумент fopen представляет собой имя файла (строку символов). Второй аргумент также является символьной строкой, показывающей, как вы намереваетесь использовать файл; допустимые режимы: читать ("r"), писать ("w") или дописать ("а").

Если файл, который вы открыли для записи или дописывания, не существует, он создается, если это возможно. Открытие для записи существующего файла вызывает уничтожение старого содержимого. Попытка читать несуществующий файл считается ошибкой, так же как и попытка читать или писать файл без разрешения. При возникновении ошибки fopen возвращает значение несуществующего указателя NULL (которое обычно определяется в как (char*)0).

Далее, нужен способ читать или писать файл после того, как он открыт. Есть несколько способов, из которых использование getc и putc самый простой. Функция getc выбирает из файла очередной символ:

с = getc(fp)

помещает в с следующий символ из файла, на который указывает fp. Эта функция возвращает EOF по достижении конца файла. Функция putc аналогична getc:

putc(c, fp)

помещает символ с в файл fp и возвращает с. Функции getc и putc возвращают EOF в случае ошибки.

Когда программа начинает выполняться, уже открыты три файла и имеются их указатели. Это стандартные потоки: входной, выходной и поток диагностики; соответствующие указатели называются stdin, stdout и stderr. Указатели на файлы описаны в и могут использоваться там, где может быть объект типа FILE*. Они являются не переменными, а константами, так что им нельзя присвоить значения. Вызов getchar() есть getc(stdin), a putchar(c) есть putc(c, stdout). На самом деле все эти четыре "функции" определены в как макрокоманды. Они выполняются быстрее обычных вызовов функций ввиду отсутствия накладных расходов по вызову функции для каждого символа (см. табл. 6.3 с некоторыми другими определениями из ).

stdin Стандартный входной поток
stdout Стандартный выходной поток
stderr Стандартный поток диагностики
EOF Конец файла; обычно -1
NULL Несуществующий указатель; обычно 0
FILE Используется для описания указателей на файлы
BUFSIZ Обычно размер буфера ввода вывода (часто 512 или 1024)
getc(fp) Возвращает один символ из потока fp
getchar() getc(stdin)
putc(c,fp) Помещает символ с в поток fp
putchar(c) putс(с,stdout)
feof(fp) Не нуль, если достигнут конец файла для потока fp
ferror(fp) Не нуль, если в потоке fp есть ошибка
fileno(fp) Дескриптор файла для потока fp (см. гл. 7)

Таблица 6.3: Некоторые определения из

Теперь вернемся снова к нашей теме и напишем третью версию vis. Если есть аргументы командной строки, они обрабатываются в порядке очередности, если же аргументов нет, используется стандартный входной поток.

/* vis: make funny characters visible (version 3) */

#include

#include

int strip = 0; /* 1 => discard special characters */

main(argc, argv)

 int argc;

 char *argv[];

{

 int i;

 FILE *fp;

 while (argc > 1 && argv[1][0] == '-') {

  switch (argv[1][1]) {

  case 's': /* -s: strip funny chars */

   strip = 1;

   break;

  default:

   fprintf(stderr, "%s: unknown arg %s\n",

    argv[0], argv[1]);

   exit(1);

  }

  argc--;

  argv++;

 }

 if (argc == 1)

  vis(stdin);

 else

 for (i = 1; i < argc; i++)

  if ((fp=fopen(argv[i], "r")) == NULL) {

   fprintf(stderr, "%s: can't open %s\n",

    argv[0], argv[i]);

   exit(1);

  } else {

   vis(fp);

   fclose(fp);

  }

 exit(0);

}

В программе принято соглашение, по которому флаги стоят в начале списка аргументов. После обработки каждого флага argv и argc модифицируются так, что остальная часть программы не зависит от присутствия этого флага. Даже если vis распознает единственный флаг, мы написали программу в виде цикла, чтобы продемонстрировать единый способ обработки аргументов. В гл. 1 отмечалось, что программы UNIX обрабатывают флаги в произвольном порядке. Как одну из причин (помимо склонности к анархии) здесь можно назвать очевидную легкость написания программы разбора аргументов при любой модификации. Включение функции getopt(3) в некоторые системы является попыткой рационально объяснить ситуацию; вы можете ее исследовать, прежде чем писать собственную.

Процедура vis выводит на печать единственный файл:

vis(fp) /* make chars visible in FILE *fp */

 FILE *fp;

{

 int c;

 while ((с = getc(fp)) != EOF)

  if (isascii(c) &&

   (isprint(с) || c=='\n' || c=='\t' || c==' '))

   putchar(c);

  else if (!strip)

   printf("\\%03o", c);

}

Функция fprintf идентична printf, за исключением аргумента указателя, специфицирующего файл, в который нужно писать.

Функция fclose разрывает связь между указателем и внешним именем файла, установленную с помощью fopen, освобождая указатель для другого файла. Так как существует ограничение (около 20) на число файлов, которые одновременно могут быть открыты в программе, лучше всего закрывать уже не требующиеся вам файлы. Обычно выходной поток, выдаваемый любой стандартной библиотечной функцией, подобной printf, putc и т.д., для большей эффективности буферизуется так, чтобы его можно было писать большими фрагментами. (Исключение составляет выходной поток терминала, который, как правило, пишется по мере своего формирования или при печати символа перевода строки.) Применение fclose к выходному файлу инициирует выдачу последней буферизованной порции, fclose также вызывается автоматически для каждого открытого файла, когда программа выполняет exit или возвращается из main.

Стандартный поток stderr присваивается программе тем же способом, что и stdin и stdout. Информация, записанная в stderr, оказывается на терминале пользователя даже при изменении назначения стандартного выходного потока. Vis пишет свою диагностику в stderr вместо stdout, так что если один из файлов по каким-то причинам недоступен, сообщение найдет путь на терминал пользователя, а не исчезнет в программном канале или в выходном файле. (Стандартный поток диагностики был изобретен позднее, чем программные каналы: после того, как сообщения об ошибках стали исчезать при передаче через эти каналы.)

Мы решили, отчасти произвольно, что vis завершается, если не может открыть входной файл; это разумно для программы, чаще всего используемой в режиме диалога и с одним входным файлом. Однако вы можете предложить и другое решение.

Упражнение 6.5.

Напишите программу printable , которая печатает имя каждого аргумента-файла, содержащего только печатаемые символы; если в файле хранится любой непечатаемый символ, имя не печатается. printable полезна в ситуациях, подобных следующей:

$ pr `printable *` | lpr

Добавьте флаг -v , чтобы изменить смысл проверки на обратный, как в grep . Что следует делать, если среди аргументов нет имен файлов? Какой код завершения должна передавать printable при возврате?

 

6.4 Вывод на экран порциями: программа

p

До сих пор мы использовали cat для просмотра файлов. Но если файл длинный, а связь с системой высокоскоростная, cat выдает выходной файл слишком быстро, что затрудняет его чтение, даже если вы успеваете делать это с помощью ctl-s и ctl-q.

Очевидно, нужно иметь программу для печати файла небольшими удобными порциями, но такая программа не является стандартной, возможно, потому, что первоначальная система UNIX была написана в те времена, когда использовались терминалы "твердой копии" (печати на бумаге) и медленные линии связи. Поэтому наш следующий пример программа p, которая выводит файл по частям, занимающим полный экран, за один раз, ожидая ответа от пользователя после выдачи каждой порции прежде, чем начать очередной вывод (краткое имя p весьма удобно для программы, которая часто используется). Подобно другим программам, p читает или из файлов, поименованных, как аргументы, или из стандартного входного потока:

$ p vis.с

...

$ grep '#define' *.[ch] | p

...

$

Эту программу легче всего писать на Си; стандартные средства неудобны, когда происходит смешанный ввод из файла или конвейера и с терминала. Решение состоит в том, чтобы печатать входной поток небольшими порциями. Удобный размер порции 22 строки, что составляет немногим меньше, чем размер в 24 строки на большинстве видеотерминалов, и одну треть стандартной страницы в 66 строк. Простой способ подсказки пользователю не печатать последний символ перевода строки каждой порции. Курсор остановится на правом конце строки, а не на левой границе (новой строки). При нажатии клавиши RETURN выполняется перевод строки, и следующая строка появляется в нужном месте. Если пользователь печатает ctl-d или q в конце экрана, выполнение программы p заканчивается.

Мы не станем принимать специальных мер для вывода длинных строк. Мы также не будем беспокоиться о множестве файлов: просто перейдем от одного к другому без примечаний. Следующая команда

$ p имена файлов...

по своему действию аналогична команде

$ cat имена файлов... | p

Если нужны имена файлов, их можно добавить циклом for:

$ for i in имена файлов

> do

>  echo $i:

>  cat $i

> done | p

В самом деле, у нас оказывается слишком много средств, которые мы можем внести в программу. Лучше сделать "неоснащенную" версию, а затем развивать ее, как подскажет опыт. Иными словами, необходимые средства это те, которые действительно вам нужны, а не те, которые, по нашему мнению, вы хотели бы иметь.

Структура p аналогична структуре vis: основная процедура выполняет цикл по файлам, вызывая функцию print, выполняющуюся с каждым файлом:

/* p: print input in chunks (version 1) */

#include

#define PAGESIZE 22

char *progname; /* program name for error message */

main(argc, argv)

 int argc;

 char *argv[];

{

 int i;

 FILE *fp, *efopen();

 progname = argv[0];

 if (argc ==1)

  print(stdin, PAGESIZE);

 else

  for (i = 1; i < argc; i++) {

   fp = efopen(argv[i], "r");

   print(fp, PAGESIZE);

   fclose(fp);

  }

 exit(0);

}

Функция efopen реализует весьма общую операцию: пытается открыть файл. Если же это невозможно, она выводит на печать сообщение об ошибке, и ее выполнение завершается. Чтобы обеспечить выдачу сообщений об ошибках, идентифицирующих программу, в которой происходит (или произошла) ошибка, efopen ссылается на внешнюю строку progname, где содержится имя программы, устанавливаемое в main:

FILE *efopen(file, mode) /* fopen file, die if can't */

 char *file, *mode;

{

 FILE *fp, *fopen();

 extern char *progname;

 if ((fp = fopen(file, mode)) != NULL)

  return fp;

 fprintf(stderr, "%s: can't open file %s mode %s\n",

  progname, file, mode);

 exit(1);

}

Мы испытали две версии программы efopen, прежде чем остановиться на данной. Одна из них должна была после печати сообщения завершиться, возвратив нулевой указатель, свидетельствующий о неудаче. Это позволяет вызвавшей программе продолжить свое выполнение или завершиться. Другая версия снабжала efopen третьим аргументом, указывающим, следует ли возвращаться после того, как файл открыть не удалось. Почти во всех наших примерах, однако, нет смысла продолжать работу, если файл недоступен, так что текущая версия efopen является для нас наилучшей.

Непосредственное выполнение команды p осуществляется в print:

print(fp, pagesize) /* print fp in pagesize chunks */

 FILE *fp;

 int pagesize;

{

 static int lines = 0; /* number of lines so far */

 char buf[BUFSIZ];

 while (fgets(buf, sizeof buf, fp) != NULL)

  if (++lines < pagesize)

   fputs(buf, stdout);

  else {

   buf[strlen(buf)-1] = '\0';

   fputs(buf, stdout);

   fflush(stdout);

   ttyin();

   lines = 0;

  }

}

Мы использовали здесь BUFSIZ, который определен в как размер буфера входного потока. Функция fgets(buf, size, fp) выбирает следующую строку входного потока из fp до символа перевода строки (включая его) в буфер и добавляет завершающий символ \0. Копируется на более size - 1 символов. По достижении конца файла возвращается NULL. (Конструкция fgets оставляет желать лучшего: она возвращает buf вместо счетчика символов и, кроме того, выдает предупреждение о том, что входная строка была слишком длинной. Символы не потеряны, но вы должны взглянуть на buf, чтобы понять, что в самом деле случилось.)

Функция strlen возвращает длину строки, поэтому мы можем отбросить завершающий символ перевода строки последней входной строки. После вызова fputs(buf, fp) строка buf записана в файл fp. При вызове fflush в конце страницы происходит вывод буферизованного выходного текста.

Считывание ответа пользователя в конце каждой страницы возложено на функцию ttyin. Функция ttyin не может читать стандартный входной поток, тогда как p должна выполняться, даже если входной поток поступает из файла или конвейера. Чтобы справиться с этим, программа открывает файл /dev/tty, которому поставлен в соответствие пользовательский терминал при любом переключении стандартного входного потока. Приведенная ниже функция ttyin возвращает первую букву ответа, но здесь это свойство не используется.

ttyin() /* process response from /dev/tty (version 1) */

{

 char buf[BUFSIZ];

 FILE *efopen();

 static FILE *tty = NULL;

 if (tty == NULL)

  tty = efopen("/dev/tty", "r");

 if (fgets(buf, BUFSIZ, tty) == NULL || buf[0] == 'q')

  exit(0);

 else /* ordinary line */

  return buf[0];

}

Указатель на файл devtty описан как статический, так что его значение сохраняется от одного вызова ttyin до другого; файл /dev/tty открывается только при первом вызове.

Очевидно, есть дополнительные средства, которые без особых усилий можно ввести в p, однако наша первая версия этой программы только печатает 22 строки и ждет следующей порции. Прошло немало времени, прежде чем в нее были добавлены другие средства, но в настоящее время ими мало кто пользуется. В частности, весьма простое дополнение ввод переменной pagesize для хранения числа строк на странице. Значение переменной можно установить из командной строки

$ p -n...

Она печатает порции по n строк. Для этого требуется лишь добавить несколько знакомых вам операторов в начале main:

/* p: print input in chunks (version 2) */

...

int i, pagesize = PAGESIZE;

progname = argv[0];

if (argc > 1 && argv[1][0] == '-') {

 pagesize = atoi(&argv[1][1]);

 argc--;

 argv++;

}

Функция atoi превращает строку символов в целое число (см. справочное руководство по atoi(3)).

Еще одно средство временно остановить вывод на экран в конце каждой страницы, чтобы выполнить какую-либо иную команду. По аналогии с ed и многими другими программами, если пользователь печатает строку, начинающуюся восклицательным знаком, остальная часть строки воспринимается как команда и передается shell для выполнения. Данное средство также тривиально, поскольку для этой цели предусмотрена функция system(3), речь о которой пойдет ниже. Модифицированная версия ttyin такова:

ttyin() /* process response from /dev/tty (version 2) */

{

 char buf[BUFSIZ];

 FILE *efopen();

 static FILE *tty = NULL;

 if (tty == NULL)

  tty = efopen("/dev/tty", "r");

 for (;;) {

  if (fgets(buf,BUFSIZ,tty) == NULL || buf[0] == 'q')

   exit(0);

  else if (buf[0] == '!') {

   system(buf+1); /* BUG here */

   printf("!\n");

  else /* ordinary line */

   return buf[0];

 }

}

К сожалению, эта версия ttyin имеет серьезный недостаток. Команда, запущенная с помощью system, получает стандартный входной поток от p, так что если p читает из программного канала или файла, их входные потоки могут мешать друг другу:

$ cat /etc/passwd | p -1

root:3d.fHR5KoB.3s:0:l:S.User:/:!ed Вызвать ed из p

? ed читает /etc/passwd

! … запутывается и завершается

Для решения этой проблемы необходимо знать, как управлять процессами в UNIX, о чем речь пойдет в разд. 7.4. Пока же примите к сведению, что использование стандартной библиотечной функции system может создать неприятности, однако ttyin работает правильно, если компилируется с версией system, описанной в гл. 7.

Итак, мы написали две программы vis и p, которые можно считать вариантами cat с некоторыми "украшениями". Может быть, им следует быть частью cat, доступной с помощью флагов -v и -р? Вопрос о том, писать ли новую программу или добавлять какие-то средства к старой, возникает всегда, как только у людей появляются новые идеи. Мы не можем со всей определенностью ответить на данный вопрос, но приведем здесь некоторые принципы, которые, возможно, вам помогут.

Основной принцип состоит в том, что программе следует выполнять только свою основную работу. Если у нее появляется слишком много функций, она становится большой, медленной, ее трудно сопровождать и использовать. В самом деле, ряд свойств часто остается невостребованным, поскольку пользователи никак не могут запомнить флаги.

Поэтому cat и vis совмещать не рекомендуется. Если cat просто копирует входной поток без изменений, то vis его трансформирует. Соединение их дает программу с двумя разными функциями. Это очевидно также для cat и p: cat предназначена для быстрого эффективного копирования страниц, p для их "перелистывания". Кроме того, p преобразует выходной поток. Каждый 22-й символ перевода строки пропускается. Три отдельные программы представляются в таком случае правильным решением.

Упражнение 6.6

Работает ли p нормально, если pagesize не является положительным?

Упражнение 6.7

Что еще можно было бы сделать с p ? Оцените и реализуйте (если оно вам подходит) свойство вновь выводить части ранее введенного текста. (Это дополнительное средство нам очень нравится.) Добавьте возможность выводить неполное содержимое экрана после каждой паузы, а также просматривать текст вперед или назад по строкам, задаваемым номером или содержимым.

Упражнение 6.8

Используйте средства манипуляций файлами, встроенные в exec shell (см. справочное руководство по sh(1) ), чтобы фиксировать обращения к system с терминала ttyin .

Упражнение 6.9

Если вы забыли определить источник ввода для p , то программа "молча" ожидает ввода с терминала. Стоит ли искать эту возможную ошибку? Если да, то как? Подсказка: isatty(3) .

 

6.5 Пример:

pick

Версия pick из гл. 5, несомненно, увеличивает возможности shell. Версия на Си, приведенная ниже, в чем-то отличается от рассмотренной в гл. 5. Если эта версия имеет аргументы, то они обрабатываются так же, как и ранее, но если определен единственный аргумент '-', pick обрабатывает свой стандартный входной поток.

Почему бы в отсутствие аргументов просто не читать стандартный входной поток? Рассмотрим вторую версию команды zap из разд. 5.6:

kill $SIG `pick\`ps-ag | egrep "$*"\` | awk '{print $1}'`

Что происходит, если шаблон egrep ни с чем не совпадает? В этом случае pick не имеет аргументов и читает свой стандартный входной поток; команда zap терпит неудачу загадочным образом. Требование явного аргумента простой способ устранить неоднозначность, и соглашение о '-' в cat и других программах показывает, как его определить.

/* pick: offer choice on each argument */

#include

char *progname; /* program name for error message */

main(argc, argv)

 int argc;

 char *argv[];

{

 int i;

 char buf[BUFSIZ];

 progname = argv[0];

 if (argc == 2 && strcmp(argv[1], "-") == 0) /* pick - */

  while (fgets(buf, sizeof buf, stdin) != NULL) {

   buf[strlen(buf)-1] = '\0'; /* drop newline */

   pick(buf);

  }

 else

  for (i = 1; i < argc; i++)

   pick(argv[i]);

 exit(0);

}

pick(s) /* offer choice of s */

 char *s;

{

 fprintf(stderr, "%s? ", s);

 if (ttyin() == 'y')

  printf("%s\n", s);

}

Версия pick предоставляет возможность диалогового выбора аргументов в одной программе. Это не только обеспечивает полезное средство, но и уменьшает потребность в "интерактивных" флагах для других команд.

Упражнение 6.10

Если есть pick , существует ли необходимость в rm -i ?

 

6.6 Об ошибках и отладке

Если вы писали программы ранее, вам знакомо понятие ошибки. Однако важно не только создавать программы, свободные от ошибок, но и заботиться о том, чтобы ваш проект был прост, тщательно реализован и сохранял свою "чистоту" в процессе модификации.

В UNIX много инструментов, которые помогут вам находить ошибки, хотя ни один из них не является действительно первоклассным. Для того чтобы продемонстрировать их, нам нужна ошибка; все же программы в этой книге совершенны. Поэтому мы "создадим" типичную ошибку. Рассмотрим приведенную выше функцию pick, но на сей раз с ошибкой (заглядывать в первоначальный вариант нечестно):

pick(s) /* offer choice of s */

 char *s;

{

 fprintf("%s? ", s);

 if (ttyin() == 'y')

  printf("%s\n", s);

}

Что произойдет, если мы откомпилируем и запустим ее?

$ сс pick.с -о pick

$ pick *.с                                  Попробуем

Ошибка при обращении к памяти - сделан дамп Катастрофа!

$

Сообщение "Ошибка при обращении к памяти" свидетельствует о том, что ваша программа пыталась работать с недозволенной областью памяти. Обычно в таком случае указатель содержит неправильное значение. "Ошибка адресации шины" другое диагностическое сообщение со сходным значением, часто обусловленное просмотром бесконечной строки. "Сделан дамп памяти" означает, что ядро сохранило состояние вашей выполняемой программы в файле core текущего справочника. Вы также можете заставить программу сделать дамп памяти, напечатав ctl-\, если она выполняется как фоновая, или с помощью команды kill -3, если она основная.

Существуют две программы adb и sdb, назначение которых разбираться в "посмертной выдаче". Подобно большинству отладчиков, они "хитроумны", сложны и без них трудно обойтись. Программа adb есть в седьмой версии системы, a sdb доступна в более поздних версиях.

Из-за ограниченного объема книги мы лишь частично покажем вам применение каждой программы, а именно распечатаем содержимое стека, т.е. выведем функцию, выполнявшуюся при аварийном завершении программы, функцию, которая ее вызывала, и т.д. Первая функция, указанная в распечатке стека, это то место, где находилась программа, когда она была аварийно завершена.

Чтобы получить распечатку стека с помощью adb, нужно ввести команду $C:

$ adb pick core Вызывает adb

$C              Запрос содержимого стека

~_strout(0175722,011,0,011200)

 adjust: 0

 fillch: 060542

__doprnt(0177345,0176176,011200)

~fprintf(011200,0177345)

 iop:  01120

 fmt:  0177345

 args: 0

~pick(0177345)

 s: 0177345

~main(035,0177234)

 argc: 035

 argv: 0177234

 i:    01

 buf:  0

ctl-d           Завершение

$

Здесь речь идет о том, что main была вызвана из pick, которая вызвала fprintf, а она в свою очередь вызвала __doprnt, вызвавшую _strout. Так как __doprnt не упомянута где-либо в pick.с, ошибка должна быть где-то в fprintf или выше. (Строки после каждой функции в распечатке показывают значения локальных переменных. $С подавляет данную информацию так же, как сама $С делает это в некоторых версиях adb.) Попытаемся теперь сделать то же самое с помощью sdb:

$ sdb pick core

Предупреждение: 'a.out не компилируется с -g

lseek: address 0xa64 Функция, где программа аварийно завершилась

*t                   Запрос распечатки стека

lseek()

fprintf(6154,2147479154)

pick(2147479154)

main(30,2147478988,2147479112)

*q                   Выход

$

Информация размещена по-иному, но есть общая основа: fprintf. (Распечатка стека другая, так как это сделано на машине VAX-11/750, на которой стандартная библиотека ввода вывода реализована иначе.) И если мы взглянем на вызов fprintf в неправильной версии pick, то обнаружим некорректность:

fprintf("%s?", s);

Здесь нет stderr, так что строка формата используется как ссылка к FILE, и, конечно, получается хаос.

Мы показали вам типичную ошибку, которая является скорее результатом просмотра, а не неправильного программирования. Искать подобные ошибки при вызове функции с неверными аргументами можно также с помощью верифицирующей программы для Си lint(1). Эта программа рассматривает Си-программы с точки зрения наличия ошибок, аспектов переносимости и сомнительных конструкций. Если мы запустим lint с файлом pick.с, ошибка идентифицируется:

$ lint pick.с

...

fprintf, arg. 1 несовместим "llib-lc"(69) :: "pick.c"(28)

...

$

Это означает, что первый аргумент в стандартной библиотеке определен иначе, чем в строке 28 вашей программы. Таким образом дана точная информация о том, что неверно.

Программа lint, с одной стороны, указывает на недостатки в вашей программе, а с другой выдает много не относящихся к делу сообщений, которые мы выше опустили, и нужен некоторый опыт, чтобы уметь разбираться, какие из них необходимы, а какие следует игнорировать. Однако имеет смысл постараться, так как lint помогает обнаружить некоторые ошибки, которые человек увидеть практически не может. После длительного редактирования всегда стоит запустить lint и убедиться в том, что каждое выдаваемое этой программой предупреждение вам понятно.

 

6.7 Пример:

zap

Программа zap, которая избирательно уничтожает процессы, отличается от той, что была представлена в виде файла shell в гл. 5. Главная проблема данной версии скорость. Она создает много процессов и поэтому работает медленно, что недопустимо для программы, уничтожающей процессы с ошибками. Если переписать zap на Си, ее быстродействие повысится. Мы, однако, снова воспользуемся ps, чтобы найти информацию о процессе. Это намного легче, чем выуживать информацию из ядра, и, кроме того, мы имеем переносимый вариант. Программа zap открывает программный канал, входной поток для которого берется из ps, и читает из него, как из файла. Функция popen(3) аналогична fopen, за исключением того, что первый аргумент является командой, а не именем файла. То же самое справедливо и для pclose, но здесь она нам не нужна.

/* zap: interactive process killer */

#include

#include

char *progname; /* program name for error message */

char *ps = "ps -ag"; /* system dependent */

main(argc, argv)

 int argc;

 char *argv[];

{

 FILE *fin, *popen();

 char buf[BUFSIZ];

 int pid;

 progname = argv[0];

 if ((fin = popen(ps, "r")) == NULL) {

  fprintf(stderr, "%s: can't run %s\n", progname, ps);

  exit(1);

 }

 fgets(buf, sizeof buf, fin); /* get header line */

 fprintf(stderr, "%s", buf);

 while (fgets(buf, sizeof buf, fin) != NULL)

  if (argc == 1 || strindex(buf, argv[1]) >= 0) {

   buf[strlen(buf)-1] = '\0'; /* suppress \n */

   fprintf(stderr, "%s? ", buf);

   if (ttyin() == 'y') {

    sscanf(buf, "%d", &pid);

    kill(pid, SIGKILL);

   }

  }

 exit(0);

}

Мы писали программу, чтобы использовать ps -ag (этот флаг системно зависим), но если вы не являетесь привилегированным пользователем, то можете уничтожать лишь свои собственные процессы.

Первый вызов fgets выбирает заголовок из ps; интересно выяснить, что случится, если попытаться уничтожить "процесс", соответствующий данному заголовку.

Функция sscanf представляет собой член семейства scanf(3) для форматного преобразования входной строки. Она преобразует строку, а не файл. Вызов kill из системы посылает специальный сигнал процессу; сигнал SIGKILL, определенный в , не может быть перехвачен или проигнорирован. Вы можете вспомнить пятую главу, где его численное значение равно девяти, но лучше использовать символические константы из файлов макроопределений, чем включать в свои программы загадочные числа.

Если аргументы отсутствуют, zap предоставляет каждую строку выходного потока ps как возможность для выбора. При наличии аргумента zap предлагает только те выходные строки ps, которые ему соответствуют. Функция strindex(s1, s2) проверяет, соответствует ли аргумент какой-либо части строки выходного потока ps, используя strncmp (см. табл. 6.2). Функция strindex возвращает позицию s2 в s1 или -1, если ее там нет.

strindex(s, t) /* return index of t in s, -1 if none */

 char *s, *t;

{

 int i, n;

 n = strlen(t);

 for (i = 0; s[i] != '\0'; i++)

  if (strncmp(s+i, t, n) == 0)

   return i;

 return -1;

}

В табл. 6.4 представлены широко используемые функции из стандартной библиотеки ввода вывода.

fp=fopen(s, mode) Открыть файл s ; значения mode "r" , "w" , "a" соответствуют чтению, записи и добавлению (при ошибке возвращается NULL)
c=gets(fp) Читать символ: getchar() это getc(stdin)
putc(c, fp) Записать символ: putchar(c) это putc(c, stdout)
ungetc(c, fp) Вернуть символ во входной файл fp ; можно вернуть не более одного символа за раз
scanf(fmt, a1, ...) Читать символы из stdin в a1 , ... в соответствии с fmt . Каждый a i должен быть указателем
fscanf(fp,...) Читать из файла fp
sscanf(s,...) Читать из строки s
printf(fmt, a1, ...) Форматировать a1 , ... в соответствии с fmt ; печатать в stdout
fprintf(fp, ...) Печатать ... в файл fp
sprintf(s, ...) Печатать ... в строку s
fqets(s, n, fp) Читать не более n символов в s из fp (возвращается NULL по концу файла)
fputs(s, fp) Печатать строку s в файл fp
fflush(fp) Занести буферизованные данные выходного потока в файл fp
fclose(fp) Закрыть файл fp
fp=popen(s, mode) Открыть программный канал для команды s (см. fopen )
pclose(fp) Закрыть программный канал fp
system(s) Запустить команду s и ждать ее окончания

Таблица 6.4: Полезные стандартные функции ввода-вывода

Упражнение 6.11

Модифицируйте zap так, чтобы можно было применять любое число аргументов. В настоящем виде zap высвечивает на экране строку, соответствующую выбранному варианту. Будет она делать это? Если нет, модифицируйте программу соответствующим образом. Подсказка: getpid(2) .

Упражнение 6.12

Постройте fgrep(1) на основе strindex . Сравните время работы при сложных поисках, например 10 слов на документ. Почему fgrep выполняется быстрее?

 

6.8 Диалоговая программа сравнения файлов:

idiff

Поддерживать две чем-то отличающиеся версии файла, каждая из которых содержит часть нужного вам файла, довольно распространенная проблема. Зачастую она возникает в тех случаях, когда изменения вносятся независимо двумя разными людьми. Программа diff подскажет вам, чем различаются файлы, но вы не получите никакой помощи, если захотите выбрать какую-то информацию из одного файла, а какую-то из другого.

В этом разделе мы напишем программу idiff (диалоговая diff), которая предоставляет пользователю каждую порцию выходного потока diff и предлагает ему возможность выбора фрагментов "от и до" или их редактирования. Программа idiff помещает выбранные фрагменты в соответствующем порядке в файл idiff.out. Допустим, даны такие два файла:

file1:            file2:

This is           This is

a test            not a test

of                of

your              our

skill             ability.

and comprehension.

diff вырабатывает следующее:

$ diff file1 file2

2c2

< a test

---

> not a test

4,6c4,5

< your

< skill

< and comprehension.

---

> our

> ability.

$

Диалог с idiff может выглядеть так:

$ idiff file1 file2

2c2 Первое различие

< a test

---

> not a test

? >             Пользователь выбрал вторую версию

4,6с4,5         Второе различие

< your

< skill

< and comprehension.

---

> our

> ability.

? <             Пользователь выбрал первую (<) версию

idiff output in file idiff.out

$ cat idiff.out Выходной поток направляется в этот файл

This is

not a test of

your skill

and comprehension.

$

Если вместо < или > выдан ответ е, idiff вызывает ed с двумя группами уже прочитанных строк. Если вторым был ответ е, буфер редактора выглядел бы следующим образом:

your

skill

and comprehension.

---

our

ability.

Все, что пишется редактором обратно в файл, идет в окончательный выходной поток.

И, наконец, любая команда может быть выполнена внутри idiff с помощью временного выхода посредством !cmd.

Технически самая трудная часть работы diff, и она уже выполнена. Таким образом, в задачи idiff входит разбор выходного потока diff, открытие, закрытие, чтение и считывание соответствующих файлов в нужное время. Главная функция idiff поддерживает файлы и запускает процесс diff:

/* idiff: interactive diff */

#include

#include

char *progname;

#define HUGE 10000 /* large number of lines */

main(argc, argv)

 int argc;

 char *argv[];

{

 FILE *fin, *fout, *f1, *f2, *efopen();

 char buf[BUFSIZ], *mktemp();

 char *diffout = "idiff.XXXXXX";

 progname = argv[0];

 if (argc != 3) {

  fprintf(stderr, "Usage: idiff file1 file2\n");

  exit(1);

 }

 f1 = efopen(argv[1], "r");

 f2 = efopen(argv[2], "r");

 fout = efopen("idiff.out", "w");

 mktemp(diffout);

 sprintf(buf,"diff %s %s >%s", argv[1], argv[2], diffout);

 system(buf);

 fin = efopen(diffout, "r");

 idiff(f1, f2, fin, fout);

 unlink(diffout);

 printf("%s output in file idiff.out\n", progname);

 exit(0);

}

Функция mktemp(3) создает файл, имя которого гарантированно отличается от имени любого существующего файла. Mktemp переписывает свой аргумент: шесть символов X заменяются идентификатором процесса и буквой. Системный вызов unlink(2) удаляет поименованный файл из файловой системы.

Циклическая обработка изменений, о которых сообщает diff, выполняется функцией idiff. Основная идея достаточно проста: печатать порцию выходного потока diff, пропускать нежелательные данные в одном файле, а затем копировать требуемый вариант из другого файла. В программе есть много утомительных подробностей, так что она оказывается несколько больше, чем нам бы хотелось, но по частям ее довольно легко понять.

idiff(f1, f2, fin, fout) /* process diffs */

 FILE *f1, *f2, *fin, *fout;

{

 char *tempfile = "idiff.XXXXXX";

 char buf[BUFSIZ], buf2[BUFSIZ], *mktemp();

 FILE *ft, *efopen();

 int cmd, n, from1, to1, from2, to2, nf1, nf2;

 mktemp(tempfile);

 nf1 = nf2 = 0;

 while (fgets(buf, sizeof buf, fin) != NULL) {

  parse(buf, &from1, ftto1, &cmd, &from2, &to2);

  n = to1-from1 + to2-from2 + 1; /* #lines from diff */

  if (cmd == 'c')

   n += 2;

  else if (cmd == 'a')

   from1++;

  else if (cmd == 'd')

   from2++;

  printf("%s", buf);

  while (n-- > 0) {

   fgets(buf, sizeof buf, fin);

   printf("%s", buf);

  }

  do {

   printf("? ");

   fflush(stdout);

   fgets(buf, sizeof buf, stdin);

   switch (buf[0]) {

   case '>':

    nskip(f1, to1-nf1);

    ncopy(f2, to2-nf2, fout);

    break;

   case '<':

    nskip(f2, to2-nf2);

    ncopy(f1, to1-nf1, fout);

    break;

   case 'e':

    ncopy(f1, from1-1-nf1, fout);

    nskip(f2, from2-1-nf2);

    ft = efopen(tempfile, "w");

    ncopy(f1, to1+1-from1, ft);

    fprintf (ft, "---\n");

    ncopy(f2, to2+1-from2, ft);

    fclose(ft);

    sprintf(buf2, "ed %s", tempfile);

    system(buf2);

    ft = efopen(tempfile, "r");

    ncopy(ft, HUGE, fout);

    fclose(ft);

    break;

  case '!':

   system(buf+1);

   printf("!\n");

   break;

  default:

   printf("< or > or e or !\n");

   break;

  }

 } while (buf[0]!='<' && buf[0]!='>' && buf[0]!='e');

 nf1 = to1;

 nf2 = to2;

 ncopy(f1, HUGE, fout); /* can fail on very long files */

 unlink(tempfile);

}

Функция parse выполняет рутинную, но тонкую работу по разбору строк, выдаваемых diff, извлекая четыре номера строки и команду (одну из а, с или d). При этом parse немного усложняется, так как diff может выдать либо один номер строки, либо два с той или другой стороны буквы команды:

parse(s, pfrom1, pto1, pcmd, pfrom2, pto2)

 char *s;

 int *pcmd, *pfrom1, *pto1, *pfrom2, *pto2;

{

#define a2i(p) while (isdigit(*s)) p = 10*(p) + *s++ - '0'

 *pfrom1 = *pto1 = *pfrom2 = *pto2 = 0;

 a2i(*pfrom1);

 if (*s == ',') {

  s++;

  a2i(*pto1);

 } else

  *pto1 = *pfrom1;

 *pcmd = *s++;

 a2i(*pfrom2);

 if (*s == ',') {

  s++;

  a2i(*pto2);

 } else

  *pto2 = *pfrom2;

}

Макрокоманда a2i выполняет специальное преобразование из ASCII в целое в тех четырех местах, где она встречается.

Функции nskip и ncopy пропускают или копируют указанное число строк из файла:

nskip(fin, n) /* skip n lines of file fin */

 FILE *fin;

{

 char buf[BUFSIZ];

 while (n-- > 0)

  fgets(buf, sizeof buf, fin);

}

ncopy(fin, n, fout) /* copy n lines from fin to fout */

 FILE *fin, *fout;

{

 char buf[BUFSIZ];

 while (n-- > 0) {

  if (fgets(buf, sizeof buf, fin) == NULL)

   return;

  fputs(buf, fout);

 }

}

Программа idiff, если ее прервать, оставляет несколько файлов, хранящихся в /tmp. В следующей главе мы покажем, как перехватывать прерывания, чтобы убрать временные файлы, подобные использованным здесь.

Если критически подойти к zap и idiff, то оказывается, что самая трудная работа была уже кем-то сделана ранее. Эти программы только обеспечивают удобное взаимодействие с другой программой, которая обрабатывает нужную информацию. Всегда имеет смысл воспользоваться плодами чужих трудов это позволяет повысить эффективность своей работы.

Упражнение 6.13

Добавьте команду q к idiff : ответ q с автоматически выберет остаток от альтернатив '<' ; q > возьмет все оставшееся от альтернатив '>' .

Упражнение 6.14

Модифицируйте idiff так, чтобы некоторые аргументы idiff передавались к diff ; -b и -h вероятные кандидаты. Выполните еще одну модификацию idiff , позволяющую определять другой редактор, как в команде

$ idiff -е другой редактор file1 file2

Как взаимодействуют эти две модификации?

Упражнение 6.15

Измените idiff , чтобы использовать popen и pclose вместо временного файла для выходного потока diff . Как это скажется на сложности и скорости выполнения программы?

Упражнение 6.16

Если один из аргументов diff -каталог, то в этом каталоге идет поиск файла с именем, заданным другим аргументом. Но если вы попробуете сделать то же самое с idiff , то она почему-то собьётся. Объясните, что в данном случае происходит, и исправьте дефект.

 

6.9 Доступ к среде

Из Си-программы легко "добраться" до переменных в среде shell, что можно использовать для упрощения адаптации программы к окружению. Допустим, например, что размер экрана вашего терминала больше обычного (24-строкового). Чего вы сможете добиться, применив p и воспользовавшись преимуществами своего терминала? Необходимость определять размер экрана всякий раз, когда вы вводите p, надоедает:

$ p -36...

Вы могли бы всегда вставлять файл shell в свой bin:

$ cat /usr/you/bin/p

exec /usr/bin/p -36 $*

$

Третье решение модифицировать p, чтобы использовать те переменные среды, которые определяют свойства вашего терминала. Предположим, что вы определили переменную PAGESIZE в своем .profile:

PAGESIZE=36

export PAGESIZE

Функция getenv("var") ищет в среде командную переменную var и возвращает ее значение как строку символов или NULL, если переменная не определена. При наличии getenv легко модифицировать p: достаточно лишь добавить пару описаний и вызов getenv к началу основной программы.

/* p: print input in chunks (version 3) */

...

char *p, *getenv();

progname = argv[0];

if ((p=getenv("PAGESIZE")) != NULL)

 pagesize = atoi(p);

if (argc > 1 && argv[1][0] == '-') {

 pagesize = atoi(&argv[1][1]);

 argc--;

 argv++;

}

Флаги обрабатываются вслед за переменной среды, так что любой явно заданный размер страницы в конце концов заменит неявно заданный.

Упражнение 6.17

Модифицируйте idiff так, чтобы она искала в среде имя редактора, который следует применить. Измените 2, 3 и т.д., чтобы использовать PAGESIZE .

Историческая и библиографическая справка

Стандартная библиотека ввода-вывода была разработана Д. Ритчи вслед за переносимой библиотекой ввода вывода М. Леска. Оба пакета имели целью предоставить пользователю стандартные средства, чтобы можно было переносить программы без изменений с UNIX в другие системы.

Наша версия p основана на программе Г. Спенсера. Программа adb написана С. Борном, sdb Г. Катцефом, a lint С. Джонсоном.

Программа idiff в общих чертах построена на базе программы, первоначально написанной Дж. Маранзано. Сама diff детище Д. МакИлроя и основана на алгоритме, созданном независимо Г. Стоуном и В. Хантом совместно с Т. Шиманским (Hunt J. W., Szymanski Т. G. "A fast algorithm for computing longest common subsequences." CACM, May 1977.) Алгоритм "diff" описан в работе M. Д. МакИлроя и Д. В. Ханта "An algorithm for differential file comparison" (Bell Labs Computing Science Technical Report 41, 1976). В заключение приведем слова МакИлроя: "Я опробовал три различных алгоритма, прежде чем выбрал окончательный вариант. По сути diff позволяет не только хорошо понять программу, но и пересматривать ее до тех пор, пока она не станет правильной".