官术网_书友最值得收藏!

4.2 Linux終端

本節(jié)將討論如何更好地控制用戶終端,也就是鍵盤輸入與屏幕輸出。除了這些,還將了解到即使是在輸入重定向的情況下,編寫的程序如何由用戶處讀取輸入,以及確保輸出到屏幕的正確位置。

4.2.1 終端讀取與寫入

當(dāng)一個(gè)程序由命令提示行啟動(dòng)時(shí),shell會(huì)將標(biāo)準(zhǔn)的輸入與輸出流連接到程序。用戶可以通過使用getchar與printf例程來讀取與寫入這些默認(rèn)流從而實(shí)現(xiàn)與用戶進(jìn)行簡(jiǎn)單地交互。例如用C語言只使用這兩個(gè)例程,來寫一個(gè)菜單范例,將寫好的程序命名為menu1.c。

(1)由下面的代碼行開始,其中定義了要作為菜單使用的數(shù)組,以及getchoice函數(shù)原型。

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

(2)main函數(shù)調(diào)用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)下面是重要的代碼:打印菜單與讀取用戶輸入的函數(shù)。

  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函數(shù)打印程序簡(jiǎn)介greet與choices,然后要求用戶選擇一個(gè)初始字符。程序會(huì)循環(huán)直到getchar函數(shù)返回一個(gè)與option數(shù)組實(shí)體的第一個(gè)字符相匹配的字符。當(dāng)編譯運(yùn)行這個(gè)程序時(shí),發(fā)現(xiàn)它并不是如我們期望的那樣運(yùn)行。下面演示運(yùn)行這個(gè)程序所進(jìn)行的對(duì)話:

  $ ./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來做出選擇。這里至少有兩個(gè)問題:一個(gè)是在每次正確地選擇之后都會(huì)得到Incorrect choice輸出;另外一個(gè)是,必須在程序讀取輸入之前按Enter鍵。這兩個(gè)問題是緊密相關(guān)的。在默認(rèn)情況下,從終端輸入直到用戶按Enter鍵后才會(huì)為程序所用。在大多數(shù)情況下,這是一個(gè)優(yōu)點(diǎn),因?yàn)檫@樣可以允許用戶使用Backspace鍵或是Delete鍵修正輸入錯(cuò)誤。只有當(dāng)用戶對(duì)在屏幕上輸入的內(nèi)容確認(rèn)以后才會(huì)按Enter鍵使得輸入為程序所用。這種行為稱之為典型模式,或者是標(biāo)準(zhǔn)模式。所有的輸入都是以行的方式進(jìn)行處理的,在一行輸入完整(通常是當(dāng)用戶按Enter鍵時(shí))后,終端界面管理所有的按鍵輸入,而且程序不會(huì)讀取任何字符。與這種模式相對(duì)的模式稱為非典型模式,此時(shí)程序在輸入字符的處理上有更多的控制權(quán)。

除此之外,Linux終端處理器喜歡將字符轉(zhuǎn)換為信號(hào),并且可以自動(dòng)執(zhí)行Backspace與Delete操作。那么我們編寫的程序中發(fā)生了什么呢?Linux系統(tǒng)在用戶按Enter鍵之前會(huì)保存輸入,然后將選擇的字符與后面的Enter信號(hào)發(fā)送給程序。所以每次輸入一個(gè)菜單選項(xiàng)時(shí),程序調(diào)用getchar處理字符,然后再次調(diào)用getchar,此時(shí)會(huì)立即返回Enter字符。

程序?qū)嶋H看到的字符并不是一個(gè)ASCII碼的回車符,CR(十進(jìn)制13,十六進(jìn)制0D),而是換行(十進(jìn)制10,十六進(jìn)制0A)。這是因?yàn)長(zhǎng)inux內(nèi)部總是使用換行來結(jié)束文本行,也就是說,Linux只使用換行來表示新行,而其他的系統(tǒng),例如MS DOS系統(tǒng),使用回車和換行來表示新行。如果輸入或是輸出設(shè)備也發(fā)送或是請(qǐng)求一個(gè)回車符,Linux終端會(huì)小心地進(jìn)行處理。這對(duì)于習(xí)慣使用MS DOS或是其他系統(tǒng)環(huán)境的用戶,會(huì)覺得有一些奇怪,但這樣的一個(gè)好處是Linux中文本與二進(jìn)制之間并沒有真正的區(qū)別。只有當(dāng)用戶向一個(gè)終端、打印機(jī)或是繪圖儀輸入或輸出時(shí)才會(huì)處理回車信息。

可以使用一些代碼來忽略額外的換行符,來簡(jiǎn)單地修改上述菜單程序的主要缺陷,如下所示:

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

這解決了第一個(gè)問題,再回到需要按Enter鍵使程序讀取輸入的第二個(gè)問題(我們會(huì)在后面討論一個(gè)更好的處理換行的方法)。

對(duì)于Linux程序,可以很容易地將它們地輸入或輸出重定向到一個(gè)文件或是其他的程序,來看一下將輸出重定到一個(gè)文件時(shí)程序是如何處理的:

  $ menu1 > file
  a
  q
  $

以上程序可以認(rèn)為是成功的,因?yàn)榛剀囍囟ㄏ虻揭粋€(gè)文件而不是終端。然而,這里卻有希望阻止的情況,或者說希望分離提示,從而可以安全地重定向。

可以通過檢測(cè)一個(gè)底層文件描述符是否與一個(gè)終端相關(guān)聯(lián)來區(qū)分標(biāo)準(zhǔn)輸出是否已被重定向。使用isatty系統(tǒng)調(diào)用可以完成這項(xiàng)工作,只需要簡(jiǎn)單地傳遞給它們一個(gè)可用的文件描述符,就可以檢測(cè)出這個(gè)文件描述符是否連接到一個(gè)終端。

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

如果打開的文件描述符fd連接到一個(gè)終端,那么isatty系統(tǒng)調(diào)用就會(huì)返回1,否則返回0。

如果stdout已經(jīng)被重定向應(yīng)該怎么辦呢?這時(shí)僅是退出是不夠的,因?yàn)橛脩舨⒉恢莱绦驗(yàn)槭裁磿?huì)運(yùn)行失敗。在stdout上打印一條信息也沒有用,因?yàn)樗呀?jīng)被重定向離開終端了。一個(gè)解決辦法就是寫入stderr,此時(shí)它并沒有被shell命令>file進(jìn)行重定向。

使用前面編寫的程序menu1.c,包含一個(gè)新的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函數(shù)來測(cè)試標(biāo)準(zhǔn)是否連接到一個(gè)終端,如果不是則會(huì)結(jié)束執(zhí)行。同樣也可以使用shell測(cè)試來決定是否提供一個(gè)提示符。比較常見的是同時(shí)重定向stdout與stderr,從而使其離開終端,可以按如下示例將錯(cuò)誤信息重定向到一個(gè)不同的文件。

  $ menu2 >file 2>file.error
  $

