Сравнение канонического и неканонического режимов
Обе эти проблемы тесно связаны. По умолчанию ввод терминала не доступен программе до тех пор, пока пользователь не нажмет клавишу <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).