|
||||||||||||||||||||||
|
Глава 5Терминалы В этой главе вы познакомитесь с некоторыми улучшениями, которые вам, возможно, захочется внести в базовое приложение из главы 2. Его, быть может, самый очевидный недостаток — пользовательский интерфейс; он достаточно функционален, но не слишком элегантен. Теперь вы узнаете, как сделать более управляемым терминал пользователя, т. е. ввод с клавиатуры и вывод на экран. Помимо этого вы научитесь обеспечивать написанным вами программам возможность получения вводимых данных от пользователя даже при наличии перенаправления ввода и гарантировать вывод данных в нужное место на экране. Несмотря на то, что заново реализованное приложение для управления базой данных компакт-дисков не увидит свет до конца главы 7, его основы вы заложите в этой главе. Глава 6 посвящена curses, которые представляют собой вовсе не древнее проклятие, а библиотеку функций, предлагающих программный код высокого уровня для управления отображением на экране терминала. Попутно вы узнаете чуть больше о размышлениях прежних профи UNIX, познакомившись с основными принципами систем Linux и UNIX и понятием терминала. Низкоуровневый доступ, представленный в этой главе, быть может именно то, что вам нужно. Большая часть того, о чем мы пишем здесь, хорошо подходит для программ, выполняющихся в окне консоли, таких как эмуляторы терминала KDE's Konsole, GNOME's gnome-terminal или стандартный X11 xterm. В этой главе вы, в частности, узнаете о: □ чтении с терминала и записи на терминал; □ драйверах терминала и общем терминальном интерфейсе (General Terminal Interface, GTI); □ структуре типа termios; □ выводе терминала и базе данных terminfo; □ обнаружении нажатия клавиш. Чтение с терминала и запись на терминалВ главе 3 вы узнали, что, когда программа запускается из командной строки, оболочка обеспечивает присоединение к ней стандартных потоков ввода и вывода. Вы получаете возможность взаимодействия с пользователем простым применением подпрограмм getcharи printfдля чтения из стандартного потока ввода и записи в стандартный поток вывода. В упражнении 5.1 в программе menu1.c вы попытаетесь переписать на языке С подпрограммы формирования меню, использующие только эти две функции. Упражнение 5.1. Подпрограммы формирования меню на языке C1. Начните со следующих строк, определяющих массив, который будет использоваться как меню, и прототип (описание) функции getchoice: #include <stdio.h> #include <stdlib.h> char *menu[] = { "a — add new record", "d — delete record", "q - quit", NULL, }; int getchoice(char *greet, char *choices[]); 2. Функция mainвызывает функцию getchoiceс образцом пунктов меню menu: int main() { int choice = 0; do { choice = getchoice("Please select an action", menu); printf("You have chosen: %c\n", choice); } while (choice != 'q'); exit(0); } 3. Теперь важный фрагмент кода — функция, которая и выводит на экран меню и считывает ввод пользователя: int getchoice(char *greet, char *choices[]) { int chosen = 0; int selected; char **option; do { printf("Choice: %s\n", greet); option = choices; while (*option) { printf("%s\n", *option); option++; } selected = getchar(); option = choices; while (*option) { if (selected == *option[0]) { chosen = 1; break; } option++; } if (!chosen) { printf("Incorrect choice, select again\n"); } } while (!chosen); return selected; } Как это работает Функция getchoiceвыводит на экран приглашение для ввода greetи меню choicesи просит пользователя ввести первый символ выбранного пункта. Далее выполняется цикл до тех пор, пока функция getcharне вернет символ, совпадающий с первой буквой одного из элементов массива option. Когда вы откомпилируете и выполните программу, то обнаружите, что она ведет себя не так, как ожидалось. Для того чтобы продемонстрировать возникающую проблему, далее приведен вариант диалога на экране терминала. $ ./menu1 Choice: Please select an action a — add new record d — delete record q — quit a You have chosen: a Choice: Please select an action a — add new record d — delete record q — quit Incorrect choice, select again Choice: Please select an action а — add new record d — delete record q — quit q You have chosen: q $ Для того чтобы сделать выбор, пользователь должен последовательно нажать клавиши <А>, <Enter>, <Q>, <Enter>. Здесь возникают, как минимум, две проблемы; самая серьезная заключается в том, что вы получаете сообщение "Incorrect choice" ("Неверный выбор") после каждого корректного выбора. Кроме того, вы еще должны нажать клавишу <Enter> (или <Return>), прежде чем программа считает введенные данные. Сравнение канонического и неканонического режимовОбе эти проблемы тесно связаны. По умолчанию ввод терминала не доступен программе до тех пор, пока пользователь не нажмет клавишу <Enter> или <Return>. В большинстве случаев это достоинство, поскольку данный способ позволяет пользователю корректировать ошибки набора с помощью клавиш <Backspace> или <Delete>. Только когда он остается доволен увиденным на экране, пользователь нажимает клавишу <Enter>, чтобы ввод стал доступен программе. Такое поведение называется каноническим или стандартным режимом. Весь ввод обрабатывается как последовательность строк. Пока строка ввода не завершена (обычно с помощью нажатия клавиши <Enter>), интерфейс терминала управляет всеми нажатыми клавишами, включая <Backspace>, и приложение не может считать ни одного символа. Прямая противоположность — неканонический режим, в котором приложение получает больше возможностей контроля над обработкой вводимых символов. Мы еще вернемся к этим двум режимам немного позже в этой главе. Помимо всего прочего, обработчик терминала в ОС Linux помогает превращать символы прерываний в сигналы (например, останавливающие выполнение программы, когда вы нажмете комбинацию клавиш <Ctrl>+<C>), он также может автоматически выполнить обработку нажатых клавиш <Backspace> и <Delete> и вам не придется реализовывать ее в каждой написанной вами программе. О сигналах вы узнаете больше в главе 11. Итак, что же происходит в данной программе? ОС Linux сохраняет ввод до тех пор, пока пользователь не нажмет клавишу <Enter>, и затем передает в программу символ выбранного пункта меню и следом за ним код клавиши <Enter>. Каждый раз, когда вы вводите символ пункта меню, программа вызывает функцию getchar, обрабатывает символ и снова вызывает getchar, немедленно возвращающую символ клавиши <Enter>. Символ, который на самом деле видит программа, — это не символ ASCII возврата каретки CR (десятичный код 13, шестнадцатеричный 0D), а символ перевода строки LF (десятичный код 10, шестнадцатеричный 0A). Так происходит потому, что на внутреннем уровне ОС Linux (как и UNIX) всегда применяет перевод строки для завершения текстовых строк, т. е. в отличие от других ОС, таких как MS-DOS, использующих комбинацию символов возврата каретки и перевода строки, ОС UNIX применяет, для обозначения новой строки только символ перевода строки. Если вводное или выводное устройство посылает или запрашивает и символ возврата каретки, в ОС Linux об этом заботится обработчик терминала. Если вы привыкли работать в MS-DOS или других системах, это может показаться странным, но одно из существенных преимуществ заключается в отсутствии в ОС Linux реальной разницы между текстовыми и бинарными файлами. Символы возврата каретки обрабатываются, только когда вы вводите или выводите их на терминал или некоторые принтеры и плоттеры. Вы можете откорректировать основной недостаток вашей подпрограммы меню, просто игнорируя дополнительный символ перевода строки с помощью программного кода, подобного приведенному далее: do { selected = getchar(); } while (selected == '\n'); Он решает непосредственно возникшую проблему, и вы увидите вывод, подобный приведенному далее: $ ./menu1 Choice: Please select an action a — add new record d — delete record q — quit a You have chosen: a Choice: Please select an action a — add new record d — delete record q — quit q You have chosen: q $ Мы вернемся позже ко второй проблеме, связанной с необходимостью нажимать клавишу <Enter>, и более элегантному решению для обработки символа перевода строки. Обработка перенаправленного выводаДля программ, выполняющихся в ОС Linux, даже интерактивных, характерно перенаправление своего ввода и вывода как в файлы, так и в другие программы. Давайте рассмотрим поведение вашей программы при перенаправлении ее вывода в файл. $ ./menu1 > file a q $ Такой результат можно было бы считать успешным, потому что вывод перенаправлен в файл вместо терминала. Однако бывают случаи, когда нужно помешать такому исходу событий или отделить приглашения или подсказки, которые пользователь должен видеть, от остального вывода, благополучно перенаправляемого в файл. О перенаправлении стандартного вывода можно судить по наличию низкоуровневого дескриптора файла, ассоциированного с терминалом. Эту проверку выполняет системный вызов isatty. Вы просто передаете ему корректный дескриптор файла, и он проверяет, связан ли этот дескриптор в данный момент с терминалом. #include <unistd.h> int isatty(int fd); Системный вызов isattyвозвращает 1, если открытый дескриптор файла fdсвязан с терминалом, и 0 в противном случае. В данной программе используются файловые потоки, но isattyоперирует только дескрипторами файлов. Для выполнения необходимого преобразования вам придется сочетать вызов isattyс подпрограммой fileno, обсуждавшейся в главе 3. Что вы собираетесь делать, если стандартный вывод stdoutперенаправлен? Просто завершить программу — не слишком хорошо, потому что у пользователя нет возможности выяснить, почему программа аварийно завершила выполнение. Вывод сообщения в stdoutтоже не поможет, поскольку оно будет перенаправлено с терминала. Единственное решение — записать сообщение в стандартный поток ошибок stderr, который не перенаправляется командой оболочки > file(упражнение 5.2). Упражнение 5.2. Проверка для выявления перенаправления вывода Внесите следующие изменения в директивы включения заголовочных файлов и функцию main программы menu1.с из упражнения 5.1. Назовите новый файл menu2.c. #include <unistd.h> ... int main() { int choice = 0; if (!isatty(fileno(stdout))) { fprintf(stderr, "You are not a terminal!\n"); exit(1); } do { choice = getchoice("Please select an action", menu); printf("You have chosen: %c\n", choice); } while (choice != 'q'); exit(0); } Теперь посмотрите на следующий пример вывода: $ ./menu2 Choice: Please select an action a — add new record d — delete record q — quit q You have chosen: q $ ./menu2 > file You are not a terminal! $ Как это работает В новом фрагменте программного кода функция isattyприменяется для проверки связи стандартного вывода с терминалом и прекращения выполнения программы при отсутствии этой связи. Это тот же самый тест, который командная оболочка использует для решения, нужно ли выводить строки приглашения. Возможно и довольно обычно перенаправление и stdout, и stderrс терминала на другое устройство. Вы можете направить поток ошибок в другой файл: $ ./menu2 >file 2>file.error $ или объединить оба выводных потока в одном файле: $ ./menu2 >file 2>&1 $ (Если вы не знакомы с перенаправлением вывода, прочтите еще раз главу 2, в которой мы более подробно рассматриваем синтаксические правила, связанные с ним.) В данном случае вам нужно отправить сообщение непосредственно на терминал пользователя. Диалог с терминаломЕсли нужно защитить части вашей программы, взаимодействующие с пользователем, от перенаправления, но разрешить его для других входных и выходных данных, вы должны отделить общение с пользователем от потоков stdoutи stderr. Это можно сделать, непосредственно считывая данные с терминала и прямо записывая данные на терминал. Поскольку ОС Linux с самого начала создавалась, как многопользовательская система, включающая, как правило, множество терминалов, как непосредственно подсоединенных, так и подключенных по сети, как вы сможете определить тот терминал, который следует использовать? К счастью, Linux и UNIX облегчают жизнь, предоставляя специальное устройство /dev/tty, которое всегда является текущим терминалом или сеансом работы в системе (login session). Поскольку ОС Linux все интерпретирует как файлы, вы можете выполнять обычные файловые операции для чтения с устройства /dev/tty и записи на него. В упражнении 5.3 вы исправите программу выбора пункта меню так, чтобы можно было передавать параметры в подпрограмму getchoiceи благодаря этому лучше управлять выводом. Назовите ее menu3.c. Упражнение 5.3. Применение /dev/tty Загрузите файл menu2.c и измените программный код так, чтобы входные и выходные данные приходили с устройства /dev/tty и направлялись на это устройство. #include <stdio.h> #include <unistd.h> #include <stdlib.h> char *menu[] = { "a — add new record", "d — delete record", "q - quit", NULL, }; int getchoice(char* greet, char* choices[], FILE* in, FILE* out); int main() { int choice = 0; FILE* input; FILE* output; if (!isatty(fileno(stdout))) { fprintf(stderr, "You are not a terminal, OK.\n"); } input = fopen("/dev/tty", "r"); output = fopen("/dev/tty", "w"); if (!input || !output) { fprintf(stderr, "Unable to open /dev/tty\n"); exit(1); } do { choice = getchoice("Please select an action", menu, input, output); printf("You have chosen: %c\n", choice); } while (choice != 'q'); exit(0); } int getchoice(char* greet, char *choices[], FILE* in, FILE *out) { int chosen = 0; int selected; char **option; do { fprintf(out, "Choice: %s\n", greet); option = choices; while (*option) { fprintf(out, "%s\n", *option); option++; } do { selected = fgetc(in); } while(selected == '\n'); option = choices; while (*option) { if (selected == *option[0]) { chosen = 1; break; } option++; } if (!chosen) { fprintf(out, "Incorrect choice, select again\n"); } } while (!chosen); return selected; } Теперь, когда вы выполните программу с перенаправленным выводом, вы сможете увидеть строки приглашения, а стандартный вывод программы (обозначающий выбранные пункты меню) перенаправляется в файл, который можно просмотреть позже: $ ./menu3 > file You are not a terminal, OK. Choice: Please select an action a — add new record d — delete record q — quit d Choice: Please select an action a — add new record d - delete record q — quit q $ cat file You have chosen: d You have chosen: q Драйвер терминала A и общий терминальный интерфейсИногда программе нужно более мощные средства управления терминалами, чем простые файловые операции. ОС Linux предоставляет ряд интерфейсов, позволяющих управлять поведением драйвера терминала и обеспечивающих больше возможностей управления вводом и выводом терминала. ОбзорКак показано на рис. 5.1, вы можете управлять терминалом с помощью вызовов набора функций общего терминального интерфейса (General Terminal Interface, GTI), разделяя их на применяемые для чтения и для записи. Такой подход сохраняет ясность интерфейса данных (чтение/запись), позволяя при этом искусно управлять поведением терминала. Нельзя сказать, что терминальный интерфейс ввода/вывода очень понятен — он вынужден иметь дело с множеством разнообразных физических устройств. Рис. 5.1 В терминологии UNIX управляющий интерфейс устанавливает "порядок обслуживания линий", обеспечивающий программе ощутимую гибкость в задании поведения драйвера терминала. К основным функциям, которыми вы можете управлять, относятся следующие: □ редактирование строки — применение для редактирования клавиши <Backspace>; □ буферизация — считывание символов сразу или после настраиваемой задержки; □ отображение — управление отображением так же, как при считывании паролей; □ CR/LF — отображение для ввода и вывода: что происходит при выводе символа перевода строки (\n); □ скорости передачи данных по линии — редко применяется для консоли ПК, эти скорости очень важны для модемов и терминалов на линиях последовательной передачи. Аппаратная модельПеред тем как подробно рассматривать общий терминальный интерфейс, очень важно проанализировать аппаратную модель, предназначенную для управления. Концептуальная схема (физическая модель на некоторых старых узлах UNIX подобна данной) включает машину с ОС UNIX, подключенную через последовательный порт с модемом и далее по телефонной линии с другим модемом к удаленному терминалу (рис. 5.2). На деле это просто вариант установки, применявшийся некоторыми малыми провайдерами интернет-услуг "на заре туманной юности" Интернета. Эта модель отдаленно напоминает организацию "клиент — сервер", при использовании которой программа выполняется на большом компьютере, а пользователи работают на терминалах ввода/вывода. Рис. 5.2 Если вы работаете на ПК под управлением ОС Linux, эта модель может показаться чересчур сложной. Однако, поскольку у обоих авторов есть модемы, мы можем при желании использовать программу эмуляции терминала, например, minicom для запуска удаленного сеанса работы в системе на любой другой машине, подобной этой, с помощью пары модемов и телефонной линии связи. Конечно, сегодня быстрый широкополосный доступ вытеснил из потребления эту рабочую модель, но она до сих пор не лишена некоторых достоинств. Преимущество применения этой аппаратной модели заключается в том, что в большинстве реальных ситуаций возникает потребность в некотором сокращенном варианте этого наиболее сложного случая. Удовлетворить эти потребности будет гораздо легче, если приведенная модель сохранит подобные функциональные возможности. Структура типа termiosТип termios— стандартный интерфейс, заданный стандартом POSIX и похожий на интерфейс termioсистемы System V. Интерфейс терминала управляется значениями в структуре типа termiosи использует небольшой набор вызовов функций. И то и другое определено в заголовочном файле termios.h. Примечание Значения, которые можно изменять для управления терминалом, разделены на группы, относящиеся к следующим режимам: □ ввод; □ вывод; □ управление; □ локальный; □ специальные управляющие символы. Минимальная структура типа termiosобычно объявляется следующим образом (хотя в стандарте X/Open разрешено включение дополнительных полей): #include <termios.h> struct termios { tcflag_t c_iflag; tcflag_t c_oflag; tcflag_t c_cflag; tcflag_t c_lflag; cc_t c_cc[NCCS]; }; Имена элементов структуры соответствуют пяти типам параметров из предыдущего перечня. Инициализировать структуру типа termiosдля терминала можно, вызвав функцию tcgetattrсо следующим прототипом или описанием: #include <termios.h> int tcgetattr(int fd, struct termios *termios_p); Этот вызов записывает текущие значения переменных интерфейса терминала в структуру, на которую указывает параметр termios_p. Если впоследствии эти значения будут изменены, вы сможете перенастроить интерфейс терминала с помощью функции tcsetattrследующим образом: #include <termios.h> int tcsetattr(int fd, int actions, const struct termios *termios_p); Поле actionsфункции tcsetattrуправляет способом внесения изменений. Есть три варианта: □ TCSANOW— изменяет значения сразу; □ TSCADRAIN— изменяет значения, когда текущий вывод завершен; □ TCSAFLUSH— изменяет значения, когда текущий вывод завершен, но отбрасывает любой ввод, доступный в текущий момент и все еще не возвращенный вызовом read. Примечание Теперь рассмотрим более подробно режимы и связанные с ними вызовы функций. Некоторые характеристики режимов довольно специализированные и редко применяются, поэтому мы остановимся только на основных. Если вы хотите знать больше, просмотрите страницы интерактивного справочного руководства вашей системы либо скопируйте стандарт POSIX или X/Open. Наиболее важный режим, который следует принять во внимание при первом прочтении, — локальный (local). Канонический и неканонический режимы — решение второй проблемы в вашем первом приложении: пользователь должен нажимать клавишу <Enter> или <Return> для чтения программой входных данных. Вам следует заставить программу ждать всю строку ввода или набрасываться на ввод, как только он набран на клавиатуре. Режимы вводаРежимы ввода управляют тем, как обрабатывается ввод (символы, полученные драйвером терминала от последовательного порта или клавиатуры) до передачи его в программу. Вы управляете вводом, устанавливая флаги в элементе c_iflagструктуры termios. Все флаги определены как макросы и могут комбинироваться с помощью поразрядной операции OR. Это свойственно всем режимам терминала. В элементе c_iflagмогут применяться следующие макросы: □ BRKINT— генерирует прерывание, когда в линии связи обнаруживается разрыв (потеря соединения); □ IGNBRK— игнорирует разрывы соединения в линии связи; □ ICRNL— преобразует полученный символ возврата каретки в символ перехода на новую строку; □ IGNCR— игнорирует полученные символы возврата каретки; □ INLCR— преобразует полученные символы перехода на новую строку в символы возврата каретки; □ IGNPAR— игнорирует символы с ошибками четности; □ INCPK— выполняет контроль четности у полученных символов; □ PARMRK— помечает ошибки четности; □ ISTRIP— обрезает (до семи битов) все входные символы; □ IXOFF— включает программное управление потоком при вводе; □ IXON— включает программное управление потоком при выводе. Примечание Вам не придется часто изменять режимы ввода, поскольку обычно стандартные значения — наиболее подходящие, и поэтому мы больше не будем их обсуждать. Режимы выводаЭти режимы управляют способом обработки выводимых символов, т.е. тем, как символы, полученные от программы, обрабатываются перед передачей на последовательный порт или экран. Как и следовало ожидать, многие из них — оборотная сторона режимов ввода. Есть несколько дополнительных флагов, которые связаны в основном с разрешениями для медленных терминалов, которым требуется время для обработки таких символов, как возвраты каретки. Почти все они либо избыточны (поскольку терминалы стали быстрее) или лучше обрабатываются с помощью базы данных характеристик терминала terminfo, которую вы примените позже в этой главе. Вы управляете режимами вывода, устанавливая флаги элемента c_oflagструктуры типа termios. В элементе c_oflagмогут применяться следующие макросы: □ OPOST— включает обработку вывода; □ ONLCR— преобразует в символ перевода строки пару символов возврат каретки/перевод строки; □ OCRNL— преобразует любой символ возврата каретки в выводе в символ перевода строки; □ ONOCR— не выводит символ возврата каретки в столбце 0; □ ONLRET— символ перехода на новую строку выполняет возврат каретки; □ OFILL— посылает символы заполнения для формирования задержки; □ OFDEL— применяет символ DELкак заполнитель вместо символа NULL; □ NLDLY— выбор задержки для символа перехода на новую строку; □ CRDLY— выбор задержки для символа возврата каретки; □ TABDLY— выбор задержки для символа табуляции; □ BSDLY— выбор задержки для символа Backspace; □ VTDLY— выбор задержки для символа вертикальной табуляции; □ FFDLY— выбор задержки для символа прокрутки страницы. Примечание Режимы вывода тоже обычно не используются, поэтому мы не будем их обсуждать в дальнейшем. Режимы управленияЭти режимы управляют аппаратными характеристиками терминала. Вы задаете режимы управления, устанавливая флаги элемента c_cflagструктуры типа termios, включающие следующие макросы: □ CLOCAL— игнорирует управление линиями с помощью модема; □ CREAD— включает прием символов; □ CS5— использует пять битов в отправляемых и принимаемых символах; □ CS6— использует шесть битов в отправляемых и принимаемых символах; □ CS7— использует семь битов в отправляемых и принимаемых символах; □ CS8— использует восемь битов в отправляемых и принимаемых символах; □ CSTOPB— устанавливает два стоповых бита вместо одного; □ HUPCL— выключает управление линиями модема при закрытии; □ PARENB— включает генерацию и проверку четности; □ PARODD— применяет контроль нечетности вместо контроля четности. Примечание Режимы управления применяются в основном при подключении к модему последовательной линии связи, хотя их можно использовать и при диалоге с терминалом. Обычно легче изменить настройку терминала, чем изменять стандартное поведение линий связи с помощью режимов управления структуры termios. Локальные режимыЭти режимы управляют разнообразными характеристиками терминала. Вы можете задать локальный режим, устанавливая флаги элемента c_iflagструктуры termiosс помощью следующих макросов: □ ECHO— включает локальное отображение вводимых символов; □ ECHOE— выполняет комбинацию Backspace, Space, Backspaceпри получении символа ERASE(стереть); □ ECHOK— стирает строку при получении символа KILL; □ ECHONL— отображает символы перехода на новую строку; □ ICANON— включает стандартную обработку ввода (см. текст, следующий за данным перечнем); □ IEXTEN— включает функции, зависящие от реализации; □ ISIG— включает генерацию сигналов; □ NOFLSH— отключает немедленную запись очередей; □ TOSTOP— посылает сигнал фоновым процессам при попытке записи. Два самых важных флага в этой группе — ECHO, позволяющий подавлять отображение вводимых символов, и ICANON, переключающий терминал в один из двух различных режимов обработки принимаемых символов. Если установлен флаг ICANON, говорится, что строка в каноническом режиме, если нет, то строка в неканоническом режиме. Специальные управляющие символыСпециальные управляющие символы — это коллекция символов подобных символам от комбинации клавиш <Ctrl>+<C>, действующих особым образом, когда пользователь вводит их. В элементе c_ccструктуры termiosсодержатся символы, отображенные на поддерживаемые функции. Позиция каждого символа (его номер в массиве) определяется макросом, других ограничений для управляющих символов не задано. Массив c_ccиспользуется двумя очень разными способами, зависящими от того, установлен для терминала канонический режим (т.е. установлен флаг ICANONв элементе c_lflagструктуры termios) или нет. Важно понять, что в двух разных режимах есть некоторое взаимное наложение при применении номеров элементов массива. По этой причине никогда не следует смешивать значения для этих двух режимов. Для канонического режима применяются следующие индексы: □ VEOF— символ EOF; □ VEOL— дополнительный символ конца строки EOL; □ VERASE— символ ERASE; □ VINTR— символ прерывания INTR; □ VKILL— символ уничтожения KILL; □ VQUIT— символ завершения QUIT; □ VSUSP— символ приостанова SUSP; □ VSTART— символ запуска START; □ VSTOP— символ останова STOP. Для канонического режима применяются следующие индексы: □ VINTR— символ INTR; □ VMIN— минимальное значение MIN; □ VQUIT— символ QUIT; □ VSUSP— символ SUSP; □ VTIME— время ожидания TIME; □ VSTART— символ START; □ VSTOP— символ STOP. Символы Поскольку для более сложной обработки вводимых символов специальные символы и неканонические значения очень важны, мы описываем их в табл. 5.1. Таблица 5.1
Значения TIMEи MINприменяются только в неканоническом режиме и действуют вместе для управления считыванием входных данных. Вместе они управляют действиями при попытке программы прочесть дескриптор файла, ассоциированный с терминалом. Возможны четыре варианта. □ MIN = 0и TIME = 0. В этом случае вызов readвсегда завершается сразу же. Если какие-то символы доступны, они будут возвращены, если нет, то readвернет ноль, и никакие символы не будут считаны. □ MIN = 0и TIME > 0. В этом случае вызов readзавершится, когда все доступные символы будут считаны или когда пройдет TIMEдесятых долей секунды. Если нет прочитанных символов из-за превышения отпущенного времени, readвернет 0. В противном случае он вернет количество прочитанных символов. □ MIN > 0и TIME = 0. В этом случае вызов readбудет ждать до тех пор, пока можно будет считать MINсимволов, и затем вернет это количество символов. В случае конца файла возвращается 0. □ MIN > 0и TIME > 0. Это самый сложный случай. После вызова readждет получения символа. Когда первый символ получен, каждый раз при получении последующего символа запускается межсимвольный таймер (или перезапускается, если он уже был запущен). Вызов readзавершится, когда либо можно будет считать MINсимволов, либо межсимвольное время превысит TIMEдесятых долей секунды. Это может пригодиться для подсчета разницы между единственным нажатием клавиши <Esc> и запуском функциональной клавиатурной escape-последовательности. Тем не менее следует знать, что сетевые соединения или высокая загрузка процессора могут полностью стереть такие полезные сведения о времени. Установив неканонический режим и используя значения MINи TIME, программы могут выполнять посимвольную обработку ввода. Доступ к режимам терминала из командной оболочки Если вы хотите просмотреть параметры termios, находясь в командной оболочке, примените следующую команду для получения их списка: $ stty -a На установленных у авторов системах Linux, обладающих структурами termiosс некоторыми расширениями по сравнению со стандартными, получен следующий вывод: speed 38400 baud; rows 24; columns 80; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O, min = 1; time = 0; -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmirk -inpck -istrip -inlcr -igncr icrnl -ixon -ixoff -iuclc -ixany -imaxbe1 iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel n10 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke Среди прочего, как видите, символ EOF— это <Ctrl>+<D>, и включено отображение. Экспериментируя с установками терминала, легко получить в результате терминал в нестандартном режиме, что затруднит его дальнейшее использование. Есть несколько способов справиться с этой трудностью. □ Первый способ — применить следующую команду, если ваша версия sttyподдерживает ее: $ stty sane Если вы потеряли преобразование клавиши возврата каретки в символ перехода на новую строку (который завершает строку), возможно, потребуется ввести stty sane, но вместо нажатия клавиши <Enter> нажать комбинацию клавиш <Ctrl>+<J> (которая обозначает переход на новую строку). □ Второй способ — применить команду stty -gи записать текущие установки sttyв форму, готовую к повторному считыванию. В командной строке вы можете набрать следующее: $ stty -g > save_stty ... <эксперименты с параметрами> ... $ stty $(cat save_stty) В финальной команде sttyвам все еще придется использовать комбинацию клавиш <Ctrl>+<J> вместо клавиши <Enter>. Ту же самую методику можно применить и в сценариях командной оболочки. save_stty="$(stty -g)" <изменение stty-параметров> stty $save_stty □ Если вы все еще в тупике, третий способ — перейти на другой терминал, применить команду psдля поиска оболочки, которую вы сделали непригодной, и затем использовать команду kill hup <id процесса>для принудительного завершения этой командной оболочки. Поскольку перед выводом регистрационного приглашения параметры sttyвсегда восстанавливаются, у вас появится возможность нормально зарегистрироваться в системе еще раз. Задание режимов терминала из командной строки Вы также можете применять команду sttyдля установки режимов терминалов непосредственно из командной строки. Для установки режима, в котором ваш сценарий командной оболочки сможет выполнять посимвольное считывание, вы должны отключить канонический режим и задать 1 и 0. Команда будет выглядеть следующим образом: $ stty -icanon min 1 time 0 Теперь терминал будет считывать символы немедленно, вы можете попробовать выполнить еще раз первую программу menu1. Вы увидите, что она работает, как первоначально и предполагалось. Вы также могли бы улучшить вашу попытку проверки пароля (см. главу 2), отключив отображение перед приглашением ввести пароль. Команда, выполняющая это действие, должна быть следующей: $ stty -echo Примечание Скорость терминалаПоследняя функция, обслуживаемая структурой termios, — манипулирование скоростью линии передачи. Для этой скорости не определен отдельный элемент структуры; она управляется вызовами функций. Скорости ввода и вывода обрабатываются отдельно. Далее приведены четыре прототипа вызовов: #include <termios.h> speed_t cfgetispeed(const struct termios *); speed_t cfgetospeed(const struct termios *); int cfsetispeed(struct termios *, speed_t speed); int cfsetospeed(struct termios *, speed_t speed); Обратите внимание на то, что они воздействуют на структуру termios, а не непосредственно на порт. Это означает, что для установки новой скорости вы должны считать текущие установки с помощью функции tcgetattr, задать скорость, применив приведенные вызовы, и затем записать структуру termiosобратно с помощью функции tcsetattr. Скорость линии передачи изменится только после вызова tcsetattr. В вызовах перечисленных функций допускается задание разных значений скорости speed, но к основным относятся следующие константы: □ B0— отключение терминала; □ B1200— 1200 бод; □ B2400— 2400 бод; □ B9600— 9600 бод; □ B19200— 19 200 бод; □ B38400— 38 400 бод. Не существует скоростей выше 38 400 бод, задаваемых стандартом, и стандартного метода обслуживания последовательных портов на более высоких скоростях. Примечание Дополнительные функцииЕсть небольшое число дополнительных функций для управления терминалами. Они работают непосредственно с дескрипторами файлов без необходимости считывания и записывания структур типа termios. #include <termios.h> int tcdrain(int fd); int tcflow(int fd, int flowtype); int tcflush(int fd, int in_out_selector); Функции предназначены для следующих целей: □ tcdrain— заставляет вызвавшую программу ждать до тех пор, пока не будет отправлен весь поставленный в очередь вывод; □ tcflow— применяется для приостановки или возобновления вывода; □ tcflush— может применяться для отказа от входных или выходных данных либо и тех, и других. Теперь, когда мы уделили довольно много внимания структуре termios, давайте рассмотрим несколько практических примеров. Возможно, самый простой из них — отключение отображения при чтении пароля (упражнение 5.4). Это делается сбрасыванием флага echo. Упражнение 5.4. Программа ввода пароля с применение termios 1. Начните вашу программу password.с со следующих определений: #include <termios.h> #include <stdio.h> #include <stdlib.h> #define PASSWORD_LEN 8 int main() { struct termios initialrsettings, newrsettings; char password[PASSWORD_LEN + 1]; 2. Далее добавьте строку, считывающую текущие установки из стандартного ввода и копирующую их в только что созданную вами структуру типа termios: tcgetattr(fileno(stdin), &initialrsettings); 3. Создайте копию исходных установок, чтобы восстановить их в конце. Сбросьте флаг ECHOв переменной newrsettingsи запросите у пользователя его пароль: newrsettings = initialrsettings; newrsettings.с_lflag &= ~ЕСНО; printf("Enter password: "); 4. Далее установите атрибуты терминала в newrsettings и считайте пароль. И наконец, восстановите первоначальные значения атрибутов терминала и выведите пароль на экран, чтобы свести на нет все предыдущие усилия по обеспечению безопасности: if (tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) { fprintf(stderr, "Could not set attributes\n"); } else { fgets(password, PASSWORD_LEN, stdin); tcsetattr(fileno(stdin), TCSANOW, &initialrsettings); fprintf(stdout, "\nYou entered %s\n", password); } exit(0); } Когда вы выполните программу, то увидите следующее: $ ./password Enter password: You entered hello $ Как это работает В этом примере слово helloнабирается на клавиатуре, но не отображается на экране в строке приглашения Enter password:. Никакого вывода нет до тех пор, пока пользователь не нажмет клавишу <Enter>. Будьте осторожны и изменяйте с помощью конструкции X&=~FLAG(которая очищает бит, определенный флагом FLAGв переменной X) только те флаги, которые вам нужно изменить. При необходимости можно воспользоваться конструкцией X|=FLAGдля установки одиночного бита, определяемого FLAG, хотя в предыдущем примере она не понадобилась. Для установки атрибутов применяется действие TCSAFLUSHдля очистки буфера клавиатуры, символов, которые пользователи вводили до того, как программа подготовилась к их считыванию. Это хороший способ заставить пользователей не начинать ввод своего пароля, пока не отключено отображение. Перед завершением программы вы также восстанавливаете первоначальные установки. Другой распространенный пример использования структуры termios— перевод терминала в состояние, позволяющее вам считывать каждый набранный символ (упражнение 5.5). Для этого отключается канонический режим и используются параметры MINи TIME. Упражнение 5.5. Считывание каждого символа Применяя только что полученные знания, вы можете изменить программу menu. Приведенная далее программа menu4.c базируется на программе menu3.c и использует большую часть кода из файла password.с, включенного в нее. Внесенные изменения выделены цветом и объясняются в пунктах описания. 1. Прежде всего, вам следует, включить новый заголовочный файл в начало программы: #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <termios.h> char *menu[] = { "a — add new record", "d — delete record", "q - quit", NULL, }; 2. Затем нужно объявить пару новых переменных в функции main: int getchoice(char *greet, char *choices[], FILE *in, FILE *out); int main() { int choice = 0; FILE *input; FILE *output; struct termios initial_settengs, new_settings; 3. Перед вызовом функции getchoiceвам следует изменить характеристики терминала, этим определяется место следующих строк: if (!isatty(fileno(stdout))) { fprintf(stderr, "You are not a terminal, OK.\n"); } input = fopen("/dev/tty", "r"); output = fopen("/dev/tty", "w"); if (!input || !output) { fprintf(stderr, "Unable to open /dev/tty\n"); exit(1); } tcgetattr(fileno(input), &initial_settings); new_settings = initial_settings; new_settings.c_lfag &= ~ICANON; new_settings.c_lflag &= ~ECHO; new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; new_settings.c_lflag &= ~ISIG; if (tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) { fprintf(stderr, "could not set attributes\n"); } 4. Перед завершением вы также должны вернуть первоначальные значения: do { choice = getchoice("Please select an action", menu, input, output); printf("You have chosen: %c\n", choice); } while (choice != 'q'); tcsetattr(fileno(input), TCSANOW, &initial_settings); exit(0); } 5. Теперь, когда вы в неканоническом режиме, необходимо проверить на соответствие возвраты каретки, поскольку стандартное преобразование CR (возврат каретки) в LF (переход на новую строку) больше не выполняется: int getchoice (char *greet, char *choices[], FILE *in, FILE *out) { int chosen = 0; int selected; char **option; do { fprintf(out, "Choice: %s\n", greet); option = choices; while (*option) { fprintf(but, "%s\n", *option); option++; } do { selected = fgetc(in); } while (selected == '\n' || selected == '\r'); option = choices; while (*option) { if (selected == *option[0]) { chosen = 1; break; } option++; } if (!chosen) { fprintf(out, "Incorrect choice, select again\n"); } } while(!chosen); return selected; } Пока вы не устроите все иначе, теперь, если пользователь нажмет в вашей программе комбинацию клавиш <Ctrl>+<C>, программа завершится. Вы можете отключить обработку этих специальных символов, очистив флаг ISIGв локальных режимах. Для этого в функцию mainвключается следующая строка: new_settings.c_lflag &= ~ISIG; Если вы внесете эти изменения в вашу программу меню, то будете получать немедленный отклик, и вводимый вами символ не будет отображаться на экране. $ ./menu4 Choice: Please select an action a — add new record d — delete record q — quit You have chosen: a Choice: Please select an action a — add new record d — delete record q — quit You have chosen: q $ Если вы нажмете комбинацию клавиш <Ctrl>+<C>, символ будет передан прямо в программу и будет истолкован, как неверный выбор. Вывод терминалаС помощью структуры типа termiosвы управляли вводом с клавиатуры, но было бы хорошо иметь такой же уровень управления выходными данными, отображаемыми на экране терминала. В начале главы вы применяли функцию printfдля вывода символов на экран, не имея при этом возможности помещать их в определенное место экрана. Тип терминалаВо многих системах UNIX применяются терминалы, несмотря на то, что сегодня во многих случаях "терминал" может на самом деле быть ПК, выполняющим программу эмуляции терминала или терминальным приложением в оконной среде, таким как xterm в графической оболочке X11. Исторически существовало очень большое число аппаратных терминалов разных производителей. Несмотря на то, что почти все они применяют escape-последовательности (строки символов, начинающиеся с escape-символа) для управления положением курсора и другими атрибутами, такими как жирное начертание или мерцание, способы реализации управления при этом слабо стандартизованы. У некоторых старых моделей терминалов также разные характеристики прокрутки экрана, который может очищаться или не очищаться, когда посылается символ Backspace, и т.д. Примечание Такое разнообразие аппаратных моделей терминалов было бы огромной проблемой для программистов, пытающихся написать программы управления экраном, выполняющиеся на терминалах разных типов. Например, терминал ANSI применяет последовательность символов Escape, [, Aдля перемещения курсора вверх на одну строку. Терминал ADM-За (очень распространенный несколько лет назад) использует один управляющий символ от комбинации клавиш <Ctrl>+<K>. Написание программы, имеющей дело с терминалами разнообразных типов, которые могут быть подключены в системе UNIX, кажется крайне устрашающей задачей. Такой программе понадобится разный программный код для терминала каждого типа. Как ни странно, решение существует в пакете, известном как terminfo. Вместо необходимости обслуживания любого типа терминала в каждой программе, ей достаточно просмотреть базу данных типов терминалов для получения корректной информации. В большинстве современных систем UNIX, включая Linux, эта база данных объединена с другим пакетом, названным curses, о котором вы узнаете в следующей главе. Для применения функций terminfo вы, как правило, должны подключить заголовочный файл curses.h пакета curses и собственный заголовочный файл term.h пакета terminfo. В некоторых системах Linux вам, возможно, придется применять реализацию curses, известную как ncurses, и включить файл ncurses.h для предоставления прототипов вашим функциям terminfo. Установите тип вашего терминалаОкружение ОС Linux содержит переменную TERM, которая хранит тип используемого терминала. Обычно она устанавливается системой автоматически во время регистрации в системе. Системный администратор может задать тип терминала по умолчанию для каждого непосредственно подключенного терминала и может сформировать подсказку с типом терминала для удаленных сетевых пользователей. Значение TERMможет быть передано rloginчерез telnet. Пользователь может запросить командную оболочку о соображениях системы по поводу используемого им или ею терминала: $ echo $TERM xterm $ В данном случае оболочка выполняется из программы, называемой xterm — эмулятора терминала для графической оболочки X Window System, или программы, обеспечивающей "такие же функциональные возможности, как KDE's Konsole или GNOME's gnome-terminal. Пакет terminfo содержит базу данных характеристик и управляющих escape-последовательностей для большого числа терминалов и предоставляет единообразный программный интерфейс для их использования. Отдельная программа, таким образом, сможет извлечь выгоду от применения новых моделей терминалов по мере расширения базы данных и не заботиться о поддержке множества разных терминалов. Характеристики терминалов в terminfo описываются с помощью атрибутов. Они хранятся в наборе откомпилированных файлов terminfo, которые обычно находятся в каталогах /usr/lib/terminfo или /usr/share/terminfo. Для каждого терминала (и многих принтеров, которые тоже могут быть заданы в terminfo) есть файл, в котором определены характеристики терминала и способ доступа к его функциям. Для того чтобы не создавать слишком большого каталога, реальные файлы хранятся в подкаталогах, имена которых — первый символ типа терминала. Так определение терминала VT100 можно найти в файле …terminfo/v/vt100. Файлы terminfo пишутся по одному на каждый тип терминала в исходном формате, пригодном (или почти пригодном!) для чтения, который затем компилируется командой ticв более компактный и эффективный формат, используемый прикладными программами. Странно, стандарт X/Open ссылается на описания исходного и откомпилированного формата, но не упоминает команду tic, необходимую для реального преобразования исходного формата в откомпилированный. Для вывода пригодной для чтения версии откомпилированного элемента набора terminfo можно использовать программу infocmp. Далее приведен пример файла terminfo для терминала VT100: $ infocmp vt100 vt100|vt100-am|dec vt100 (w/advanced video), am, mir, msgr, xenl, xon, cols#80, it#8, lines#24, vt#3, acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>, clear=\E[H\E[J$<50>, cr=\r, csr=\E[%i%p1%d;%p2%dr, cub=\E[%p1%dD, cub1=\b, cud=\E[%p1%dB, cud1=\n, cuf=\E[%p1%dC, cuf1=\E[C$<2>, cup=\E[%i%p1%d; %p2%dH$<5>, cuu=\E[%p1%dA, cuu1=\E[A$<2>, ed=\E[J$<50>, el=\E[K$<3>, el1=\E[1K$<3>, enacs=\E(B\E)0, home=\E[H, ht=\t, hts=\EH, ind=\n, ka1=\EOq, ka3=\EOs, kb2=\EOr, kbs=\b, kc1=\EOp, kc3=\EOn, kcub1=\EOD, kcud1=\EOB, kcuf1=\EOC, kcuu1=\EOA, kent=\EOM, kf0=\EOy, kf1=\EOP, kf10=\EOx, kf2=\EOQ, kf3=\EOR, kf4=\EOS, kf5=\EOt, kf6=\EOu, kf7=\EOv, kf8=\EOl, kf9=\EOw, rc=\E8, rev=\E[7m$<2>, ri=\EM$<5>, rmacs=^O, rmkx=\E[?11\E>, rmso=\E[m$<2>, rmul=\E[m$<2>, rs2=\E>\E[?31\E[?41\E[?51\E[?7h\E[?8h, sc=\E7, sgr=\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t^N%e^O%;, sgr0=\E[m^0$<2>, smacs=^N, smkx=\E[?1h\E=, smso=\E[1;7m$<2>; smul=\E[4m$<2>, tbc=\E[3g, Каждое определение в terminfoсостоит из трех типов элементов. Каждый элемент называется capname(имя характеристики) и определяет характеристику терминала. Булевы или логические характеристики просто обозначают наличие или отсутствие поддержки терминалом конкретного свойства. Например, булева характеристика xonприсутствует, если терминал поддерживает управление потоком XON/XOFF. Числовые характеристики определяют размеры или объемы, например lines— это количество строк на экране, a cols— количество столбцов. Число отделяется от имени характеристики символом #. Для описания терминала с 80 столбцами и 24 строками следует написать cols#80, lines#24. Строковые характеристики немного сложнее. Они применяются для двух разных типов характеристик: определения строк вывода, необходимых для доступа к функциям терминала, и определения строк ввода, которые будут получены, когда пользователь нажмет определенные клавиши, обычно функциональные или специальные клавиши на цифровой клавиатуре. Некоторые строковые параметры очень просты, например el, что означает "стереть до конца строки". Для того чтобы сделать это на терминале VT100, потребуется escape-последовательность Esc, [, K. В исходном формате terminfo это записывается как еl=\Е[K. Специальные клавиши определены аналогичным образом. Например, функциональная клавиша <F1> на терминале VT100 посылает последовательность Esc, O, P, которая определяется как kf1=\EOP. Все несколько усложняется, если escape-последовательности требуются какие-либо параметры. Большинство терминалов могут перемещать курсор в заданные строку и столбец. Ясно, что неразумно хранить отдельную характеристику для каждой точки экрана, в которую можно переместить курсор, поэтому применяется общая строковая характеристика с параметрами, определяющими значения, которые вставляются при использовании характеристики. Например, терминал VT100 использует последовательность Esc, [, <row>, <col>, Hдля перемещения курсора в заданную позицию. В исходном формате terminfo это записывается довольно устрашающе: cup=\E[%i%p1%d;%p2%dH$<5>. Эта строка означает следующее: □ \E— послать escape-символ; □ [— послать символ [; □ %i— дать приращение аргументам; □ %p1— поместить первый аргумент в стек; □ %d— вывести число из стека как десятичное; □ ;— послать символ ;; □ %р2— поместить второй аргумент в стек; □ %d— вывести число из стека как десятичное; □ H—послать символ H. Данная запись кажется сложной, но позволяет задавать параметры в строгом порядке, не зависящем от порядка, в котором терминал ожидает их появления в финальной escape-последовательности. Приращение аргументов %iнеобходимо, поскольку стандартная адресация курсора задается, начиная от верхнего левого угла экрана (0, 0), а терминал VT100 обозначает начальную позицию курсора как (1, 1). Заключительные символы $<5>означают, что для обработки терминалом перемещения курсора требуется задержка, эквивалентная времени вывода пяти символов. Примечание Применение характеристик terminfoТеперь, когда вы знаете, как определить характеристики терминала, нужно научиться обращаться к ним. Когда используется terminfo, прежде всего вам нужно задать тип терминала, вызвав функцию setupterm. Она инициализирует структуру TERMINALдля текущего типа терминала. После этого вы сможете запрашивать характеристики терминала и применять его функциональные возможности. Делается это с помощью вызова setupterm, подобного приведенному далее: #include <term.h> int setupterm(char *term, int fd, int *errret); Библиотечная функция setuptermзадает текущий тип терминала в соответствии с заданным параметром term. Если term— пустой указатель, применяется переменная окружения TERM. Открытый дескриптор файла, предназначенный для записи на терминал, должен передаваться в параметре fd. Результат функции хранится в целой переменной, на которую указывает errret, если это не пустой указатель. Могут быть записаны следующие значения: □ -1 — нет базы данных terminfo; □ 0 — нет совпадающего элемента в базе данных terminfo; □ 1 — успешное завершение. Функция setuptermвозвращает константу OKв случае успешного завершения и ERRв случае сбоя. Если на параметр errretустановлен как пустой указатель, setuptermвыведет диагностическое сообщение и завершит программу в случае своего аварийного завершения, как в следующем примере: #include <stdio.h> #include <term.h> #include <curses.h> #include <stdlib.h> int main() { setupterm("unlisted", fileno(stdout), (int *)0); printf("Done.\n"); exit(0); } Результат выполнения этой программы в вашей системе может не быть точной копией приведенного далее, но его смысл будет вполне понятен. " Done." не выводится, поскольку функция setuptermпосле своего аварийного завершения вызвала завершение программы: $ cc -о badterm badterm.с -lncurses $ ./badterm 'unlisted': unknown terminal type. $ Обратите внимание на строку компиляции в примере: в этой системе Linux мы используем реализацию ncurses библиотеки curses со стандартным заголовочным файлом, находящимся в стандартном каталоге. В таких системах вы можете просто включить файл curses.h и задать -lncursesдля библиотеки. В функции выбора пункта меню хорошо было бы иметь возможность очищать экран, перемещать курсор по экрану и записывать его положение на экране. После вызова функции setuptermвы можете обращаться к характеристикам базы данных terminfo с помощью вызовов трех функций, по одной на каждый тип характеристики: #include <term.h> int tigetflag(char *capname); int tigetnum(char *capname); char *tigetstr(char *capname); Функции tigetflag, tigetnumи tigetstrвозвращают значения характеристик terminfo булева или логического, числового и строкового типов соответственно. В случае сбоя (например, характеристика не представлена) tigetflagвернет -1, tigetnum— -2, a tigetstr— (char*)-1. Вы можете применять базу данных terminfo для определения размера экрана терминала, извлекая характеристики colsи linesс помощью следующей программы sizeterm.c: #include <stdio.h> #include <term.h> #include <curses.h> #include <stdlib.h> int main() { int nrows, ncolumns; setupterm(NULL, fileno(stdout), (int *)0); nrows = tigetnum("lines"); ncolumns = tigetnum("cols"); printf("This terminal has %d columns and %d rows\n", ncolumns, nrows); exit(0); } $ echo $TERM vt100 $ ./sizeterm This terminal has 80 columns and 24 rows Если запустить эту программу в окне рабочей станции, вы получите результат, отражающий размер текущего окна: $ echo $TERM xterm $ ./sizeterm This terminal has 88 columns and 40 rows $ Если применить функцию tigetstrдля получения характеристики перемещения курсора ( cup) терминала типа xterm, вы получите параметризованный ответ: \Е[%p1%d;%p2%dH. Этой характеристике требуются два параметра: номер строки и номер столбца, в которые перемещается курсор. Обе координаты измеряются, начиная от нулевого значения в левом верхнем углу экрана. Вы можете заменить параметры в характеристике реальными значениями с помощью функции tparm. До девяти параметров можно заменить значениями и получить в результате применяемую escape-последовательность символов. #include <term.h> char *tparm(char *cap, long p1, long p2, ..., long p9); После формирования escape-последовательности с помощью tparm, ее нужно отправить на терминал. Для корректной обработки этой последовательности не следует пересылать строку на терминал с помощью функции printf. Вместо нее примените одну из специальных функций, обеспечивающих корректную обработку любых задержек, необходимых для завершения операции, выполняемой терминалом. К ним относятся следующие: #include <term.h> int putp(char *const str); int tputs(char *const str, int affcnt, int (*putfunc)(int)); В случае успешного завершения функция putpвернет константу OK,в противном случае — ERR. Эта функция принимает управляющую строку терминала и посылает ее в стандартный вывод stdout. Итак, для перемещения в строку 5 и столбец 30 на экране можно применить блок программного кода, подобный приведенному далее: char *cursor; char *esc_sequence; cursor = tigetstr("cup"); esc_sequence = tparm(cursor, 5, 30); putp(esc_sequence); Функция tputsпредназначена для ситуаций, в которых терминал не доступен через стандартный вывод stdout, и позволяет задать функцию, применяемую для вывода символов. Она возвращает результат заданной пользователем функции putfunc. Параметр affcntпредназначен для обозначения количества строк, подвергшихся изменению. Обычно он устанавливается равным 1. Функция, используемая для вывода строки, должна иметь те же параметры и возвращать тип значения как у функции putfunc. В действительности putp(string)эквивалентна вызову tputs (string, 1, putchar). В следующем примере вы увидите применение функции tputs, используемой с функцией вывода, определенной пользователем. Имейте в виду, что в некоторых старых дистрибутивах Linux последний параметр функции tputsопределен как int (*putfunc)(char), что заставит вас изменить определение функции char_to_terminalиз упражнения 5.6. Примечание Вы почти готовы добавить обработку экрана в вашу функцию выбора пункта меню. Единственно, что осталось, — очистить экран просто с помощью свойства clear. Некоторые терминалы не поддерживают характеристику clear, которая помещает курсор в левый верхний угол экрана. В этом случае вы можете поместить курсор в левый верхний угол и применить команду ed— удалить до конца экрана. Для того чтобы собрать всю полученную информацию вместе, напишем окончательную версию примера программы выбора пункта меню screenmenu.c, в которой вы "нарисуете" варианты пунктов меню на экране для того, чтобы пользователь выбрал нужный пункт (упражнение 5.6). Упражнение 5.6. Полное управление терминаломВы можете переписать функцию getchoiceиз программы menu4.c для предоставления полного управления терминалом. В этом листинге функция mainпропущена, потому что она не меняется. Другие отличия от программы menu4.c выделены цветом. #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <termios.h> #include <term.h> #include <curses.h> static FILE* output_stream = (FILE *)0; char *menu[] = { "a — add new record", "d — delete record", "q - quit", NULL, }; int getchoice(char *greet, char *choices[], FILE *in, FILE *out); int char_to_terminal(int_char_to_write); int main() { ... } int getchoice(char *greet, char* choices[], FILE[]* in, FILE* out) { int chosen = 0; int selected; int screenrow, screencol = 10; char **option; char* cursor, *clear; output_stream = out; setupterm(NULL, fileno(out), (int*)0); cursor = tigetstr("cup"); clear = tigetstr("clear"); screenrow =4; tputs(clear, 1, (int*)char_to_terminal); tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out, "Choice: %s", greet); screenrow += 2; option = choices; while (*option) { ftputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out, "%s", *option); screenrow++; option++ } fprintf(out, "\n"); do { fflush(out); selected = fgetc(in); option = choices; while (*option) { if (selected == *option[0]) { chosen = 1; break; } option++; } if (!chosen) { tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out, "Incorrect choice, select again\n"); } } while (!chosen); tputs(clear, 1, char_to_terminal); return selected; } int char_to_terminal(int char_to_write) { if (output_stream) putc(char_to_write, output_stream); return 0; } Сохраните эту программу как menu5.с. Как это работает Переписанная функция getchoiceвыводит то же меню, что и в предыдущих примерах, но подпрограммы вывода изменены так, чтобы можно было воспользоваться характеристиками из базы данных terminfo. Если вы хотите видеть на экране сообщение "You have chosen:" дольше, чем одно мгновение перед очисткой экрана и подготовкой его к следующему выбору пункта меню, добавьте в функцию mainвызов sleep: do { choice = getchoice("Please select an action", menu, input, output); printf("\nYou have chosen: %c\n", choice); sleep(1); } while (choice != 'q'); Последняя функция в этой программе char_to_terminalвключает в себя вызов функции putc, которую мы упоминали в главе 3. В завершение этой главы бегло рассмотрим пример определения нажатий клавиш. Обнаружение нажатий клавишПользователи, программировавшие в ОС MS-DOS, часто ищут в ОС Linux эквивалент функции kbhit, которая определяет, была ли нажата клавиша, без реального ее считывания. К сожалению, их поиски оказываются безуспешными, поскольку прямого аналога нет. Программисты в среде UNIX не ощущают этого отсутствия, т.к. UNIX запрограммирована так, что программы очень редко (если когда-либо вообще) озабочены ожиданием события. Поскольку это обычный способ применения kbhit, ее нехватка редко ощущается в системах UNIX и Linux. Однако, когда вы переносите программы из MS-DOS, часто удобно эмулировать функцию kbhit, которую можно применять на деле в неканоническом режиме ввода (упражнение 5.7). Упражнение 5.7. Исключительно ваша собственная kbhit 1. Начните со стандартной заголовочной информации и пары структур для установки параметров терминала. peek_characterприменяется для проверки нажатия клавиши. Далее описываются функции, которые будут использоваться позже: #include <stdio.h> #include <stdlib.h> #include <termios.h> #include <term.h> #include <curses.h> #include <unistd.h> static struct termios initial_settings, new_settings; static int peek_character = -1; void init_keyboard(); void close_keyboard(); int kbhit(); int readch(); 2. Функция main вызывает функцию init_keyboardдля настройки терминала, затем выполняет цикл один раз в секунду, каждый раз вызывая в нем функцию kbhit. Если нажата клавиша <q>, функция close_keyboardвосстанавливает нормальный режим и программа завершается: int main() { int ch = 0; init_keyboard(); while (ch != 'q') { printf("looping\n"); sleep(1); if (kbhit()) { ch = readch(); printf("you hit %c\n", ch); } } close_keyboard(); exit(0); } 3. Функции init_keyboardи close_keyboardнастраивают терминал в начале и конце программы: void init_keyboard() { tcgetattr(0, &initial_settings); new_settings = initial_settings; new_settings.c_lflag &= ~ICANON; new_settings.c_lflag &= ~ECHO; new_settings.c_lflag &= ~ISIG; new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; tcsetattr(0, TCSANOW, &new_settings); } void close_keyboard() { tcsetattr(0, TCSANOW, &initial_settings); } 4. Теперь функция, проверяющая нажатие клавиши: int kbhit() { char ch; int nread; if (peek_character != -1) return 1; new_settings.c_cc[VMIN] = 0; tcsetattr(0, TCSANOW, &new_settings); nread = read(0, sch, 1); newrsettings.c_cc[VMIN] = 1; tcsetattr(0, TCSANOW, &new_settings); if (nread == 1) { peek_character = ch; return 1; } return 0; } 5. Нажатый символ считывается следующей функцией readch, которая затем восстанавливает значение -1 переменной peek_characterдля выполнения следующего цикла: int readch() { char ch; if (peek_character != -1) { ch = peek_character; peek_character = -1; return ch; } read(0, &ch, 1); return ch; } Когда вы выполните программу (kbhit.c), то получите следующий вывод: $ ./kbhit looping looping looping you hit h looping looping looping you hit d looping you hit q $ Как это работает Терминал настраивается в функции init_keyboardна считывание одного символа ( MIN=1, TIME=0). Функция kbhitизменяет это поведение на проверку ввода и его немедленный возврат ( MIN=0, TIME=0) и затем восстанавливает исходные установки перед завершением. Обратите внимание на то, что вы должны считать символ нажатой клавиши, но сохраняете его локально, готовые вернуть символ в вызывающую программу по ее требованию. Виртуальные консолиОС Linux предоставляет средство, называемое виртуальными консолями. Экран, клавиатуру и мышь одного ПК может использовать ряд терминальных устройств, доступных на этом компьютере. Обычно установка ОС Linux рассчитана на использование от 8 до 12 виртуальных консолей. Виртуальные консоли становятся доступными благодаря символьным устройствам /dev/ttyN, где N — номер, начинающийся с 1. Если вы регистрируетесь в вашей системе Linux в текстовом режиме, как только система активизируется, вам будет предложено регистрационное приглашение. Далее вы регистрируетесь с помощью имени пользователя и пароля. В этот момент используемое вами устройство — первая виртуальная консоль, терминальное устройство /dev/tty1. С помощью команд whoи psвы можете увидеть, кто зарегистрировался и какие командная оболочка и программы выполняются на этой виртуальной консоли: $ who neil tty1 Mar 8 18:27 $ ps -e PID TTY TIME CMD 1092 tty1 00:00:00 login 1414 tty1 00:00:00 bash 1431 tty1 00:00:00 emacs Из этого укороченного вывода видно, что пользователь neil зарегистрировался и запустил редактор Emacs на консоли ПК, устройстве /dev/tty1. Обычно ОС Linux запускается с процессом getty, выполняющимся на первых шести виртуальных консолях, поэтому есть возможность зарегистрироваться шесть раз, используя одни и те же экран, клавиатуру и мышь. Увидеть эти процессы можно с помощью и команды ps: $ ps -а PID TTY TIME CMD 1092 tty1 00:00:00 login 1093 tty2 00:00:00 mingetty 1094 tty3 00:00:00 mingetty 1095 tty4 00:00:00 mingetty 1096 tty5 00:00:00 mingetty 1097 tty6 00:00:00 mingetty В этом выводе представлен стандартный вариант программы getty для обслуживания консоли в системе SUSE, mingetty, выполняющийся на пяти следующих виртуальных консолях и ожидающий регистрации пользователя. Переключаться между виртуальными консолями можно с помощью комбинации клавиш <Ctrl>+<Alt>+<FN>, где N — номер виртуальной консоли, на которую вы хотите переключиться. Таким образом, для того чтобы перейти на вторую виртуальную консоль, нажмите <Ctrl>+<Alt>+<F2>, и <Ctrl>+<Alt>+<F1>, чтобы вернуться на первую консоль. (При переключении из регистрации в текстовом режиме, а не графическом, также работает комбинация клавиш <Ctrl>+<FN>.) Если в Linux запущена регистрация в графическом режиме, либо с помощью программы startx илн менеджера экранов xdm, на первой свободной консоли, обычно /dev/tty7, стартует графическая оболочка X Window System. Переключиться с нее на текстовую, консоль вы сможете с помощью комбинации клавиш <Ctrl>+<Alt>+<FN>, а вернуться с помощью <Ctrl>+<Alt>+<F7>. В ОС Linux можно запустить более одного сеанса X. Если вы сделаете это, скажем, с помощью следующей команды $ startx -- :1 Linux запустит сервер X на следующей свободной виртуальной консоли, в данном случае на /dev/tty8, и переключаться между ними вы сможете с помощью комбинаций клавиш <Ctrl>+<Alt>+<F8> и <Ctrl>+<Alt>+<F7>. Во всех остальных отношениях виртуальные консоли ведут себя как обычные терминалы, описанные в этой главе. Если процесс обладает достаточными правами, виртуальные консоли можно открывать, считывать с них данные, писать на них информацию точно так же, как в случае обычного терминала. ПсевдотерминалыУ многих UNIX-подобных систем, включая Linux, есть средство, именуемое псевдотерминалом. Это устройства, очень похожие на терминалы, которые мы использовали в данной главе, за исключением того, что у них нет связанного с ними оборудования. Они могут применяться для предоставления терминалоподобного интерфейса другим программам. Например, с помощью псевдотерминалов возможно создание двух программ, играющих друг с другом в шахматы, не взирая на тот факт, что сами по себе программы проектировались для взаимодействия посредством терминала с человеком-игроком. Приложение, действующее как посредник, передает ходы одной программы другой и обратно. Оно применяет псевдотерминалы, чтобы обмануть программы и заставить их вести себя нормально при отсутствии терминала. Одно время реализация псевдотерминалов (если вообще существовала) сильно зависела от конкретной системы. Сейчас они включены в стандарт Single UNIX Specification (единый стандарт UNIX) как UNIX98 Pseudo-Terminals (псевдотерминалы стандарта UNIX98) или PTY. РезюмеВ этой главе вы узнали о трех аспектах управления терминалом. В начале главы рассказывалось об обнаружении перенаправления и способах прямого диалога с терминалом в случае перенаправления дескрипторов стандартных файлов. Вы посмотрели на аппаратную модель терминала и немного познакомились с его историей. Затем вы узнали об общем терминальном интерфейсе и структуре termios, предоставляющей в ОС Linux возможность тонкого управления и манипулирования терминалом. Вы также увидели, как применять базу данных terminfo и связанные с ней функции для управления в аппаратно-независимом стиле выводом на экран, и познакомились с приемами мгновенного обнаружения нажатий клавиш. В заключение вы узнали о виртуальных консолях и псевдотерминалах. |
|
||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||
|