或者是將兩個(gè)輸出流組合到一個(gè)文件中,如下所示。

  $ menu2 >file 2amp;>&1
  $

在這個(gè)例子中,需要向控制臺(tái)發(fā)送一條消息。

如果需要阻止程序中與用戶交互的部分被重定向,但是對(duì)于其他的輸入或是輸出是允許的,此時(shí)就需要分離與stdout和stderr的交互,這可以通過直接讀寫終端來做到。Linux是一個(gè)多用戶系統(tǒng),通常有許多終端直接相連或是通過網(wǎng)絡(luò)相連,如何來確定要使用的正確終端呢?幸運(yùn)地,Linux和Unix系統(tǒng)通過提供一個(gè)特殊的設(shè)備/dev/tty使事情變得簡(jiǎn)單,這個(gè)設(shè)備通常是當(dāng)前的終端或是登錄對(duì)話。因?yàn)長(zhǎng)inux將所有的內(nèi)容都視為文件,因此可以使用通常的文件操作來讀寫/dev/tty設(shè)備。

接下來修改選擇程序,從而可以向getchoice例程傳遞參數(shù),以更好地控制輸出。這里將其命名為menu3.c。

打開menu2.c,將其內(nèi)容改為下列代碼,這樣輸入和輸出就可以重定向到/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;
  }

這樣,當(dāng)使用輸出重定向來運(yùn)行這個(gè)程序時(shí),可以看到提示符與通常的程序輸出是分離的。

  $ 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

4.2.2 終端驅(qū)動(dòng)器與通用終端接口

有時(shí)程序需要更好地控制終端而不是使用簡(jiǎn)單的文件操作來控制。Linux提供了一個(gè)可以用來控制終端驅(qū)動(dòng)器的接口集合,從而更好地控制終端的輸入與輸出處理過程。

可以通過一個(gè)用來進(jìn)行分離讀寫操作的函數(shù)調(diào)用集合來控制終端,使得數(shù)據(jù)接口更為清晰,同時(shí)又能更好地控制終端的行為。這并不說是說終端I/O接口是清晰的,而是可以處理各種不同的硬件。

在Linux術(shù)語中,用于“行規(guī)程”(line discipline)控制接口,從而使程序在指定終端驅(qū)動(dòng)器的行為方面更為靈活。可以控制的主要特征包括以下幾個(gè)。

行編輯 決定是否允許編輯使用Backspace。

緩沖 決定是立即讀取字符,或是在一個(gè)延時(shí)后讀取。

回顯 允許控制回顯,例如當(dāng)正在讀取密碼時(shí)回顯。

CR/LF 決定輸入與輸出映射,也就是當(dāng)輸入一個(gè)/n時(shí)會(huì)發(fā)生什么。

行速度 很少用在PC控制臺(tái)上,這些速度對(duì)于調(diào)制解調(diào)器和串口線上的終端非常重要。

termios是POSIX所指定的標(biāo)準(zhǔn)接口,與System V接口的termio類似。終端接口是通過在一個(gè)termios類型的結(jié)構(gòu)中設(shè)置值以及使用一組函數(shù)調(diào)用來進(jìn)行控制的,所有這些都定義在頭文件termios.h中。

使用定義在termios.h中的函數(shù)的程序需要使用一個(gè)合適的函數(shù)庫(kù)進(jìn)行鏈接。這通常是curses庫(kù),所以編譯程序時(shí),需要在編譯器命令行的最后加上-lcurses。在一些老的Linux系統(tǒng)中,curses庫(kù)是由一個(gè)新curses或是ncurses來提供的,在這種情況下,庫(kù)名字與鏈接參數(shù)就分別變?yōu)?lncurses。

可以通過操作來影響終端的值,分為幾種模式:輸入(input)、輸出(output)、控制(control)、本地(local)及特殊控制字符(Special control characters)。

一個(gè)最小的termios結(jié)構(gòu)通常聲明如下(X/Open規(guī)范允許添加一些其他的域)。

  #include <termios.h>
  struct termios {
  tcflag_t c_iflag;
    tcflag_t c_oflag;
    tcflag_t c_cflag;
    tcflag_t c_lflag;
    cc_t c_cc[NCCS];
  };

成員的名字對(duì)應(yīng)上面列表中的五個(gè)參數(shù)。

可以通過調(diào)用tcgetattr函數(shù)來為終端初始化termios結(jié)構(gòu),其函數(shù)原型如下。

  #include <termios.h>
  int tcgetattr(int fd, struct termios *termios_p);

這個(gè)函數(shù)調(diào)用將終端接口變量的當(dāng)前值寫入由termios_p所指向的結(jié)構(gòu)中。如果這些值被修改了,可以使用tcsetattr函數(shù)來重新配置終端接口。

  #include <termios.h>
  int tcsetattr(int fd, int actions, const struct termios *termios_p);

tcsetattr函數(shù)中的actions域控制如何應(yīng)用這些修改。三個(gè)可能的值分別如下。

(1)TCSANOW:立即更改。

(2)TCSADRAIN:當(dāng)前輸出完成時(shí)更改。

(3)TCSAFLUSH:當(dāng)前輸出完成時(shí)更改,但是忽略當(dāng)前可用的輸入與read調(diào)用中未返回的輸入。

在程序啟動(dòng)前保存終端設(shè)置是非常重要的,通常程序負(fù)責(zé)初始保存并在程序完成時(shí)恢復(fù)設(shè)置。

下面詳細(xì)講述這些模式以及相關(guān)的函數(shù)調(diào)用。

1.輸入模式

輸入模式控制輸入(終端驅(qū)動(dòng)器在串口或是鍵盤上接收的字符)在傳遞給程序之前是如何處理的,通過在termios結(jié)構(gòu)的c_iflag成員中設(shè)置相應(yīng)的標(biāo)記來進(jìn)行設(shè)置。所有這些標(biāo)記都定義為宏,而且可以使用位或進(jìn)行組合。對(duì)于所有的終端模式都是如此。

可以用于c_iflag的宏如下。

BRKINT:在一行中檢測(cè)到中斷(break)條件時(shí)產(chǎn)生一個(gè)中斷。

IGNBRK:在一行中忽略中斷條件。

INCRNL:將接收到的回車轉(zhuǎn)換為換行。

IGNCR:忽略接收到的回車。

INLCR:將接收到的新行轉(zhuǎn)換為回車。

IGNPAR:忽略帶有奇偶檢驗(yàn)誤差的字符。

INPCK:在接收到的字符上執(zhí)行奇偶校驗(yàn)。

PARMRK:標(biāo)記奇偶校驗(yàn)誤差。

ISTRIP:去除所有的輸入字符。

