linenoise是一个命令行编辑库(line editing library),由redis数据库设计者Antirez发布一个用于颠覆“一个命令行编辑库需要20000行C代码”的观点。目前linenoise已经使用在Redis, MongoDB,Android等应用上。其支持特性如下:
1) 尽可能简小,无配置,支持BSD license。
2) 单行或多行编辑模式,使用常用的快捷键绑定实现。
3)支持历史键入文本查询。
4)兼容性。
5)大约只有1100行代码,支持BSD license。
6)只使用VT100 escapes键码的子集。
我阅读了linenoise源码,非常简单,非常简练,其中也有很多值得学习的东西,于是将这些东西记录下来,供自己使用,也供各位参考。
一、 文件构成:
linenoise库文件非常少,没有子目录,其中的核心文件仅仅是linenoise.c 和linenoise.h,还有一个example.c,作为使用的教学程序。当然,还有一个makefile和README.markdown,此处我只分析核心的linenoise.c和linenoise.h。
二、数据结构:
(1)linenoiseCompletions:
typedef struct linenoiseCompletions {
size_t len;
char **cvec;
} linenoiseCompletions;
此结构用于帮助实现用户输入自动补全功能。就像我们使用linux命令行终端一样,当我们键入命令头几个字母,使用tab键便可以完成自动补全或查找匹配,或者我们指定cd路径,输入路径头几个字符,tab后完成后面的补全工作。而linenoise实现的也是这个功能,但真正的匹配工作需要你注册函数完成,就是linenoise中的completionCallback。
此结构体只有两个导出方法,第一个是:
void linenoiseAddCompletion(linenoiseCompletions *, const char *);
将字符串指针添加到结构体的cvec变量。这个过程主要通过memmove() 和 realloc() 两个函数完成。
typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *);
void linenoiseSetCompletionCallback(linenoiseCompletionCallback *);
注册用于完成匹配工作的函数,仅仅是个简单的指针赋值,指针存储在一个全局变量中。
非导出函数:
static void freeCompletions (linenoiseCompletions *lc);
回首指定的结构体linenoiseCompetions,仅仅回首其中的char** cvec和减少len字段的值。循环len次调用free()回首所有的字符串指针,再回收一次cvec。
static int completeLine(struct linenoiseState *ls)
这是自动补全功能的核心函数。参数为linenoise库状态结构体,这个结构体记录了当前用户的输入状态,用于控制输入输出,是整个库的核心。此函数提供给内部函数linenoiseEdit()调用,用于处理用户输入的tab键,并完成自动补全。当然,这个函数必须在用户已经注册linenoiseCallback后才能使用。处理过程如下:
a)初始化一个局部变量linenoiseCompletions lc,调用回调函数匹配用户没有补全的输入,并将查找到的结果填充到lc。
b)若结果为空,beep一次,否则进入循环输入处理,循环将用户输入分三类处理,tab,esc和其他。
c)循环查找lc中的匹配命令,若已经完成一次遍历,调用refreshLine (),此函数只是调整linenoiseState结构体,并不输出。若还有剩余元素需要遍历,存储state当前状态,将匹配到的命令存入state,再refreshLine(),最后再还原。
d)接着读取标准输入的一个字符,失败则调用freeCompletions()回收lc,否则switch成三类按键
若是tab键,将lc中数组的循环变量加1,这里不是简单加1,公式如下:
i = (i+1) % (lc.len+1);
i是循环变量,其取值范围控制在[ 0, lc.len ]范围,这样当i取到最大值时,下次循环会再次回绕到最小值,保证了不停的循环这个lc.cvec数组。也就是我们在命令行不停按tab时提供的匹配功能。当i取lc.len时,判断已经完成一次循环迭代。
若按的是escape(ESC)键,则先检测是否已经迭代完了,迭代完则调整一次state,refreshLine ()。最后跳出循环。
若是按其他键,则将查找到的结果存入state,并调整,最后跳出循环。
e)回收lc,将d)中read得到的字符作为返回值输出。
以上描述可能很冗杂,下面是源代码,对照看即可:
/* This is an helper function for linenoiseEdit() and is called when the
* user types the <tab> key in order to complete the string currently in the
* input.
*
* The state of the editing is encapsulated into the pointed linenoiseState
* structure as described in the structure definition. */
static int completeLine(struct linenoiseState *ls) {
linenoiseCompletions lc = { 0, NULL };
int nread, nwritten;
char c = 0;
// ls -> buf stored user typed command.
completionCallback(ls->buf,&lc); // found completion.
if (lc.len == 0) { // find nothine !
linenoiseBeep();
} else {
size_t stop = 0, i = 0;
while (!stop) {
/* Show completion or original buffer */
if (i < lc.len) {
struct linenoiseState saved = *ls;
ls->len = ls->pos = strlen(lc.cvec[i]);
ls->buf = lc.cvec[i];
refreshLine(ls); // do not print to stdout.
ls->len = saved.len;
ls->pos = saved.pos;
ls->buf = saved.buf;
} else {
refreshLine(ls);
}
nread = read(ls->ifd,&c,1);
if (nread <= 0) {
freeCompletions(&lc);
return -1;
}
switch(c) {
case 9: /* tab */
i = (i+1) % (lc.len+1);
if (i == lc.len) linenoiseBeep();
break;
case 27: /* escape */
/* Re-show original buffer */
if (i < lc.len) refreshLine(ls);
stop = 1;
break;
default:
/* Update buffer and return */
if (i < lc.len) {
nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]);
ls->len = ls->pos = nwritten;
}
stop = 1;
break;
}
}
}
freeCompletions(&lc);
return c; /* Return last read character */
}
(2)结构体abuf,实现成最简单的append buffer,是一个heap allocated string。这个结构体很简单,只有一个char*变量作为buffer,还有一个len字段标示buffer当前长度。
这个结构体作为库的内部使用,不向外导出。主要用于refreshLine函数,进行字符串追加操作。所以提供的函数也很简单:
static void abInit(struct abuf *ab);
初始化,不调用malloc(),仅在append时才调用realloc(),分配内存。
static void abAppend(struct abuf *ab, const char *s, int len);
将s字符串追加到ab中,len指定了s长度。此函数只是简单调用realloc()调整分配的内存大小和memcpy()进行数据复制。
static void abFree (struct abuf *ab)
回收一个abuf的结构体实例,此方法并不回收abuf,只是回收abuf 中的char* 缓冲区。
以上是两个内部结构体在库中的用途和接口分析,还有一个核心linenoiseState,我在下一篇博客中分析。