上一篇博客我分析了linenoise中的数据结构linenoiseCompletions和abuf,这篇我分析一下linenoise的历史命令取回和linenoiseState结构体。linenoiseState结构体即是这个库核心的部分,起到控制当前用户输入状态的功能。而history部分并不复杂,只是仅仅操作一个char** history数组,这个数组是固定大小的“双端队列”。
一、history模块:这部分功能都写在linenoise.c文件,以下是几个主要的全局变量,用于控制:
static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN;
static int history_len = 0;
static char **history = NULL;
history_max_len,所能记录的history条数; history_len ,目前history变量中装填的history数量; history, 用于历史记录存储,每个历史记录是一个char*。
history提供四个导出函数,一个内部函数:
int linenoiseHistoryAdd(const char *line);
int linenoiseHistorySetMaxLen(int len);
int linenoiseHistorySave(const char *filename);
int linenoiseHistoryLoad(const char *filename);
static void freeHistory(void);
单看名字应该能猜到这些函数的用途了。
(1)linenoiseHistoryAdd ():将字符串存入history中,这其中说一点,history变量的初始化是通过Load函数或者Add函数完成的,当用户有history文件时,调用Load函数直接加载到history变量,history即完成了初始化,并且填充了元素。若用户没有调用Load函数,在使用Add时,会检测history是否已经初始化,若没有初始化,则malloc(),元素个数就是history_max_len。接着完成添加任务,上一次的历史若与这一次欲添加的历史相同,则直接返回,不添加。若不同,检测history是否已经装满,若装满,将history[0]释放,用memmove腾出最后一个元素空间,把欲添加的字符串strdup()后赋值。
/* This is the API call to add a new entry in the linenoise history.
* It uses a fixed array of char pointers that are shifted (memmoved)
* when the history max length is reached in order to remove the older
* entry and make room for the new one, so it is not exactly suitable for huge
* histories, but will work well for a few hundred of entries.
*
* Using a circular buffer is smarter, but a bit more complex to handle. */
int linenoiseHistoryAdd(const char *line) {
char *linecopy;
if (history_max_len == 0) return 0;
/* Initialization on first call. */
if (history == NULL) {
history = malloc(sizeof(char*)*history_max_len);
if (history == NULL) return 0;
memset(history,0,(sizeof(char*)*history_max_len));
}
/* Don't add duplicated lines. */
if (history_len && !strcmp(history[history_len-1], line)) return 0;
/* Add an heap allocated copy of the line in the history.
* If we reached the max length, remove the older line. */
linecopy = strdup(line);
if (!linecopy) return 0;
if (history_len == history_max_len) { // no free space.
free(history[0]);
memmove(history,history+1,sizeof(char*)*(history_max_len-1));
history_len--;
}
history[history_len] = linecopy;
history_len++;
return 1;
}
(2)int linenoiseSetMaxLen (int len):此函数用于设置变量history_max_len,当然,设置后需要扩展或收缩history指向的char*数组。这里先新malloc()一个空间,之后将队列中的元素复制到新的空间,再用这个空间赋值给history。
if (len < 1) return 0;
if (history) {
int tocopy = history_len;
new = malloc(sizeof(char*)*len);
if (new == NULL) return 0;
/* If we can't copy everything, free the elements we'll not use. */
if (len < tocopy) {
int j;
for (j = 0; j < tocopy-len; j++) free(history[j]);
tocopy = len;
}
memset(new,0,sizeof(char*)*len);
memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy);
free(history);
history = new;
}
代码中注意判断len < tocopy的块,若新设置的len小于原来的history_len大小,则无法将原来的history全部复制到history数组,只能截取一部分复制。设计者的处理方式是将最近的历史保留下来,即是将“队尾”(history末尾len个元素)的元素复制到新分配的内存空间中。
(3)int linenoiseHistorySave (const char* filename):将history中的数据存入文件中,成功则返回0,失败返回-1。函数很简单,直接调用open打开文件并for 写入history数据。
(4)int linenoiseHistoryLoad (const char* filename); 加载历史数据到history,也是用open () 打开文件,并用循环调用了linenoiseHistoryAdd (),所以可以看出,其实真正的history内存分配只有一个,就是linenoiseHistoryAdd () 函数。
二、linenoiseState结构体:
/* The linenoiseState structure represents the state during line editing.
* We pass this state to functions implementing specific editing
* functionalities. */
struct linenoiseState {
int ifd; /* Terminal stdin file descriptor. */
int ofd; /* Terminal stdout file descriptor. */
char *buf; /* Edited line buffer. */
size_t buflen; /* Edited line buffer size. */
const char *prompt; /* Prompt to display. */
size_t plen; /* Prompt length. */
size_t pos; /* Current cursor position. */
size_t oldpos; /* Previous refresh cursor position. */
size_t len; /* Current edited line length. */
size_t cols; /* Number of columns in terminal. */
size_t maxrows; /* Maximum num of rows used so far (multiline mode) */
int history_index; /* The history index we are currently editing. */
};
这个结构体并不对外导出。作者给了一个example.c,其中只是调用了一个入口函数linenoise(),循环接收用户的输入,当用户回车时,这个函数返回,并返回用户输入的字符串命令。
50 while((line = linenoise ("hello> ")) != NULL)
在linenoise() 中,将调用linenoiseRaw (),而linenoiseRaw () 再调用linenoiseEdit () 。一个linenoiseState实体将在这个函数中定义,当然是分配到栈上。换言之,当进行一次while循环时,用一个linenoiseState控制这次循环,每次循环这个linenoiseState都是新的。我们先从这三个函数看起。
(1)char* linenoise (const char* prompt); 这是linenoise库的核心函数,其中先进行了terminal支持性判断,根据判断做相应的操作。这里linenoise不支持以下几种终端类型:
static char *unsupported_term[] = {"dumb","cons25","emacs",NULL};
可以通过在terminal执行命令 echo $TERM, 打印这个环境变量来检测当前是什么terminal。我的Kali2系统使用的是xterm。若是linenoise不支持的终端,则那些终端快捷键将无法使用,只能等待用户输入,并将整行命令返回。
if (isUnsupportedTerm()) {
size_t len;
printf("%s",prompt);
fflush(stdout);
if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL;
len = strlen(buf);
while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
len--;
buf[len] = '\0';
}
return strdup(buf);
} else {
count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt);
if (count == -1) return NULL;
return strdup(buf);
}
此处通过调用isUnsupportedTerm()检测,具体是遍历数组unsupported_term,不做说明。
若是支持的terminal,则继续调用linenoiseRaw(), 这个函数将用户的输入存入buf,并返回count,命令字串的长度。返回时使用了strdup(),所以每次while 循环linenoise()后,都要进行free处理。此处也可以进行改进,比如linenoise函数可以接收一个存储缓冲区,while运行单线程,这样即可只分配一次内存,长期使用了,不用每次都调用strdup和free。
(2) linenoiseRaw 函数:
/* This function calls the line editing function linenoiseEdit() using
* the STDIN file descriptor set in raw mode. */
static int linenoiseRaw(char *buf, size_t buflen, const char *prompt);
在这个函数中,主要检测是否STDIN_FILENO是否是tty,若不是,则将stdin按文件或管道读取,否则,调用linenoiseEdit (),这个函数是与用户交互的真正入口。
/* Interactive editing. */
if (enableRawMode(STDIN_FILENO) == -1) return -1;
count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);
disableRawMode(STDIN_FILENO);
printf("\n");
先开启RawMode,再调用linenoiseEdit (),最后关闭disableRawMode.而在linenoiseEdit函数中,将存在一个while循环,用于接收用户的每个字符输入,除非接收到enter键、其他字符,或者ctrl+c,退出循环。解读每一个字符,使用的跳转是switch。以下列出了linenoise库支持的终端快捷键,在这个switch中都有跳转,很多直接实现成函数,主要是调整state的pos,len变量,并调用refreshLine()刷新state结构体:
a)光标移动到行尾,end键或者esc 0 F
b)光标移动到行首,home键或者esc 0 H
c)光标左移动,方向键<-或者esc [ D
d)光标右移动,方向键->或者esc [ C
e)enter键
f)ctrl + c,中断
g)backspace,退格键
h)ctrl+h,同backspace
i)ctrl+D,同delete键
j)ctrl+t,交换光标和光标前一个字符的位置。
k)ctrl +b,光标左移一格
l)ctrl+f,光标右移一格
m)ctrl+p,取回上一个历史命令
n)ctrl+n,取回下一个历史命令
o)esc [ 3 ~,同delete键
p)ctrl+u,删除整行
q)ctrl+k,删除从当前光标到行尾的所有字符
r)ctrl+a,同home
s)ctrl+e,同end
t)ctrl+l,清屏,同终端命令clean
u)ctrl+w,删除前一个单词
此处不一一列举出来了。