GNU Readline是一個跨平臺開源程序庫,提供交互式的文本編輯功能。應用程序藉助該庫函數,容許用戶編輯鍵入的命令行,並提供自動補全和命令歷史等功能。Bash(Bourne Again Shell)、GDB、ftp 和mail等程序就使用Readline庫提供其命令行界面。php
Readline是GNU通用公共許可證版本3(GNU GPLv3)下的自由軟件。這意味着若發佈或分發的程序使用Readline庫,則該程序必須是自由軟件,並持有GPL-兼容的許可證。固然,用戶也可以使用本身的實現替代庫的行爲。html
本文將基於若干典型的Readline庫函數,給出一個簡單的交互式調測器示例。示例代碼的運行環境以下:前端
本節簡要介紹後文將要用到的幾個Readline庫函數,如用來替代fgets()的readline()函數。關於庫函數的更多描述可參考http://cnswww.cns.cwru.edu/php/chet/readline/rltop.html。數組
readline()函數的ANSI C聲明以下:函數
char *readline(const char *prompt);ui |
該函數打印參數prompt 所指的提示字符串,而後讀取並返回用戶輸入的單行文本(剔除換行符)。若prompt 爲空指針或指向空字符串,則不顯示任何提示。若還沒有讀取到字符就遇到EOF,則該函數返回NULL;不然返回由malloc()分配的命令行內存,故調用結束後應經過free()顯式地釋放內存。spa
讀取命令行後,可調用add_history()函數將該行存入命令行歷史列表中,以便後續重取。命令行
void add_history(const char *string);3d |
rl_completion_matches()函數用於自動補全:指針
typedef char *rl_compentry_func_t(const char *text, int state); char **rl_completion_matches(const char *text, rl_compentry_func_t *entry_func); |
該函數返回一個字符串數組,即參數text的補全列表;若沒有補全項,則函數返回NULL。該數組以空指針結尾,首個條目將替換text,其他條目爲可能的補全項。函數指針entry_func指向一個具備兩個參數的函數。其中參數state在首次調用時爲零,後續調用時爲非零。未匹配到text時,entry_func返回空指針。
函數指針rl_completion_entry_function指向rl_completion_matches()所使用的補全生成函數:
rl_compentry_func_t *rl_completion_entry_function; |
若其值爲空,則使用默認的文件名補全生成函數,即rl_filename_completion_function()。
應用程序可經過函數指針rl_attempted_completion_function自定義補全函數:
typedef char **rl_completion_func_t(const char *text, int start, int end); rl_completion_func_t * rl_attempted_completion_function; |
該變量指向建立匹配補全的替代函數。函數參數 start和 end爲字符串緩衝區rl_line_buffer的下標,以定義參數text的邊界。若該函數存在且返回NULL,或者該變量值被設置爲NULL,則rl_complete()將調用rl_completion_entry_function指向的函數來生成匹配結果;除此以外,程序使用該變量所指函數返回的字符串數組。若該變量所指函數將變量rl_attempted_completion_over設置爲非零值,則Readline將不執行默認的文件名補全(即便自定義補全函數匹配失敗)。
首先,自定義用戶命令結構及其執行函數(爲簡化實現,執行函數僅打印函數名):
1 //命令結構體 2 typedef int (*CmdProcFunc)(void); 3 typedef struct{ 4 char *pszCmd; 5 CmdProcFunc fpCmd; 6 }CMD_PROC; 7 8 //命令處理函數定義 9 #define MOCK_FUNC(funcName) \ 10 int funcName(void){printf(" Enter "#funcName"!\n"); return 0;} 11 12 MOCK_FUNC(ShowMeInfo); 13 MOCK_FUNC(SetLogCtrl); 14 MOCK_FUNC(TestBatch); 15 MOCK_FUNC(TestEndianOper);
基於上述定義,建立命令列表以下:
1 //命令表項宏,用於簡化書寫 2 #define CMD_ENTRY(cmdStr, func) {cmdStr, func} 3 #define CMD_ENTRY_END {NULL, NULL} 4 5 //命令表 6 static CMD_PROC gCmdMap[] = { 7 CMD_ENTRY("ShowMeInfo", ShowMeInfo), 8 CMD_ENTRY("SetLogCtrl", SetLogCtrl), 9 CMD_ENTRY("TestBatch", TestBatch), 10 CMD_ENTRY("TestEndian", TestEndianOper), 11 12 CMD_ENTRY_END 13 }; 14 #define CMD_MAP_NUM (sizeof(gCmdMap)/sizeof(CMD_PROC)) - 1/*End*/
而後,提供兩個檢索命令列表的函數:
1 //返回gCmdMap中的CmdStr列(必須爲只讀字符串),以供CmdGenerator使用 2 static char *GetCmdByIndex(unsigned int dwCmdIndex) 3 { 4 if(dwCmdIndex >= CMD_MAP_NUM) 5 return NULL; 6 return gCmdMap[dwCmdIndex].pszCmd; 7 } 8 9 //執行命令 10 static int ExecCmd(char *pszCmdLine) 11 { 12 if(NULL == pszCmdLine) 13 return -1; 14 15 unsigned int dwCmdIndex = 0; 16 for(; dwCmdIndex < CMD_MAP_NUM; dwCmdIndex++) 17 { 18 if(!strcmp(pszCmdLine, gCmdMap[dwCmdIndex].pszCmd)) 19 break; 20 } 21 if(CMD_MAP_NUM == dwCmdIndex) 22 return -1; 23 gCmdMap[dwCmdIndex].fpCmd(); //調用相應的函數 24 25 return 0; 26 }
考慮到實際應用中,程序運行平臺不必定包含Readline庫,所以須要用__READLINE_DEBUG條件編譯控制庫的使用。
1 #ifdef __READLINE_DEBUG 2 #include <readline/readline.h> 3 #include <readline/history.h> 4 5 static const char * const pszCmdPrompt = "clover>>"; 6 7 //退出交互式調測器的命令(不區分大小寫) 8 static const char *pszQuitCmd[] = {"Quit", "Exit", "End", "Bye", "Q", "E", "B"}; 9 static const unsigned char ucQuitCmdNum = sizeof(pszQuitCmd) / sizeof(pszQuitCmd[0]); 10 static int IsUserQuitCmd(char *pszCmd) 11 { 12 unsigned char ucQuitCmdIdx = 0; 13 for(; ucQuitCmdIdx < ucQuitCmdNum; ucQuitCmdIdx++) 14 { 15 if(!strcasecmp(pszCmd, pszQuitCmd[ucQuitCmdIdx])) 16 return 1; 17 } 18 19 return 0; 20 } 21 22 //剔除字符串首尾的空白字符(含空格) 23 static char *StripWhite(char *pszOrig) 24 { 25 if(NULL == pszOrig) 26 return "NUL"; 27 28 char *pszStripHead = pszOrig; 29 while(isspace(*pszStripHead)) 30 pszStripHead++; 31 32 if('\0' == *pszStripHead) 33 return pszStripHead; 34 35 char *pszStripTail = pszStripHead + strlen(pszStripHead) - 1; 36 while(pszStripTail > pszStripHead && isspace(*pszStripTail)) 37 pszStripTail--; 38 *(++pszStripTail) = '\0'; 39 40 return pszStripHead; 41 } 42 43 static char *pszLineRead = NULL; //終端輸入字符串 44 static char *pszStripLine = NULL; //剔除前端空格的輸入字符串 45 char *ReadCmdLine() 46 { 47 //若已分配命令行緩衝區,則將其釋放 48 if(pszLineRead) 49 { 50 free(pszLineRead); 51 pszLineRead = NULL; 52 } 53 //讀取用戶輸入的命令行 54 pszLineRead = readline(pszCmdPrompt); 55 56 //剔除命令行首尾的空白字符。若剔除後的命令不爲空,則存入歷史列表 57 pszStripLine = StripWhite(pszLineRead); 58 if(pszStripLine && *pszStripLine) 59 add_history(pszStripLine); 60 61 return pszStripLine; 62 } 63 64 static char *CmdGenerator(const char *pszText, int dwState) 65 { 66 static int dwListIdx = 0, dwTextLen = 0; 67 if(!dwState) 68 { 69 dwListIdx = 0; 70 dwTextLen = strlen(pszText); 71 } 72 73 //當輸入字符串與命令列表中某命令部分匹配時,返回該命令字符串 74 const char *pszName = NULL; 75 while((pszName = GetCmdByIndex(dwListIdx))) 76 { 77 dwListIdx++; 78 79 if(!strncmp (pszName, pszText, dwTextLen)) 80 return strdup(pszName); 81 } 82 83 return NULL; 84 } 85 86 static char **CmdCompletion (const char *pszText, int dwStart, int dwEnd) 87 { 88 //rl_attempted_completion_over = 1; 89 char **pMatches = NULL; 90 if(0 == dwStart) 91 pMatches = rl_completion_matches(pszText, CmdGenerator); 92 93 return pMatches; 94 } 95 96 //初始化Tab鍵能補齊的Command函數 97 static void InitReadLine(void) 98 { 99 rl_attempted_completion_function = CmdCompletion; 100 } 101 102 #endif
自動補全後的命令字符串結尾多出一個空格,故需調用StripWhite將該空格剔除。
最後,可編寫交互式調測函數以下:
1 int main(void) 2 { 3 #ifndef __READLINE_DEBUG 4 printf("Note: Macro __READLINE_DEBUG is Undefined, thus InteractiveCmd is Unavailable!!!\n\n"); 5 #else 6 printf("Note: Welcome to Interactive Command!\n"); 7 printf(" Press 'Quit'/'Exit'/'End'/'Bye'/'Q'/'E'/'B' to quit!\n\n"); 8 InitReadLine(); 9 while(1) 10 {//也可加入超時機制以避免忘記退出 11 char *pszCmdLine = ReadCmdLine(); 12 if(IsUserQuitCmd(pszCmdLine)) 13 { 14 free(pszLineRead); 15 break; 16 } 17 18 ExecCmd(pszCmdLine); 19 } 20 #endif 21 22 return 0; 23 }
該函數用法相似Shell,便於定製調測命令的隨機執行。命令中首個參數(本文參數惟一)支持自動補全,但參數區分大小寫。
編譯連接時需加載readline庫和termcap(或ncurses)庫。ncurses庫一般使用terminfo(終端信息),少數實現會使用termcap(終端能力)。啓用Readline庫時,執行結果以下:
1 [wangxiaoyuan_@localhost test1]$ gcc -Wall -o ReadLine ReadLine.c -D__READLINE_DEBUG -lreadline -lncurses 2 [wangxiaoyuan_@localhost test1]$ ./ReadLine 3 Note: Welcome to Interactive Command! 4 Press 'Quit'/'Exit'/'End'/'Bye'/'Q'/'E'/'B' to quit! 5 6 clover>>ShowMeInfo(完整輸入) 7 Enter ShowMeInfo! 8 clover>>ShowMeInfo(UP鍵調出歷史命令) 9 Enter ShowMeInfo! 10 clover>>SetLogCtrl (輸入'Se'自動補全) 11 Enter SetLogCtrl! 12 clover>> TestEndianOper(錯誤輸入) 13 clover>>TestEndian (輸入'T'自動補全爲"Test",再輸入'E'自動補全爲"TestEndian ") 14 Enter TestEndianOper! 15 clover>> TestBatch (命令首尾加空格,沒法自動補全) 16 Enter TestBatch! 17 clover>>ReadLine (輸入'R'自動補全文件名) 18 clover>>quit 19 [wangxiaoyuan_@localhost test1]$
不啓用Readline庫時,執行結果以下:
1 [wangxiaoyuan_@localhost test1]$ gcc -Wall -o ReadLine ReadLine.c 2 ReadLine.c:41: warning: 'GetCmdByIndex' defined but not used 3 ReadLine.c:49: warning: 'ExecCmd' defined but not used 4 [wangxiaoyuan_@localhost test1]$ ./ReadLine 5 Note: Macro __READLINE_DEBUG is Undefined, thus InteractiveCmd is Unavailable!!!