2021-07-13 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 库时,可以使用 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_functionrl_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() 来绑定使用哪种 generatorrl_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:自己动手写几个例子试试,如果有机会运用到你的项目中。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容

  • Node.js中文网的 v6.10.3 文档提供了readline模块,可以从可读流(process.stdin)...
    Evtion阅读 5,458评论 0 4
  • require('readline') 模块提供了一个接口,用于从可读流(如 process.stdin)读取数据...
    imakan阅读 10,116评论 0 1
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,322评论 0 6
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,380评论 0 5
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,535评论 28 53