IXOFF:在輸入上允許軟件流控制。

IXON:在輸出上允許軟件流控制。

如果沒有設(shè)置BRKINT與IGNBRK,一行中的break條件將會(huì)被讀取為NULL(Ox00)字符。我們并不需要經(jīng)常改變輸入模式,因?yàn)槟J(rèn)值通常是最合適的,所以在這里不會(huì)進(jìn)行更深入地討論。

2.輸出模式

輸出模式控制輸出字符是如何進(jìn)行處理的,也就是說,程序所發(fā)送的字符在傳遞到串口或是屏幕之前是如何被處理的。許多輸出模式都有相對(duì)應(yīng)的輸入模式。此外還存在一些其他的標(biāo)記,這些標(biāo)記主要關(guān)注于需要時(shí)間處理字符的慢速終端。幾乎所有這些模式都可以使用終端功能的terminfo數(shù)據(jù)進(jìn)行處理,這會(huì)在后面介紹到。

通過設(shè)置termios結(jié)構(gòu)的c_flag成員標(biāo)記可以控制輸出模式,在c_oflag允許使用的標(biāo)記如下。

OPOST:打開輸出處理。

ONLCR:將輸出的新行轉(zhuǎn)換為回車-換行。

OCRNL:將輸出的回車轉(zhuǎn)換為新行。

ONOCR:在第0列不輸出回車。

ONLRET:新行也需要一個(gè)回車。

OFILL:發(fā)送填充字符來提供延時(shí)。

OFDEL:使用DEL作為填充字符,而不是NULL。

NLDLY:新行延時(shí)選擇。

CRDLY:回車延時(shí)選擇。

TABDLY:Tab延時(shí)選擇。

BSDLY:Backspace延時(shí)選擇。

VTDLY:垂直Tab延時(shí)選擇。

FFDLY:換頁延時(shí)選擇。

如果OPOST沒有設(shè)置,所有其他的標(biāo)記都會(huì)被忽略。輸出模式也不經(jīng)常使用,所以這里也不進(jìn)行深入討論。

3.控制模式

控制模式控制終端的硬件特點(diǎn)。可以通過設(shè)置termios結(jié)構(gòu)中的c_cflag成員的值來指定控制模式,其可用的值如下。

CLOCAL:忽略調(diào)制解調(diào)器狀態(tài)行。

CREAD:允許字符接收。

CS5:在發(fā)送或是接收的字符中使用5位(5 bits)。

CS6:在發(fā)送或是接收的字符中使用6位。

CS7:在發(fā)送或是接收的字符中使用7位。

CS8:在發(fā)送或是接收的字符中使用8位。

CSTOPB:每個(gè)字符使用兩個(gè)結(jié)束位,而不是一個(gè)。

HUPCL:關(guān)閉時(shí)掛起調(diào)制解調(diào)器。

PARENB:允許奇偶校驗(yàn)生成與檢測(cè)。

PARODD:設(shè)置為奇校驗(yàn)。

如果設(shè)置了HUPCL,當(dāng)終端驅(qū)動(dòng)器檢測(cè)到指向終端的最后一個(gè)文件描述符已經(jīng)關(guān)閉時(shí),它就會(huì)將調(diào)制解調(diào)器控制行設(shè)置為掛起。控制模式主要用于串口線連接到一個(gè)調(diào)制解調(diào)器上的情況,盡管他們也可以用于與終端交互。通常,使用termios的控制模式改變終端的配置要比改變默認(rèn)行行為簡(jiǎn)單得多。

4.本地模式

本地模式控制終端的各種特性,可以通過設(shè)置termios結(jié)構(gòu)中的c_lflag成員的值來指定本地模式,其可用的宏如下。

ECHO:允許輸入字符的本地回顯。

ECHOE:在接收EPASE時(shí)執(zhí)行Backspace、Space和Backspace組合。

ECHOK:在KILL字符上執(zhí)行清除行。

ECHONL:回顯新行字符。

ICANON:允許正規(guī)輸入處理。

IEXTEN:允許實(shí)現(xiàn)特定函數(shù)。

ISIG:允許信號(hào)。

NOFLSH:禁止隊(duì)列flush。

TOSTOP:在寫嘗試上發(fā)送后臺(tái)處理信號(hào)。

最重要的兩個(gè)標(biāo)記為ECHO(允許系統(tǒng)管理者抑制輸入字符的回顯)和ICANON(用于在兩個(gè)不同的處理接收字符模式中切換終端)。如果設(shè)置了ICANON標(biāo)記,這一行就處理正規(guī)模式;如果沒有,這一行就處理非正規(guī)模式。

5.特殊控制字符

另外還有一些字符集合,例如Ctrl-C,當(dāng)用戶輸入時(shí)會(huì)以特殊的方式運(yùn)行。termios結(jié)構(gòu)的c_cc數(shù)組成員包含映射到每一個(gè)支持函數(shù)的字符。每一個(gè)字符的位置(在數(shù)組中的索引)是由一個(gè)宏定義的,但是它們必須控制的字符并沒有限制。

依據(jù)終端是否設(shè)置為正規(guī)模式(例如,在termios的c_lfalg成員設(shè)置ICANON標(biāo)記),c_cc數(shù)組以兩種不同的方式來使用。這里要注意的是,兩種不同模式的數(shù)組索引值所使用的方式有某些重疊。正因如此,絕不能混用這兩種模式的值。

對(duì)于正規(guī)模式,數(shù)組索引如下。

VEOF:EOF字符。

VEOL:EOL字符。

VERASE:ERASE字符。

VINTR:INTR字符。

VKILL:KILL字符。

VQUIT:QUIT字符。

VSUSP:SUSP字符。

VSTART:START字符。

VSTOP:STOP字符。

對(duì)于非正規(guī)模式,數(shù)組索引如下。

VINTR:INTR字符。

VMIN:MIN值。

VQUIT:QUIT字符。

VSUSP:SUSP字符。

VTIME:TIME值。

VSTART:START字符。

VSTOP:STOP字符。

因?yàn)樘厥庾址约胺钦?guī)字符MIN與TIME值對(duì)于高級(jí)輸入字符的處理非常重要,所以這里進(jìn)行詳細(xì)地解釋,如表4-10所示。

表4-10 特殊字符與描述

TIME與MIN的值只用于非正規(guī)模式,而且共同作用來控制輸入的讀取。同時(shí),它們也控制當(dāng)程序試圖讀取與一個(gè)終端相關(guān)聯(lián)的文件描述符時(shí)會(huì)發(fā)生什么,有四種情況。

MIN=0同時(shí)TIME=0 在這種情況下,一個(gè)read調(diào)用會(huì)立即返回。如果某些字符可用,它們就會(huì)立即返回;如果沒有可用字符,read會(huì)返回零并且不會(huì)讀取任何字符。

