Linux终端(三)
終端輸出
使用termios結構,我們可以控制鍵盤輸入,但是如果在顯示在屏幕上的輸出上可以進行同樣級別的控制也許會更好。在我們本章的開始,我們使用printf來向屏幕輸出字符,但是卻沒有辦法將輸出定位在屏幕上的某個特定位置上。
終端類型
許多Unix系統使用終端,盡管在今天的許多情況下,終端也許實際上是一個運行終端程序的PC。從歷史上來說,不同的生產產商提供了大量的硬件終端。盡管他們都是使用轉義序列(以轉義字符開始的字符串)來提供對光標與屬性的控制,例如粗體與閃爍等,但是他們并沒有以標準的方式來提供這些特性。某些老的終端同時還具有不同的滾動功能,當發送backspace滾動條也許會消失。
硬件終端的多樣性對于那些希望編寫控制屏幕以及運行在多個終端類型上的軟件的程序員是一個極大的問題。例如,ANSI標準使用轉義序列Escape+[+A來將光標上移一行,然而ADM-3a終端卻使用單獨的控制字符Ctrl+K。
要編寫處理各種不同的連接到Unix系統上的終端類型的程序是一件極其困難的任務。程序也許要為每一個終端類型提供不同的源代碼。
這樣在一個名為terminfo的包中提供一個解決方案就顯得并不驚奇。程序并不會迎合各種終端類型,相反,程序會查找一個終端類型數據庫來得到正確的信息。在大多數的現代Unix系統中,包括Linux,這些已經被集成到一個名為curses的軟件包中,這就是我們下一章要了解的內容。
在Linux上,我們也許要使用curses的一個名為ncurses的實現,并且要包含ncurses.h文件來提供我們terminfo函數的原型。terminfo函數本身聲明在他們自己的頭文件term.h中,或者至少以前是這種情況。而在新版本的Linux系統上,在terminfo與ncurses之間有一個模糊的界線,許多需要terminfo函數的程序必須同時包含ncurses頭文件。為避免以后的混亂,現代的Linux發行版本同時提供一個與Unix系統更兼容的curses頭文件與庫。在這些系統上,我們推薦使用curses.h與-lcurses。
標識我們的終端類型
Linux環境包含一個變量,TERM,他被設置為我們正在使用的終端類型。通常他是在系統登陸時由系統自動設置的。系統管理員也許會為每一個直接連接到終端的用戶設置一個默認的終端類型,這些用戶也許是要提供終端類型的遠程或是網絡用戶。TERM的值可以通過telnet協商,并通過rlogin傳遞。
用戶可以查詢shell來確定他正使用的終端類型。
$ echo $TERM
xterm
$
在這個例子中,shell是由一個名為xterm的程序來運行的,這是一個X Window系統的終端模擬器,或是提供類似功能的程序,例如KDE的Konsole或是Gnome的gnome-terminal。
terminfo軟件包包含了一個功能與大量終端的轉義序列的數據庫,并且為程序員提供了統一的接口。這樣編寫的程序就可以在數據庫擴展時利用未來終端的優點,而不是每一個程序都必須為不同的終端提供支持。
terminfo的功能是通過屬性來描述的。這些屬性存儲在已編譯的terminfo文件集合中,并且通常可以在/usr/lib/terminfo或是/usr/share/terminfo中找到。對于每一個終端(也有一些可以在terminfo中指定的打印機),有一個文件來定義其功能以及如何訪問這些特性。為了避免創建一個非常大的目錄,實際的文件存儲在子目錄中,而子目錄的名字只是簡單的終端類型的第一個字符。所以,VT100的定義可以在...terminfo/v/vt100中找到。
對于每一個終端類型都會以可讀的源碼的格式來編寫一個terminfo文件,然后使用tic命令將其編譯為應用程序可用的更為緊湊和高效的格式。奇怪的是,X/Open規范談到源碼以及編譯的格式定義,但是卻沒有提到實際編譯源碼的tic命令。我們可以使用infocmp程序來輸出一個已編譯的terminfo實體的可讀版本信息。
下面是一個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,
每一個terminfo定義由三種類型的實體構成。每一個實體被稱之為capname并且定義了一個終端功能。
布爾功能只是簡單的指示一個終端是否支持一個特定的特性。例如,如果終端支持XON/XOFF流控制就會顯示xon布爾功能。
數字功能定義了尺寸,例如lines定義了屏幕上的行數,而cols定義了屏幕上的列數。實際的數字是通過#字符與功能相區分的。要定義一個具有80列與24行的終端,我們可以寫成cols#80,lines#24。
字符串功能要顯得有些復雜。他們用于兩種不同的功能:定義訪問終端所需要的輸出字符串以及定義當用戶按下特定的按鍵時會接收的輸入字符串,通常為功能鍵或是數字鍵盤上的特殊鍵。某些字符功能相當簡單,例如el,表示"清除直到一行結束"。在一個VT100的終端上,要完成這個任務的轉義序列為Esc+[+K。在terminfo的源碼格式中則為el=/E[K。
特殊的鍵的定義與此相類似。例如,VT100上的功能鍵F1發送的轉義序列為Esc+O+P,其定義為kf1=/EOP。
當轉義序列需要一些參數時,其定義會顯得有些復雜。大多數的終端可以將光標移動到一個特定的行與列位置。對于每一個可能的光標位置都會有一個不 的功能是不切實際的,所有使用一個通用字符串,并且帶有參數定義當字符串被使用時要插入的值。例如,VT100終端使用轉義序列Esc+[+<row>+;+<col>+H來將光標移到一個特定的位置。在terminfo源碼格式中,其定義為cup=/E[%i%p1%d;%p2%dH$<5>。
其含義為:
/E:發送Escape
[:發送[字符
%i:增加參數
%p1:將第一個參數放入堆棧
%d:將堆棧上的數字作為十進制數字輸出
;:發送;字符
%p2:將第二個參數放入堆棧
%d:將堆棧上的數字作為十進制數字輸出
H:發送H字符。
這看起來似乎有一些復雜,但是卻允參數以固定的順序出現,獨立于終端希望他們出現在最終的轉義序列中的順序。增加參數的%i是必須的,因為標準的光標位置是屏幕的左上角(0,0),但是VT100的光標位置為(1,1)。最后的$<5>表明需要等同于輸出5個字符的時間來允許終端處理光標移動。
注意,我們將會定義許多許多終端,但是幸運的是,大多數的Unix和Linux系統已經預定義了大多數的終端。如果我們需要添加一個新的終端,我們可以在terminfo手冊頁中查找到完整的功能列表。一個好的起點就是定位那些與我們的新終端相似的終端,將新終端定義為已存在終端的一個變體。
使用terminfo功能
現在我們知道了如何定義終端功能,我們需要了解如何訪問他們。當我們使用terminfo時,我們需要做的第一件事就是通過調用setupterm來設置終端類型。這會為當前的終端類型初始化一個TERMINAL結構。然后我們就可以訪問并使用終端功能。setupterm函數原型如下:
#include <term.h>
int setupterm(char *term, int fd, int *errret);
setupterm庫函數將當前的終端類型設置為參數term所指定的終端類型。如果term為一個空指針,那么就會使用TERM環境變量。寫入終端所用的打開的文件描述符必須由參數fd傳遞。函數的執行結果存儲在由errret所指向的整型變量中(如果他不為空)。寫入的值可能是:
-1:沒有terminfo數據庫
0:在terminfo數據庫中沒有匹配的實體
1:成功
如果成功,setupterm函數會返回常量OK,如果失敗則會返回ERR。如果errret設置為一個空指針,函數執行失敗時就會輸出一個診斷信息并且退出程序,如下面的例子所示:
#include <stdio.h>
#include <term.h>
#include <ncurses.h>
int main()
{
setupterm(“unlisted”,fileno(stdout),(int *)0);
printf(“Done./n”);
exit(0);
}
運行在我們系統上的程序輸出也許并不是這里給出的樣子,但是其含義已經足夠明顯了。在這里并沒有打印出Done,因為setupterm函數執行失敗從而導致程序退出。
$ cc -o badterm badterm.c -I/usr/include/ncurses -lncurses
$ badterm
‘unlisted’: unknown terminal type.
$
注意上面例子中的編譯命令:在這個Linux系統上,ncurses頭文件位于/usr/include/ncurses目錄,所以我們必須使用-I選項來指示編譯器在這里進行查找。而某些Linux系統也許會將ncurses庫可以由標準位置進行訪問。在這些系統上,我們只需要簡單的包含curses.h頭文件,并且為庫指定-lcurses選項。
對于我們的菜單選擇函數,我們希望可以清屏,在屏幕上移動光標,并且可以屏幕上的任意位置寫入。一旦我們調用了setupterm函數,我們就可以使用不同的函數來訪問terminfo功能,功能類型如下:
#include <term.h>
int tigetflag(char *capname);
int tigetnum(char *capname);
char *tigetstr(char *capname);
函數tigetflag,tigetnum,tigetstr分別返回布爾,數字值以及字符串terminfo功能。如果失敗,tigetflag會返回-1,tigetnum會返回-2,而tigetstr會返回(char *)-1。
下面我們使用程序sizeterm.c程序取得cols與lines功能來確定終端尺寸:
#include <stdio.h>
#include <term.h>
#include <ncurses.h>
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
$
如果我們在工作站的一個窗口內運行這個程序,我們會得到反映當前窗口尺寸的答案:
$ echo $TERM
xterm
$ sizeterm
This terminal has 88 columns and 40 rows
$
如果我們使用tigetstr來取得xterm終端類型的光標移動功能(cup),我們會得到一個參數化的答案: /E[%p1%d;%p2%dH。
這個功能需要兩個參數:光標要移動到的行與列。這兩個坐標都是由屏幕左上角的零點處開始計量的。
我們可以使用tparm函數用實際的值來代替功能中的參數。最多可以替換九個參數,并且會返回一個可用的轉義序列:
#include <term.h>
char *tparm(char *cap, long p1, long p2, ..., long p9);
一旦我們使用tparm來組織終端轉義序列,我們必須將其發送到終端。要正確的處理,我們不應使用printf來向終端發送字符串,相反,我們要使用特殊的函數,這些函數為終端完成一個操作的正確處理提供了必要的延時。這些函數為:
#include <term.h>
int putp(char *const str);
int tputs(char *const str, int affcnt, int (*putfunc)(int));
如果成功,putp返回OK,如果失敗,則會返回ERR。putp函數將終端控制字符串作為參數并且將其發送到標準輸出。
所以要移動到屏幕的第5行,第30列,我們可以使用下面的代碼塊:
char *cursor;
char *esc_sequence;
cursor = tigetstr(“cup”);
esc_sequence = tparm(cursor,5,30);
putp(esc_sequence);
tputs函數是為那些不可以通過stdout訪問終端并且允許我們指定輸出字符所使用的函數的情況而提供的。他會返回用戶指定的函數putfunc的結要。affcnt參數用來指明更改會影響到的行數,通常將其設置為1。用于輸出字符串的函數必須與putchar函數具有相同的參數與返回結果。事實上,putp(string)等同于調用tputs(string,1,putchar)。我們將會在下面的例子中的看到用用戶指定的輸出函數來使用tputs函數。
要小心,一些老的Linux發行版本將tputs函數的最后一個參數定義為int (*putfunc)(char),這會強制我們修改在我們的下一個試驗中所定的char_to_terminal函數。
注意,如果我們查看tparm與終端功能的信息手冊頁,我們也許會遇到一個tgoto函數。他為移動光標提供了一個更為簡單的方案,但是我們不使用這個函數的原因是因為X/Open規范并沒有將其包含在1997版本中。所以我們推薦不要在新程序中使用這些函數。
現在我們準備好為我們的菜單選擇功能添加屏幕處理了。還有一件需要做的事就是簡單的使用clear來清除屏幕。某些終端并不支持clear功能,從而會使得光標停留在屏幕的左上角。在這種情況下,我們可以將光標放置在左上角,并且使用"刪除直到顯示結尾"命令ed。
將所有這些信息結合在一起,我們可以編寫我們例子菜單程序的最終版本,screen-menu.c,在這里我們將會在屏幕上"畫"出選項,從而供用戶選擇。
試驗--完全終端控制
我們可以重新編寫menu4.c的getchoice函數從而為我們提供完全的終端控制。在這個列表中,省略了main函數,因為他并沒有改變。
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <term.h>
#include <curses.h>
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函數實現了與我們前面的例子中相同的菜單,但是輸出函數進行了修改從而來使用terminfo功能。如果我們希望在屏幕被清除之前看到You have chosen:信息停留一會,可以使用下面的選擇,在main函數中添加一個sleep調用:
do {
choice = getchoice(“Please select an action”, menu, input, output);
printf(“/nYou have chosen: %c/n”, choice);
sleep(1);
} while (choice != ‘q’);
這個程序中的最后一個函數,char_to_terminal,包含了一個我們在第3章提到的putc函數調用。
要結束這一章,我們將會看一個如何檢測擊鍵的例子。
總結
以上是生活随笔為你收集整理的Linux终端(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web3 0.2.x 和 1.x.x版本
- 下一篇: IPFS下载安装和配置