Главная » Программирование для UNIX » Поэкранный вывод: команда p

0

До сих пор для исследования файлов применялась команда cat. Но если файл достаточно длинный, а соединение с системой высокоскоростное, то сat выводит данные слишком быстро  – так, что даже хорошая реакция (быстрое нажатие ctl-s  и ctl-q) не помогает прочитать его.

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

$ p vis.c

$ grep  ‘#define’  *.[ch] | p

$

Эту программу лучше всего  писать на  Си, потому что  на  Си проще – стандартные  средства  не  подходят при  смешивании ввода  из  файла или канала с терминальным вводом.

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

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

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

будет аналогично

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

Имена файлов можно добавить при помощи цикла for:

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

> do

>            echo $i:

>            cat  $i

> done | p

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

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

/*  p:  печатать  входные данные порциями  (версия  1)  */

#include <stdio.h>

#define PAGESIZE         22

char        *progname;    /*  имя программы  для сообщения об  ошибке  */

main(argc,  argv) int  argc;

char  *argv[];

{

218             Глава 6. Программирование с использованием стандартного ввода–вывода

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, завершиться если это  невозможно  */

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) /*  печатать  fp  порциями размером  с pagesize */ FILE  *fp;

int pagesize;

{

static int lines  = 0;      /*  количество  строк к этому моменту  */ 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, определенной в фай ле <stdio.h>. Функция fgets(buf,size,fp) переносит следующую строку ввода (вплоть до символа новой строки, включая его) из fp в buf, добавляя завершающий символ \0; максимальное количество копируемых символов равно  size–1.  В конце файла возвращается NULL.  (Функция fgets могла бы быть  построена удобнее: она возвращает не количество символов, а  buf;  к  тому  же  не  выдается предупреждения  в  случае слишком длинной строки ввода. Правда символы не теряются, но при ходится просматривать buf, чтобы понять, что же происходит на самом  деле.)

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

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

ttyin()       /*  обработка ответа  из /dev/tty (версия  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];

}

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

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

Одно из легко реализуемых дополнений – это введение переменной pa– gesize, задающей количество строк на странице, она может устанавливаться в командной строке:

$ p n  

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

/*  p:  печатать  входные данные порциями  (версия  2)  */

int i, pagesize = PAGESIZE;

progname = argv[0];

if  (argc >  1  &&  argv[1][0]  == ‘–’)  { pagesize  =  atoi(&argv[1][1]); argc––;

argv++;

}

Функция atoi  преобразует символьную  строку  в  целое   число   (см.

atoi(3)).

Еще одним  дополнением p может быть возможность временного выхо да в конце каждой страницы для выполнения какой-то другой команды. По аналогии с ed и многими другими программами, если пользователь вводит строку, которая начинается с восклицательного знака, то оставшаяся часть строки воспринимается как команда и передается на исполнение оболочке. Такое  изменение также тривиально реализуемо, так как существует функция system(3), которая делает именно то, что нужно (но следует проявить осторожность, далее  будет  пояснено, почему). Вот и новая версия ttyin:

ttyin() /*  обработка ответа  из /dev/tty (версия  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);    /*  здесь  ОШИБКА  */ printf("!\n");

}

else        /*  ordinary  line  */ return  buf[0];

}

}

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

$ cat  /etc/passwd | p -1

root:3D.fHR5KoB.3s:0:1:S.User:/:!ed                 Вызывает  ed из  p

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

!                                                         … запутывается и выходит

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

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

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

Поэтому лучше не комбинировать cat  и vis, ведь cat  просто  копирует свои входные данные, не изменяя их, в то время как vis  их преобразу-

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

Упражнение 6.6.  Будет ли  p разумно работать в случае, если  pagesize

является неположительным числом? ~

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

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

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

Источник: Керниган Б., Пайк Р., UNIX. Программное окружение. – Пер. с англ. – СПб: Символ-Плюс, 2003. – 416 с., ил.

По теме:

  • Комментарии