MIN=0同時(shí)TIME>0 在這種情況下,當(dāng)有任何可以讀取的字符或是TIME的1/10秒逝去時(shí),read會(huì)返回。如果因?yàn)闀r(shí)間過期沒有讀取任何字符,read就會(huì)返回零。否則,會(huì)返回讀取的字符數(shù)。

MIN>0同時(shí)TIME=0 在這種情況下,read會(huì)等待直到有MIN個(gè)字符可以讀取,然后返回讀取的字符數(shù)。在文件結(jié)尾時(shí)會(huì)返回零。

MIN>0同時(shí)TIME>0 這是最復(fù)雜的情況。當(dāng)調(diào)用read時(shí),它等待接收一個(gè)字符。當(dāng)接收到第一個(gè)字符,以及在接下來的時(shí)間序列內(nèi)接收到一個(gè)字符時(shí),就會(huì)啟動(dòng)一個(gè)中間字符(inter-character)計(jì)時(shí)器(如果已經(jīng)在運(yùn)行就重新啟動(dòng))。當(dāng)有MIN個(gè)字符可以讀取或是中間字符計(jì)時(shí)器的TIME時(shí)間值過去1/10秒時(shí),read會(huì)返回。這可以用于區(qū)分Escape按鍵的一次按下操作與一個(gè)函數(shù)鍵值轉(zhuǎn)義序列的啟動(dòng)之間的區(qū)別。但要小心,網(wǎng)絡(luò)通信或是高級(jí)處理器會(huì)擦除時(shí)間信息。

通過設(shè)置非正規(guī)模式及使用MIN與TIME值,程序可以處理逐個(gè)字符的輸入。如果要查看正在使用的shell所使用的termios設(shè)置,可以使用下面的命令來得到一個(gè)列表。

  $ styy -a

在Linux系統(tǒng)上,對(duì)標(biāo)準(zhǔn)的termios結(jié)構(gòu)進(jìn)行一些擴(kuò)展,其輸出如下。

  speed 38400 baud; rows 44; columns 109; line = 0;
  intr = ^C; quit = ^/; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
  eol2 =
  <undef>; start = ^Q; stop = ^S;
  susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time
  = 0;
  -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
  -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon
  -ixoff -
  iuclc -ixany -imaxbel
  opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0
  ff0
  isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
  echoctl
  echoke

在這些輸出結(jié)果之間,可以看到EOF字符為Ctrl+D并允許回顯。如果試驗(yàn)終端控制,可以很容易使這個(gè)終端處理非標(biāo)準(zhǔn)狀態(tài),從而會(huì)其使用非常困難。有幾種方法可以做到這一點(diǎn)。

第一種方法是,如果stty版本支持,可以使用下面的命令。

  $ stty sane

如果失去了Enter鍵到新行字符的映射,需要輸入“stty sane”,但不是按Enter鍵,而是按Ctrl+J(新行字符)。

第二種方法是使用stty -g命令將當(dāng)前的stty設(shè)置保存為可以重新讀取的格式。在命令行中,可以使用下面的命令。

  $ stty -g > save_stty
  ..
  <experiment with settings>
  ..
  $ stty $(cat save_stty)

對(duì)于最后一個(gè)stty命令仍然需要使用Ctrl+J而不是回車。在shell腳本中也使用同樣的技術(shù)。

  save_stty="$(stty -g)"
  <alter stty settings>
  stty $save_stty

第三種方法是使用另一個(gè)不同的終端,通過ps命令來查看要使其成為不可用的shell,然后使用kill HUP<process id>來強(qiáng)制結(jié)束這個(gè)shell。因?yàn)閟tty參數(shù)總是在一個(gè)登錄提示出現(xiàn)之前進(jìn)行設(shè)置的,所以可以正常登錄。

也可以使用stty命令直接由命令行來設(shè)置終端模式。要設(shè)置一個(gè)shell腳本可以執(zhí)行單個(gè)字符讀取的模式,需要關(guān)閉正規(guī)模式,同時(shí)將min設(shè)置為1,而time設(shè)置為0。命令如下。

  $ stty -icanon min 1 time 0

現(xiàn)在終端被設(shè)置為可以立即讀取字符。可以試著運(yùn)行第一個(gè)程序,會(huì)發(fā)現(xiàn)其運(yùn)行情況正如我們所希望的那樣。也可以在提示輸入密碼之前關(guān)閉回顯來輸入密碼,其命令如下。

  $ stty -echo

一定要記住,在試驗(yàn)之后一定要用命令stty echo來打開回顯。

termios結(jié)構(gòu)所提供的最后一個(gè)函數(shù)可以操作線速率。是由函數(shù)調(diào)用來設(shè)置。四個(gè)調(diào)用原型如下。

  #include <termios.h>
  speed_t cfgetispeed(const struct termios *);
  speed_t cfgetospeed(const struct termios *);
  int cfsetispeed(struct termios *, speed_t speed);
  int cfsetospeed(struct termios *, speed_t speed);

這些函數(shù)是作用在termios結(jié)構(gòu)上的,而不是直接作用在端口上。這就意味著要設(shè)置一個(gè)新的速率,必須使用tcgetattr讀取當(dāng)前的設(shè)置,使用上述函數(shù)中的一個(gè)來設(shè)置速率,然后使用tcsetattr將termios結(jié)構(gòu)寫回。只有tcsetattr調(diào)用之后,線速率才會(huì)改變。

在上面的函數(shù)調(diào)用中允許各種速率值,其中最重要的為:B0,掛起終端;B1200,1200波特;B2400,2400波特;B9600,9600波特;B19200,19200波特;B38400:38400波特。標(biāo)準(zhǔn)并沒有定義大于38400的速率,對(duì)于大于這個(gè)速率的串口也沒有相應(yīng)的支持函數(shù)。一些系統(tǒng),包括Linux,為選擇更快的速率定義了B57600、B115200和B230400這幾個(gè)速率。如果使用的是Linux的某個(gè)早期版本,不可以使用這些常量,則可以使用setserial命令來獲得57600和115200等非標(biāo)準(zhǔn)速率。在這種情況下,當(dāng)選擇B38400時(shí)會(huì)使用這些速率。這兩種方法都是不可以移植的,所以使用時(shí)要注意。

對(duì)于終端控制還有一些其他的函數(shù),這些函數(shù)直接作用在文件描述符上,而不需要讀取與設(shè)置termios結(jié)構(gòu)。它們的定義如下。

  #include <termios.h>
  int tcdrain(int fd);
  int tcflow(int fd, int flowtype);
  int tcflush(int fd, int in_out_selector);

這些函數(shù)的作用如下。

tcdrain:會(huì)使調(diào)用函數(shù)在所有的輸出隊(duì)列發(fā)送之前等待。

tcflow:用于中止或是重啟輸出。

