GNU Readline 庫及程式設計簡介
用過 Bash 命令列的一定知道,Bash 有幾個特性:
TAB
鍵可以用來命令補全↑
或↓
鍵可以用來快速輸入歷史命令- 還有一些互動式行編輯快捷鍵:
C-A
/C-E
將游標移到行首/行尾C-B
/C-F
將游標向左/向右移動一個位置C-D
刪除游標下的一個字元C-K
刪除游標及游標到行尾的所有字元C-U
刪除游標到行首的所有字元- ...
同樣的操作在很多互動式程式都有類似的操作,例如 ftp、gdb 等等,那麼你是否想過這些是如何實現的呢?如果我們要做一個命令列下的互動式開源軟體,是否希望也能有這些命令補全、搜尋歷史命令、行編輯快捷鍵等等這些人性化的互動方式呢?
要想實現這些,你有兩種途徑:可以自己寫程式實現,或者呼叫開源的庫 Readline Lib。例如上面介紹的 bash、ftp、gdb 等等軟體都使用了 GNU 的開源跨平臺庫,為其提供互動式的文字編輯功能。當然需要注意的是,Readline Library 是 GNU 自由軟體,在 GNU GPL V3 協議下發布,因此如果你的程式中需要用到該庫,也必須遵守相關協議。
本文首先簡單介紹一下該庫的基本使用方法,後面會稍微詳細介紹下如何使用 Readline 來自定義命令補全功能。
Readline 基本操作
很多命令列互動式程式互動方式都差不多,輸出提示符,等待使用者輸入命令,使用者輸入命令之後按回車,程式開始解析命令並執行。那麼這裡面有個動作是讀入使用者的輸入,以前我們也許使用 gets()
readline()
函式來替換它,該函式在 ANSI C 中定義如下:
char *readline (char *prompt);
該函式帶有一個引數 prompt,表示命令提示符,例如 ftp 中就是 "ftp>",使用者在後面可以輸入命令,當按下回車鍵時,程式讀入該行(不包括最後的換行符)存入字元緩衝區中,readline
的返回值就是該行文字的指標。注意:當該行文字不需要使用時,需要釋放該指標指向的空間,防止記憶體洩漏。當讀入 EOF
時,如果還未讀入其它字元,則返回 (char *) NULL
,否則讀入結束,與讀入換行效果相同。
除了能讀入使用者的輸入,我們有時希望互動更簡單些,例如命令補全。當有很多命令時,如果希望使用者都能準確記憶命令的拼寫是困難的,那麼一般做法是按下 TAB
鍵進行命令提示及補全,如 ftp 下輸入一個字元c
之後按下 TAB
鍵,會列出所有以 c
開頭的命令:
ftp> c
case cd cdup chmod close cr
readline
函式其實已經給使用者預設的 TAB
補全的功能:根據當前路徑下檔名來補全。
如果你不想 Readline 根據檔名補全,你可以通過 rl_bind_key()
函式來改變 TAB
鍵的行為。該函式的原型為:
int rl_bind_key(int key, int (*function)());
該函式帶有兩個引數:key 是你想繫結鍵的 ASCII 碼字元表示,function 是當 key 鍵按下時觸發呼叫函式的地址。如果想按下 TAB
鍵就輸入一個製表符本身,可以將 TAB
繫結到 rl_insert()
函式,這是 Readline 庫提供的函式。如果 key 不是有效的 ASCII 碼值(0~255之間),rl_bind_key()
返回非 0。
這樣,禁止 TAB
的預設行為,下面這樣做就可以了:
rl_bind_key('\t', rl_insert);
這個程式碼需要在你程式一開始就呼叫;你可以寫一個函式叫 initialize_readline()
來執行這個動作和其它一些必要的初始化,例如安裝使用者自定義補全。
當我們希望輸入 TAB
時不是列出當前路徑下的所有檔案,而是列出程式內建的一些命令,例如上面舉到 ftp 的例子,這種行為稱為自定義補全。 該操作較複雜,我們留在後面一節主要介紹。
基本操作還有一個——搜尋歷史。我們希望輸入過的命令列,還可以通過 C-p
或者 C-s
來搜尋到,那麼就需要將命令列加入到歷史列表中,可以呼叫 add_history()
函式來完成。但儘量將空行也加入到歷史列表中,因為空行佔用歷史列表的空間而且也毫無用處。綜上,我們可以寫出一個 Readline 版的 gets()
函式 rl_gets()
:
/* A static variable for holding the line. */
static char *line_read = (char *)NULL;
/* Read a string, and return a pointer to it. Returns NULL on EOF. */
char *
rl_gets ()
{
/* If the buffer has already been allocated, return the memory
to the free pool. */
if (line_read)
{
free (line_read);
line_read = (char *)NULL;
}
/* Get a line from the user. */
line_read = readline ("");
/* If the line has any text in it, save it on the history. */
if (line_read && *line_read)
add_history (line_read);
return (line_read);
}
自定義補全
上面也提到了什麼是自定義補全,無疑這在命令列互動式程式中是非常重要的,直接影響到使用者體驗。Readline 庫提供了兩種比較常用的補全方式——按照檔名補全和按照使用者名稱補全,分別對應 Readline 中已經實現的兩個函式 rl_filename_completion_function
和 rl_username_completion_function
。如果我們既不希望按照檔名和使用者名稱來補全,希望按照程式的命令補全,應該怎麼做呢?也很容易想到,只要實現自己的補全函式就好了。
Readline 補全的工作原理如下:
- 使用者介面函式
rl_complete()
呼叫rl_completion_matches()
來產生可能的補全列表; - 內部函式
rl_completion_matches()
使用程式提供的 generator 函式來產生補全列表,並返回這些匹配的陣列,在此之前需要將 generator 函式的地址放到rl_completion_entry_function
變數中,例如上面提到的按檔名或使用者名稱補全函式就是不同的 generators; - generator 函式在
rl_completion_matches()
中不斷被呼叫,每次返回一個字串。generator 函式帶有兩個引數:text 是需要補全的單詞的部分,state 在函式第一次呼叫時為 0,接下來呼叫時非 0。generator 函式返回(char *)NULL
通知rl_completion_matches()
沒有剩下可能的匹配。
Readline 庫中有個變數 rl_attempted_completion_function
,改變數型別是一個函式指標rl_completion_func_t *
,我們可以將該變數設定我們自定義的產生匹配的函式,該按下 TAB
鍵時會呼叫該函式,函式具有三個引數:
- text: 該引數是待補全的單詞的部分,例如在 Bash 提示符後輸入一個
c
字元,按下TAB
,此時 text指向的是 "c" 字串的指標;在 Bash 提示符後輸入一個cd /home/gu
字串,按下TAB
,此時 text指向的是 "/home/gu" 字串的指標; - start: text 字串在該行輸入中的起始位置,例如對於上面的例子,第一種情況下是 0,第二種情況下是 3;
- end: text 字串在該行輸入中的結束位置,例如對於上面的例子,第一種情況下是 1,第二種情況下是 11。
我們自定義的補全函式可以根據傳入的引數來設定我們希望按照什麼方式補全,例如對於 Bash 下的 cd
命令,我們希望開始是命令補全,當命令補全之後,後面接著跟的是檔名補全,這樣可以使用rl_completion_matches()
來繫結使用哪種 generator,rl_completion_matches()
函式的原型是:
char ** rl_completion_matches (const char *text, rl_compentry_func_t *entry_func)
帶有兩個引數:text 就是上面介紹的傳入的待補全的單詞,第二個引數 entry_func 是上面反覆介紹的generator 函式的指標。該函式的返回值是 generator 產生的可能匹配 text 的字串陣列指標,該陣列的最後一項是 NULL
指標。
好了,上面說了這麼多關於自定義補全的函式和變數,到底怎麼用呢,估計還是比較模糊,那麼看一個例子估計就很清楚了,這個例子是 Readline 官方提供的示例程式,由於比較長,就不在這裡貼出來了,你可以在 http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC49 找到。
總結
其實,雖然說了很多,但還只是 Readline 庫的皮毛,這個庫的功能遠遠比這強大的多,如果想深入瞭解並且運用,你必須要做三件事:
- Read The Fucking Manual:閱讀官方的 文件
- Read The Fucking Source Code:閱讀官方提供的例子程式碼,如果想了解更深入可以去看 Readline 的原始碼
- Show Your Code:自己動手寫幾個例子試試,如果有機會運用到你的專案中。