Linux程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

5.1 对终端进行读写

在第3章中,你了解到当一个程序在命令提示符中被调用时,shell负责将标准输入和标准输出流连接到你的程序。通过在程序中使用getchar和printf函数,你可以很容易地对这些默认流进行读写,实现程序和用户之间的交互。

在下面的实验中,你将使用上面提到的两个函数getchar和printf重写菜单例程,新程序的文件名为menu1.c。

实验 用C语言编写的菜单例程

(1)程序开始部分的语句定义了一个用来显示菜单内容的字符串数组和getchoice函数的原型:

(2)main函数以刚才定义的样本菜单字符串数组menu为参数调用getchoice函数:

(3)下面是这个程序的核心代码:负责显示菜单及读取用户输入的函数getchoice:

实验解析

getchoice函数显示程序的介绍信息greet和样本菜单choices,并要求用户输入代表某个菜单选项的首字符。接下来,程序进入循环,直到getchar函数返回与option字符串数组中某个数组成员的首字母匹配的字符为止。

编译并运行这个程序,你会发现它并没有像你所期望的那样工作。下面的例子说明了这一问题:

用户必须要输入“a/回车/q/回车”等才能做出选择。从上面的例子中可以看出,这个程序至少有两个问题。最严重的问题是,每当你做出正确的选择后,屏幕上都会出现错误提示Incorrect choice, select again(错误的选择,请重新选择)。另一个问题是,只有在按下回车键后程序才会读取输入。

1.标准模式和非标准模式

这两个问题是紧密相关的。默认情况下,只有在用户按下回车键后,程序才能读到终端的输入。在大多数情况下,这样做是有益的,因为它允许用户使用退格键(Backspace)或删除键(Delete)来纠正输入中的错误,用户只在对自己在屏幕上看到的内容满意时,才会按下回车键把键入的数据传递给程序。

这种处理方式被称为规范模式(canonical mode)或标准模式(standard mode)。所有的输入都基于行进行处理,在一个输入行完成前(通常是用户按下回车键之前),终端接口负责管理所有的键盘输入,包括退格键,应用程序读不到用户输入的任何字符。

与标准模式相对的是非标准模式(non-canonical mode),在这种模式中,应用程序对用户输入字符的处理拥有更大的控制权。我们稍后会再回到这两种模式上来。

除此之外,Linux终端处理程序能够把中断字符转换为对应的信号(例如,按下Ctrl+C可以中断程序)从而自动替用户完成对退格键和删除键的处理,用户无需在自己编写的每个程序中重新实现它。我们将在第11章详细介绍信号。

那么,这个程序的问题究竟在哪里呢?是这样的,Linux会暂存用户输入的内容,直到用户按下回车键,然后将用户选择的字符及紧随其后的回车符一起传递给程序。所以,每当你输入一个菜单选择时,程序就调用getchar函数处理该字符,而当程序在下一次循环中再次调用getchar函数时,它会立刻返回一个回车符。

程序真正看到的字符并不是ASCII码的回车符CR(十进制表示为13,十六进制表示为0D),而是换行符LF(十进制表示为10,十六进制表示为0A)。这是因为,Linux同UNIX系统一样,在其内部都是以换行符作为文本行的结束。也就是说,UNIX用一个单独的换行符来表示一行的结束,而其他的操作系统(如MS-DOS)用回车符和换行符两个字符的结合来表示一行的结束。如果输入或输出设备本身需要发送或接收一个回车符,则由Linux终端处理程序负责完成它。如果你已经习惯MS-DOS或其他操作系统的环境,你可能会对Linux的这种做法感到有一些奇怪。但这样做的最大好处是,它使得在Linux系统中,文本文件和二进制文件无任何实际的区别。只有在对终端、某些打印机或绘图仪进行输入输出时,你才需要对回车符进行处理。

在下面的代码中,通过忽略额外的换行符来纠正菜单例程中的主要错误:

它解决了燃眉之急,你将看到如下所示的输出:

我们将在本章的后面针对这个程序的第二个问题“必须按下回车键才能让程序继续执行”,给出一个更加精巧的解决方案。

2.处理重定向输出

Linux程序,甚至是交互式的Linux程序,经常会把它们的输入或输出重定向到文件或其他程序。我们来看看把程序的输出重定向到一个文件时出现的情况:

你可以把这种处理方式看作是成功的,因为程序的输出确实被重定向到文件,而不是显示在终端上。但有时你并不想这么做,或者你希望对准备让用户看到的提示信息与其他输出进行区别对待,前者仍然输出到终端上,而后者可以被安全地重定向。

如果想知道标准输出是否被重定向了,只需检查底层文件描述符是否关联到一个终端即可。系统调用isatty就是用来完成这一任务的。你只需将有效的文件描述符传递给它,它就可判断出该描述符是否连接到一个终端。

如果打开的文件描述符fd连接到一个终端,则系统调用isatty返回1,否则返回0。

在这个程序中,你使用的是文件流,但isatty只能对文件描述符进行操作。为了提供必要的转换,你需要把isatty调用与在第3章中介绍的fileno函数结合使用。

如果stdout(标准输出)已被重定向,你该做什么呢?直接退出不是一个好办法,因为用户无法知道程序为什么会运行失败。向stdout输出一条消息也不起作用,因为这条消息也会被重定向。一种解决方法是将消息写到stderr(标准错误输出),它不会被shell的>file命令重定向。

实验 检查是否存在输出重定向

沿用上面实验中创建的menu1.c程序,在其中加上新的include语句,对main函数进行如下的修改,并重新将该程序命名为menu2.c。

请看这个程序给出的样本输出:

实验解析

新代码段用isatty函数来测试标准输出是否已连接到一个终端,如果没有,则退出程序。shell也用同一种技术来判断是否需要提供终端提示符。将stdout和stderr同时重定向也是可能的,而且极为常见。你可以像下面这样把错误流重定向到另一个文件:

或者如下面这样,将两个输出流重定向到同一个文件:

如果你不熟悉输出重定向的语法,请仔细阅读本书的第2章。在该章中,我们详细介绍了它的语法。在本例中,你需要将错误信息直接发送到用户终端上。