tcflush:可用于沖刷輸入、輸出或是二者。

前面已經(jīng)討論了關(guān)于termios結(jié)構(gòu)相當(dāng)多的主題內(nèi)容,下面來看一些實(shí)際的例子。也許最簡(jiǎn)單的例子就是在讀取密碼時(shí)禁止回顯了,這可以通過關(guān)閉ECHO標(biāo)記來做到。

【例4-1】使用termios讀取密碼。

(1)密碼程序password.c,以下面的定義開始。

  #include <termios.h>
  #include <stdio.h>
  #define PASSWORD_LEN 8
  int main()
  {
   struct termios initialrsettings, newrsettings;
   char password[PASSWORD_LEN + 1];

(2)接下來使用下面的語句由當(dāng)前的標(biāo)準(zhǔn)輸入讀取當(dāng)前的設(shè)置,并且將其復(fù)制到前面創(chuàng)建的termios結(jié)構(gòu)中。

  tcgetattr(fileno(stdin), &initialrsettings)

(3)制作一份原始設(shè)置的拷貝來替換它們。在newrsettings中關(guān)閉ECHO標(biāo)記,并且詢問用戶密碼。

  newrsettings = initialrsettings;
  newrsettings.c_lflag &= ~ECHO;
  printf("Enter password: ");

(4)將終端屬性設(shè)置為newrsettings并讀取密碼。最后,將終端屬性設(shè)置為其原始屬性,并且打印密碼來驗(yàn)證前面的效果。

   if(tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) {
   fprintf(stderr,"Could not set attributes/n");
   }
   else {
   fgets(password, PASSWORD_LEN, stdin);
   tcsetattr(fileno(stdin), TCSANOW, &initialrsettings);
   fprintf(stdout, "/nYou entered %s/n", password);
   }
   exit(0);
  }

執(zhí)行過程如下。

  $ ./password
  Enter password:
  You entered hello
  $

在這個(gè)例子中,在“Enter password:”提示之后輸入“hello”,但是輸入的字符并沒有回顯,直到用戶按Enter鍵時(shí)才產(chǎn)生輸出。我們很小心地使用語句X &= ~FLAG(清除相應(yīng)的FLAG位)來改變需要改變的標(biāo)記位。如果需要,可以使用X |= FLAG來設(shè)置由FLAG定義的位,雖然在上面的這個(gè)例子中并不需要這樣做。

當(dāng)設(shè)置屬性時(shí),使用TCSAFLUSH來忽略程序準(zhǔn)備讀取之前用戶所輸入的字符,這是使用戶在回顯關(guān)閉之前不需要輸入密碼的一個(gè)好辦法。同時(shí),也在程序結(jié)束之前恢復(fù)了先前的設(shè)置。

termios結(jié)構(gòu)的另一個(gè)通常用法是可以設(shè)置終端為一種可以立即讀取用戶輸入字符的狀態(tài),這可通過關(guān)閉正規(guī)模式并且設(shè)置MIN與TIME的值來做到。

【例4-2】讀取每個(gè)字符。

(1)下面的代碼與pasword.c相類似,但是需要插入到menu3.c中來生成新程序menu4.c。在開始之前,程序頂部包含一個(gè)新的頭文件,內(nèi)容如下。

  #include <stdio.h>
  #include <unistd.h>
  #include <termios.h>

(2)然后在main函數(shù)中定義一些新的變量。

  int choice = 0;
  FILE *input;
  FILE *output;
  struct termios initial_settings, new_settings;

(3)在調(diào)用getchoice函數(shù)之前修改終端特點(diǎn),這里就是需要插入代碼的地方。

   fprintf(stderr, "Unable to open /dev/tty/n");
   exit(1);
  }
  tcgetattr(fileno(input),&initial_settings);
  new_settings = initial_settings;
  new_settings.c_lflag &= ~ICANON;
  new_settings.c_lflag &= ~ECHO;
  new_settings.c_cc[VMIN] = 1;
  new_settings.c_cc[VTIME] = 0;
  if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
   fprintf(stderr,"could not set attributes/n");
  }
   fprintf(stderr, "Unable to open /dev/tty/n");
   exit(1);
  }
  tcgetattr(fileno(input),&initial_settings);
  new_settings = initial_settings;
  new_settings.c_lflag &= ~ICANON;
  new_settings.c_lflag &= ~ECHO;
  new_settings.c_cc[VMIN] = 1;
  new_settings.c_cc[VTIME] = 0;
  if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
   fprintf(stderr,"could not set attributes/n");
  }

(4)在程序結(jié)束之前恢復(fù)原始設(shè)置。

  do {
   choice = getchoice("Please select an action", menu, input, output);
   printf("You have chosen: %c/n", choice);
   } while (choice != 'q');
   tcsetattr(fileno(input),TCSANOW,&initial_settings);
   exit(0);
  }

(5)檢測(cè)回車換行符以確保處于非正規(guī)模式,因?yàn)椴粫?huì)再執(zhí)行默認(rèn)的CR到LF的映射。

  do {
   selected = fgetc(in);
  } while (selected == '/n' || selected == '/r');

(6)如果此時(shí)用戶在我們程序運(yùn)行時(shí)按Ctrl+C鍵,程序就會(huì)終止。可以通過在本地模式中清除ISIG標(biāo)記來禁止特殊字符的處理,在主函數(shù)中添加下面的代碼行即可。

  new_settings.c_lflag &= ~ISIG;

如果將這些修改加入到上面的程序中,就會(huì)得到一個(gè)立即響應(yīng)并且輸入不會(huì)回顯的主程序。

  $ ./menu4
  Choice: Please select an action
  a - add new record
  d - delete record
  q - quit
  You have chosen: a
  Choice: Please select an action
  a - add new record
  d - delete record
  q - quit
  You have chosen: q
  $

4.2.3 終端輸出

使用termios結(jié)構(gòu)可以控制鍵盤輸入,但是如果能對(duì)顯示在屏幕上的輸出進(jìn)行同樣級(jí)別的控制也許會(huì)更好。本節(jié)將介紹使用printf向屏幕輸出字符的方法,但是這種輸出無法將輸出定位在屏幕上的某個(gè)特定位置。

1.終端類型

許多Linux系統(tǒng)使用終端,盡管在現(xiàn)在的多數(shù)情況下,終端也許就是一個(gè)運(yùn)行終端程序的PC。從歷史上來說,不同的生產(chǎn)廠商提供了大量的硬件終端。盡管它們都使用轉(zhuǎn)義序列(以轉(zhuǎn)義字符開始的字符串)來提供對(duì)光標(biāo)與屬性的控制,例如粗體與閃爍等,但是并沒有以標(biāo)準(zhǔn)的方式來提供這些特性。某些老的終端還具有不同的滾動(dòng)功能,當(dāng)發(fā)送backspace滾動(dòng)條時(shí)也許會(huì)消失。

