Вы можете откорректировать основной недостаток вашей подпрограммы меню, просто игнорируя дополнительный символ перевода строки с помощью программного кода, подобного приведенному далее:
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 с самого начала создавалась, как многопользовательская система, включающая, как правило, множество терминалов, как непосредственно подсоединенных, так и подключенных по сети, как вы сможете определить тот терминал, который следует использовать?