Основы программирования в Linux - Нейл Мэтью
Шрифт:
Интервал:
Закладка:
#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: %cn", choice);
} while (choice != 'q');
exit(0);
}
3. Теперь важный фрагмент кода — функция, которая и выводит на экран меню и считывает ввод пользователя:
int getchoice(char *greet, char *choices[]) {
int chosen = 0;
int selected;
char **option;
do {
printf("Choice: %sn", greet);
option = choices;
while (*option) {
printf("%sn", *option);
option++;
}
selected = getchar();
option = choices;
while (*option) {
if (selected == *option[0]) {
chosen = 1;
break;
}
option++;
}
if (!chosen) {
printf("Incorrect choice, select againn");
}
} 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))) {