硬件終端的多樣性對(duì)于那些希望編寫控制屏幕以及運(yùn)行在多個(gè)終端類型上的軟件的程序員來說,是一個(gè)極大的問題。例如,ANSI標(biāo)準(zhǔn)使用轉(zhuǎn)義序列Escape+[+A來將光標(biāo)上移一行,然而ADM-3a終端卻使用控制字符Ctrl+K。要編寫處理各種不同的連接到Linux系統(tǒng)上的終端類型的程序是一件極其困難的任務(wù),程序也許要為每一個(gè)終端類型提供不同的源代碼。

這樣,在一個(gè)名為terminfo的包中提供了一個(gè)解決方案就顯得并不為奇。程序不會(huì)迎合各種終端類型,相反,程序會(huì)查找一個(gè)終端類型數(shù)據(jù)庫(kù)來得到正確的信息。在大多數(shù)的現(xiàn)代Unix系統(tǒng)中,包括Linux,這些信息已經(jīng)被集成到一個(gè)名為curses的軟件包中。在Linux中要使用ncurses來實(shí)現(xiàn),并且要包含ncurses.h文件來提供terminfo函數(shù)的原型。terminfo函數(shù)本身的聲明在其頭文件term.h中。而在新版本的Linux系統(tǒng)中,在terminfo與ncurses之間有一個(gè)模糊的界線,許多需要terminfo函數(shù)的程序必須同時(shí)包含ncurses頭文件。為避免混亂,現(xiàn)在的Linux發(fā)行版本同時(shí)提供一個(gè)與Unix系統(tǒng)更兼容的curses頭文件與庫(kù)。在這些系統(tǒng)上,推薦使用curses.h與-lcurses。

2.標(biāo)識(shí)終端類型

Linux環(huán)境包含一個(gè)變量TERM,它被設(shè)置為當(dāng)前正在使用的終端類型,通常是在系統(tǒng)登錄時(shí)由系統(tǒng)自動(dòng)設(shè)置。系統(tǒng)管理員也許會(huì)為每一個(gè)直接連接到終端的用戶設(shè)置一個(gè)默認(rèn)的終端類型,這些用戶也許是要提供終端類型的遠(yuǎn)程或是網(wǎng)絡(luò)用戶。TERM的值可以通過telnet協(xié)商,并通過rlogin傳遞。

用戶可以查詢shell來確定正使用的終端類型。

  $ echo $TERM
  xterm
  $

在這個(gè)例子中,shell是由一個(gè)名為xterm的程序來運(yùn)行的,它是一個(gè)X Window系統(tǒng)的終端模擬器,或是提供類似功能的程序,例如KDE的konsole或是gnome的gnome-terminal。

3.定義terminfo功能

terminfo軟件包包含了一個(gè)由大量不同類型終端的功能標(biāo)志和轉(zhuǎn)義序列等信息組成的數(shù)據(jù)庫(kù),并且為程序員提供了統(tǒng)一的接口。這樣編寫的程序就可以在數(shù)據(jù)庫(kù)擴(kuò)展時(shí)利用未來終端的優(yōu)點(diǎn),而不是每一個(gè)程序都必須為不同的終端提供支持。terminfo的功能是通過屬性來描述的,這些屬性存儲(chǔ)在已編譯的terminfo文件集合中,并且通常可以在/usr/lib/terminfo或是/usr/share/terminfo中找到。對(duì)于每一個(gè)終端(也包括一些可以在terminfo中指定的打印機(jī)),有一個(gè)文件來定義其功能以及如何訪問這些特性。為了避免創(chuàng)建一個(gè)非常大的目錄,實(shí)際應(yīng)用中文件都存儲(chǔ)在子目錄中,而子目錄的名字只是終端類型的第一個(gè)字符。所以,VT100的定義可以在...terminfo/v/vt100中找到。

對(duì)于每一個(gè)終端類型都會(huì)以可讀的源碼格式來編寫一個(gè)terminfo文件,然后使用tic命令將其編譯為應(yīng)用程序可用的更為緊湊和高效的格式。奇怪的是,X/Open規(guī)范談到源碼以及編譯的格式定義,但是卻沒有提到實(shí)際編譯源碼的tic命令。使用infocmp程序可以輸出一個(gè)已編譯的terminfo實(shí)體的可讀版本信息。

下面是一個(gè)VT100終端的terminfo文件的例子。

  $ infocmp vt100
  vt100|vt100-am|dec vt100 (w/advanced video),
  am, mir, msgr, xenl, xon,
  cols#80, it#8, lines#24, vt#3,
  acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
  bel=^G, blink=/E[5m$<2>, bold=/E[1m$<2>,
  clear=/E[H/E[J$<50>, cr=/r, csr=/E[%i%p1%d;%p2%dr,
  cub=/E[%p1%dD, cub1=/b, cud=/E[%p1%dB, cud1=/n,
  cuf=/E[%p1%dC, cuf1=/E[C$<2>,
  cup=/E[%i%p1%d;%p2%dH$<5>, cuu=/E[%p1%dA,
  cuu1=/E[A$<2>, ed=/E[J$<50>, el=/E[K$<3>,
  el1=/E[1K$<3>, enacs=/E(B/E)0, home=/E[H, ht=/t,
  hts=/EH, ind=/n, ka1=/EOq, ka3=/EOs, kb2=/EOr, kbs=/b,
  kc1=/EOp, kc3=/EOn, kcub1=/EOD, kcud1=/EOB,
  kcuf1=/EOC, kcuu1=/EOA, kent=/EOM, kf0=/EOy, kf1=/EOP,
  kf10=/EOx, kf2=/EOQ, kf3=/EOR, kf4=/EOS, kf5=/EOt,
  kf6=/EOu, kf7=/EOv, kf8=/EOl, kf9=/EOw, rc=/E8,
  rev=/E[7m$<2>, ri=/EM$<5>, rmacs=^O, rmkx=/E[?1l/E>,
  rmso=/E[m$<2>, rmul=/E[m$<2>,
  rs2=/E>/E[?3l/E[?4l/E[?5l/E[?7h/E[?8h, sc=/E7,
  sgr=/E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t^
  N%e^O%;,
  sgr0=/E[m^O$<2>, smacs=^N, smkx=/E[?1h/E=,
  smso=/E[1;7m$<2>, smul=/E[4m$<2>, tbc=/E[3g,

每一個(gè)terminfo定義由三種類型的實(shí)體構(gòu)成,每一個(gè)實(shí)體被稱之為capname并且定義了一個(gè)終端功能。布爾功能只是簡(jiǎn)單地指示一個(gè)終端是否支持一個(gè)特定的功能。例如,如果終端支持XON/XOFF流控制就會(huì)顯示xon布爾功能。數(shù)值功能定義了與長(zhǎng)度有關(guān)的參數(shù),例如lines定義了屏幕上的行數(shù),而cols定義了屏幕上的列數(shù)。指定的數(shù)字是通過#字符與功能相區(qū)分的。例如要定義一個(gè)具有80列與24行的終端,可以寫成cols#80,lines#24。字符串功能顯得有些復(fù)雜,它們用于兩種不同的功能:定義訪問終端所需要的輸出字符串以及定義當(dāng)用戶按下特定的按鍵時(shí)會(huì)接收的輸入字符串,通常為功能鍵或是數(shù)字鍵盤上的特殊鍵。某些字符功能相當(dāng)簡(jiǎn)單,例如el,表示“清除直到一行結(jié)束”。在一個(gè)VT100的終端上,要完成這個(gè)任務(wù)的轉(zhuǎn)義序列為Esc+[+K,在terminfo的源碼格式中則為el=/E[K。特殊鍵的定義與此相類似,例如,VT100上的功能鍵F1發(fā)送的轉(zhuǎn)義序列為Esc+O+P,其定義為kf1=/EOP。

當(dāng)轉(zhuǎn)義序列需要一些參數(shù)時(shí),其定義會(huì)顯得有些復(fù)雜。大多數(shù)的終端可以將光標(biāo)移動(dòng)到一個(gè)特定的行與列位置。例如,VT100終端使用轉(zhuǎn)義序列Esc+[+<row>+;+<col>+H來將光標(biāo)移到一個(gè)指定位置,在terminfo源碼格式中,其定義為cup=/E[%i%p1%d;%p2%dH來將光標(biāo)移動(dòng)到指定位置。

其含義如下。

/E:發(fā)送Escape。

[:發(fā)送“[”字符。

%i:增加參數(shù)。

%p1:將第一個(gè)參數(shù)放入堆棧。

%d:將堆棧上的數(shù)字作為十進(jìn)制數(shù)字輸出。

;:發(fā)送“;”字符。

%p2:將第二個(gè)參數(shù)放入堆棧。

%d:將堆棧上的數(shù)字作為十進(jìn)制數(shù)字輸出。

H:發(fā)送H字符。

這看起來似乎有些復(fù)雜,但是卻允參數(shù)以固定的順序出現(xiàn),獨(dú)立于終端希望它們出現(xiàn)在最終的轉(zhuǎn)義序列中的順序。增加參數(shù)%i是必須的,因?yàn)闃?biāo)準(zhǔn)的光標(biāo)位置位于屏幕的左上角(0,0),但是VT100的光標(biāo)位置為(1,1)。最后的$<5>表明需要等同于輸出5個(gè)字符的時(shí)間來讓終端處理光標(biāo)移動(dòng)。

我們將會(huì)定義許多終端,幸運(yùn)的是,大多數(shù)的Unix和Linux系統(tǒng)已經(jīng)預(yù)定義了大多數(shù)的終端。如果需要添加一個(gè)新的終端,可以在terminfo手冊(cè)頁中查找到完整的功能列表。一個(gè)好的起點(diǎn)是定位那些與新終端相似的終端,將新終端定義為已存在終端的一個(gè)變體。

4.使用terminfo功能

知道了如何定義終端這項(xiàng)功能,接下來需要了解如何訪問終端。當(dāng)使用terminfo時(shí),需要做的第一件事就是通過調(diào)用setupterm來設(shè)置終端類型,這會(huì)為當(dāng)前的終端類型初始化一個(gè)TERMINAL結(jié)構(gòu),然后才可以訪問并使用終端功能。setupterm函數(shù)原型如下。

  #include &lt;term.h&gt;
  int setupterm(char *term, int fd, int *errret);

setupterm庫(kù)函數(shù)將當(dāng)前的終端類型設(shè)置為參數(shù)term所指定的終端類型。如果term為一個(gè)空指針,那么就會(huì)使用TERM環(huán)境變量。寫入終端所用的打開的文件描述符必須由參數(shù)fd傳遞。函數(shù)執(zhí)行結(jié)果存儲(chǔ)在由errret所指向的整型變量中(如果該變量不為空)。寫入的值可能如下。

-1:沒有terminfo數(shù)據(jù)庫(kù)。

0:在terminfo數(shù)據(jù)庫(kù)中沒有匹配的實(shí)體。

1:成功。

如果成功,setupterm函數(shù)會(huì)返回常量OK,如果失敗則會(huì)返回ERR。如果errret設(shè)置為一個(gè)空指針,函數(shù)執(zhí)行失敗時(shí)就會(huì)輸出一個(gè)診斷信息并且退出程序,如下面的例子所示:

  #include &lt;stdio.h&gt;
  #include &lt;term.h&gt;
  #include &lt;ncurses.h&gt;
  int main()
  {
   setupterm("unlisted",fileno(stdout),(int *)0);
   printf("Done./n");
   exit(0);
  }

在這里并沒有打印出Done,因?yàn)閟etupterm函數(shù)執(zhí)行失敗從而導(dǎo)致程序退出。

  $ cc -o badterm badterm.c -I/usr/include/ncurses -lncurses
  $ badterm
    'unlisted’: unknown terminal type.
  $

注意上面例子中的編譯命令。在這個(gè)Linux系統(tǒng)上,ncurses頭文件位于/usr/include/ ncurses目錄,所以必須使用-I選項(xiàng)來指示編譯器在這里進(jìn)行查找。而某些Linux系統(tǒng)也許會(huì)由標(biāo)準(zhǔn)位置訪問ncurses庫(kù),在這些系統(tǒng)上,只需要簡(jiǎn)單地包含curses.h頭文件,并且為庫(kù)指定-lcurses選項(xiàng)即可。

對(duì)于菜單選擇函數(shù),我們希望可以有清屏、在屏幕上移動(dòng)光標(biāo),以及可以在屏幕上的任意位置寫入。一旦調(diào)用了setupterm函數(shù),就可以使用不同的函數(shù)來訪問terminfo功能,其功能類型如下。

  #include &lt;term.h&gt;
  int tigetflag(char *capname);
  int tigetnum(char *capname);
  char *tigetstr(char *capname);

函數(shù)tigetflag、tigetnum、tigetstr分別返回布爾數(shù)字值以及字符串terminfo功能。如果失敗,tigetflag會(huì)返回-1,tigetnum會(huì)返回-2,而tigetstr會(huì)返回(char *)-1。

下面使用程序sizeterm.c取得cols與lines的設(shè)置值以確定終端尺寸。

  #include &lt;stdio.h&gt;
  #include &lt;term.h&gt;
  #include &lt;ncurses.h&gt;
  int main()
  {
   int nrows, ncolumns;
   setupterm(NULL, fileno(stdout), (int *)0);
   nrows = tigetnum("lines");
   ncolumns = tigetnum("cols");
   printf("This terminal has %d columns and %d rows/n", ncolumns, nrows);
   exit(0);
  }
  $ echo $TERM
  vt100
  $ sizeterm
  This terminal has 80 columns and 24 rows
  $

如果在工作站的一個(gè)窗口內(nèi)運(yùn)行這個(gè)程序,會(huì)得到反映當(dāng)前窗口尺寸的答案。

  $ echo $TERM
  xterm
  $ sizeterm
  This terminal has 88 columns and 40 rows
  $

如果使用tigetstr來取得xterm終端類型的光標(biāo)移動(dòng)功能(cup),會(huì)得到一個(gè)參數(shù)化的答案/E[%p1%d;%p2%dH。這個(gè)功能需要兩個(gè)參數(shù):光標(biāo)要移動(dòng)到的行與列,這兩個(gè)坐標(biāo)都是由屏幕左上角的零點(diǎn)處開始計(jì)量的。

可以使用tparm函數(shù)用實(shí)際的值來代替功能中的參數(shù),最多可以替換9個(gè)參數(shù),并且會(huì)返回一個(gè)可用的轉(zhuǎn)義序列。

  #include &lt;term.h&gt;
  char *tparm(char *cap, long p1, long p2, ..., long p9);

一旦使用tparm來組織終端轉(zhuǎn)義序列,必須將其發(fā)送到終端。要正確地處理,不應(yīng)使用printf來向終端發(fā)送字符串,而要使用特殊的函數(shù),這些函數(shù)為終端完成一個(gè)操作的正確處理提供了必要的延時(shí)。這些函數(shù)包括:

  #include &lt;term.h&gt;
  int putp(char *const str);
  int tputs(char *const str, int affcnt, int (*putfunc)(int));

如果成功,putp返回OK,如果失敗,則會(huì)返回ERR。putp函數(shù)將終端控制字符串作為參數(shù)并且將其發(fā)送到標(biāo)準(zhǔn)輸出設(shè)備。

如果要移動(dòng)到屏幕的第5行,第30列,可以使用下面的代碼。

  char *cursor;
  char *esc_sequence;
  cursor = tigetstr("cup");
  esc_sequence = tparm(cursor,5,30);
  putp(esc_sequence);

tputs函數(shù)是為那些不能通過stdout訪問終端但允許指定輸出字符所使用的函數(shù)而提供的。它會(huì)返回用戶指定的函數(shù)putfunc的結(jié)果。affcnt參數(shù)用來指明更改會(huì)影響到的行數(shù),通常將其設(shè)置為1。用于輸出字符串的函數(shù)必須與putchar函數(shù)具有相同的參數(shù)與返回結(jié)果。事實(shí)上,putp(string)等同于調(diào)用tputs(string,1,putchar),讀者將會(huì)在下面的例子中以用戶指定的輸出函數(shù)來使用tputs函數(shù)。一些老的Linux版本將tputs函數(shù)的最后一個(gè)參數(shù)定義為int (*putfunc)(char),這會(huì)強(qiáng)制要求修改char_to_terminal函數(shù)。

現(xiàn)在已經(jīng)準(zhǔn)備好為菜單選擇功能添加屏幕處理功能了,還有一件需要做的事就是使用clear來清除屏幕。某些終端不支持clear功能,使得光標(biāo)停留在屏幕的左上角。在這種情況下,可以將光標(biāo)放置在左上角,并且使用“刪除直到顯示結(jié)尾”命令ed。

將所有這些信息結(jié)合在一起,就可以編寫菜單程序的最終版本screen-menu.c,到這一步將會(huì)在屏幕上“畫”出選項(xiàng),從而供用戶選擇。

【例4-3】完全終端控制。

重新編寫menu4.c的getchoice函數(shù)可為用戶提供完全的終端控制。在下面的程序代碼中,省略了main函數(shù),因?yàn)樗鼪]有變化。

  #include &lt;stdio.h&gt;
  #include &lt;unistd.h&gt;
  #include &lt;termios.h&gt;
  #include &lt;term.h&gt;
  #include &lt;curses.h&gt;
  static FILE *output_stream = (FILE *)0;
  char *menu[] = {
   "a - add new record",
   "d - delete record",
   "q - quit",
   NULL,
  };
  int getchoice(char *greet, char *choices[], FILE *in, FILE *out);
  int char_to_terminal(int char_to_write);
  int main()
  {
  ...
  }
  int getchoice(char *greet, char *choices[], FILE *in, FILE *out)
  {
   int chosen = 0;
   int selected;
   int screenrow, screencol = 10;
   char **option;
   char *cursor, *clear;
   output_stream = out;
   setupterm(NULL,fileno(out), (int *)0);
   cursor = tigetstr("cup");
   clear = tigetstr("clear");
   screenrow = 4;
   tputs(clear, 1, (int *) char_to_terminal);
   tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
   fprintf(out, "Choice: %s, greet);
   screenrow += 2;
   option = choices;
   while(*option) {
   tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
   fprintf(out,"%s", *option);
   screenrow++;
   option++;
   }
   fprintf(out, "/n");
   do {
   fflush(out);
   selected = fgetc(in);
   option = choices;
   while(*option) {
   if(selected == *option[0]) {
   chosen = 1;
   break;
   }
   option++;
   }
   if(!chosen) {
   tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal);
   fprintf(out,"Incorrect choice, select again/n");
   }
   } while(!chosen);
   tputs(clear, 1, char_to_terminal);
   return selected;
  }
  int char_to_terminal(int char_to_write)
  {
   if (output_stream) putc(char_to_write, output_stream);
   return 0;
  }

重寫的getchoice函數(shù)實(shí)現(xiàn)了與前面的例子中相同的菜單,但是對(duì)輸出函數(shù)進(jìn)行了修改,從而可以使用terminfo功能。如果希望在屏幕被清除之前使“You have chosen:”的信息停留一會(huì),可以使用下面的選擇,在main函數(shù)中添加一個(gè)sleep調(diào)用。

  do {
   choice = getchoice("Please select an action", menu, input, output);
   printf("/nYou have chosen: %c/n", choice);
   sleep(1);
   } while (choice != 'q');
主站蜘蛛池模板: 驻马店市| 吴江市| 嵩明县| 汉中市| 双柏县| 鄢陵县| 新昌县| 灌云县| 龙海市| 得荣县| 九龙城区| 平原县| 祁东县| 永济市| 吉林省| 新宁县| 南阳市| 阿巴嘎旗| 济阳县| 长兴县| 邯郸县| 峨眉山市| 翼城县| 肃南| 静海县| 东山县| 宣恩县| 兰州市| 长沙市| 南投县| 景泰县| 视频| 巨鹿县| 图们市| 旺苍县| 广元市| 洪江市| 庐江县| 武乡县| 正镶白旗| 河东区|