当前位置: 首页 > 工具软件 > linenoise > 使用案例 >

linenoise 源码分析(一)

潘驰
2023-12-01

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,我在下一篇博客中分析。

 类似资料: