在这章,我们将会考虑将我们第2章的程序进行改进。也许最明显的失败就是用户界面;他的功能也并不优雅。在这里,我们将会讨论如何更好的控制用户终端;也就说是键盘输入与屏幕输出。除了这些,我们还会了解我们编写的程序如何由用户处读取输入,即使是在输入重定向的情况下,以及确保输出到屏幕的正确位置。

尽管改进的CD数据程序会直到第7章我们才会看到,但是在这一章我们会做许多基础工作。第6章会关注于curses,这并不是某些远古的咒语,而提供一个代码高层来控制终端屏幕显示的函数库。同时,我们会通过介绍一些Linux和Unix的哲学思想以及终端输入输出的概念来检测一些早期Unix设置的想法。这里所提供的一些底层访问也许正是我们所寻找的。我们在这里所涉及的大部分内容也同样可以很好的适用于运行在终端窗口下的程序,例如KDE的Konsole,GNOME的gnome-terminal,或者是标准的X11 xterm。

在这一章,我们将会了解下面一些内容:

终端读取
终端驱动器以及通用终端接口
termios
终端输出与terminfo
检测按键

由终端读取与向终端写入

在第3章,我们了解到当一个程序由命令提示行启动时,shell会将标准的输入与输出流连接到我们的程序。我们可以通过使用getchar与printf例程来读取与写入这些默认流而与用户进行简单的交互。

下面我们要用C语言重写我们的菜单例程,只使用这两个例程,将其命名为menu1.c。

试验--C语言的菜单例程

1 由下面的代码行开始,其中定义要作为菜单使用的数组,以及getchoice函数原型:

#include <stdio.h>
char *menu[] = {
    “a - add new record”,
    “d - delete record”,
    “q - quit”,
    NULL,
};
int getchoice(char *greet, char *choices[]);

2 main函数使用例子菜单menu调用getchoice:

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
a - add new record
d - delete record
q - quit
q
You have chosen: q
$

在这里用户必须输入A/Enter/Q/Enter来做出选择。这至少有两个问题:最严重的问题是我们在每次正确的选择之后都会得到Incorrect choise输出;另外,我们必须在程序读取我们的输入之前按下Enter。

典型与非典型模式

这两个问题是紧密相关的。在默认情况下,终端输入直到用户按下Enter下才会为程序可用。在大多数情况下,这是一个好处,因为他可以允许用户使用Backspace或是Delete修正输入错误。只有当他们对他们在屏幕上看到的感到满意以后才会按下Enter使得输入为程序可用。

这种行为称之为典型模式,或是者是标准模式。所有的输入都是以行的方式进行处理的。在一行输入完整(通常是当用户按下Enter时),终端界面管理所有的按键输入,包括Backspace,而且程序不会读取任何字符。

这种模式的相反面为非典型模式,此时程序在输入字符的处理上有更多的控制权。我们会在后面再回来讨论这两种模式。

除了这些之外,Linux终端处理器喜欢将字符转换为信号,并且可以为我们自动执行Backspace与Delete,所以在我们所编写的程序中并不需要重新实现。我们将会在第11章讨论更多关于信号的内容。

那么在我们的程序中发生了什么呢?Linux在用户按下Enter之前会保存输入,然后将选择的字符与后面的Enter发送给程序。所以每次我们输入一个菜单选项时,程序调用getchar,处理字符,然后再次调用getchar,此时会立即返回Enter字符。

程序实际看到的字符并不是一个ASCII码的回车,CR(十进制13,十六进制0D),而换行(十进制10,十六进制0A)。这是因为Linux(类似Unix)内部总是使用换行来结束文本行;也就是说,Unix只使用一个换行来表示新行,而其他的系统,例如MS-DOS,使用回车和换行来表示新行。如果输入或是输出设备也发送或是请求一个回车,Linux终端会小心的进行处理。如果我们习惯于使用MS-DOS或是其他的环境,那就会显得有一些奇怪,但是这样考虑的一个好处是Linux中文本与二进制之间并没有真正的区别。只有当我们向一个终端或是打印机,绘图仪时输入或输出时才会处理回车。

我们可以使用一些代码来忽略额外的换行符来简单的修改我们的菜单例程的主要缺陷,如下所示:

do {
        selected = getchar();
} while(selected == ‘/n’);

这解决了第一个问题。我们回到需要按下回车的第二个问题,而我们会在后面讨论一个更好的处理换行的方法。

处理重定向输出

对于Linux程序,可以很容易的将他们的输入或输出重定向到一个文件或是其他的程序。让我们看一下当我们将输出重定到一个文件时我们的程序是如何处理的:

$ menu1 > file
a
q
$

我们可以认为这是成功的,因为车出重定向到一个文件而不是终端。然而,这里却有我们希望阻止的情况,或者说我们希望分离提示,我们希望用户可以从其他的输出查看,从而可以安全的重定向。

我们可以通过检测是一个底层文件描述符是否与一个终端相关联来区别标准输出是否已被重定向。isatty系统调用可以完成这个工作。我们只需要简单的传递给他们一个可用的文件描述符,他就可以检测出这个文件描述符是否连接到一个终端。

#include <unistd.h>
int isatty(int fd);

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

在我们的程序中,我们使用文件流,但是isatty只可以操作一个文件描述符。为了提供必要的转换,我们需要结合使用isatty调用与我们在第3章讨论的fileno例程。

如果stdout已经被重定向了我们要怎么办呢?仅是退出是不够的,因为用户并不知道程序为什么会运行失败。在stdout上打印一条信息也没有用,因为他已经被重定向离开终端了。一个解决办法就是写入stderr,此时他并没有被shell命令>file进行重定向。

试验--检测输出重定向

使用我们在前面所编写的程序menu1.c,包含一个新的include,将main改为下面的代码,并将其称为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);
}

工作原理

新版本的代码使用isatty函数来测试标准是否连接到一个终端,如果不是则会结束执行。同样也可以使用shell测试来决定是束提供一个提示符。当然可以,而且也比较常见的是同时重定向stdout与stderr,从而使其离开终端。我们可以像如下的样子将错误流重定向到一个不同的文件:

$ menu2 >file 2>file.error
$

或者是将两个输出流组合到一个文件中,如下所示:

$ menu2 >file 2>&1
$

在这个例子中,我们需要向控制台发送一条消息。

与终端交互

如果我们需要阻止我们程序中与用户交互的部分被重定向,但是对于其他的输入或是输出我们还是允许发生的,此时我们需要分离与stdout和stderr的交互。我们可以通过直接读写终端来做到。因为Linux是一个多用户系统,通常有许多终端直接相连或是通过网络相连,那么我们如何来确定要使用的正确终端呢?

幸运的,Linux和Unix系统通过提供一个特殊的设备,/dev/tty,从而使得事情变得简单,这个设备通常是当前的终端或是登陆会话。因为Linux将所有的内容都看作文件,我们可以使用通常的文件操作来读写/dev/tty设备。

现在我们要来修改我们的选择程序,从而我们可以向getchoice例程传递参数,来更好的控制输出。我们将命名为menu3.c。

试验--使用/dev/tty

打开menu2.c,将其内容改为下列的样子,这样输入和输出就可以重定向到/dev/tty:

#include <stdio.h>
#include <unistd.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

Logo

更多推荐