Linux下使用readline库编程实现多级CLI菜单

来源:互联网 发布:金华比奇网络充值诈骗 编辑:程序博客网 时间:2024/05/22 17:27

一、背景

CLI是一种快速简洁的人机交互方式,优秀的CLI(如 mysql、vtysh、gdb)带给我们非常好的体验。那么CLI都是如何开发出来的?


二、相关知识

2.1 CLI vs GUI

文章[1] 纵观CLI与GUI的发展进行比对:CLI命令行交互对于使用者而言,就是专业、高效;而GUI界面式的交互就是直观、易用;

2.2 readline

CLI的开发中可以借助 readline库提高输入的体验性,如文章[3]所分析,在Bash的使用中经常用到的:

  • tab自动补齐;
  • 上下查看历史命令;
  • 光标移动、输入删除;

这些特性均可以由 readline库进行提供,关于API的使用可以参考官方文档-文章[2];

三、实现

本实现根据 readline/example/fileman.c 案例进行修改;

考虑设计多级菜单选项时,需要通过提示符进行切换,如 "system >"、"system (route) >"、"system (route-config) >"提示所在的菜单项;

并且在每个菜单项下,需要支持不同的命令集,对不同的命令进行相应操作,如 open 加载配置、write 保存配置、quit返回上级、exit 退出程序等操作;

所以就在上下文数据结构上,使用下图的这种结构:


typedef struct command{char name[SIZE_NAME_NORMAL];int (*callback)(void *, char *);void *args;char info[SIZE_NAME_LONG];} command_t;typedef struct menu{char prompt[SIZE_NAME_NORMAL];size_t cmd_size;struct command *pcmd;} menu_t;typedef struct instance{u8 enable;enum menu_e {MENU_1 = 0,MENU_2,MENU_3,MENU_MAX,} menu_idx;struct menu menu[MENU_MAX];} instance_t;
为了便于结构的查看,对各个菜单使用 menu1、2、3进行抽象;然后对应各个菜单定义提示内容、命令集;

static instance_t g_inst = {.enable = 1,.menu = {[ MENU_1 ] = {"menu_1 > ", 0, NULL},[ MENU_2 ] = {"menu_2 > ", 0, NULL},[ MENU_3 ] = {"menu_3 > ", 0, NULL},}};static command_t g_menu1_cmd[] = {{"cmd_1_2", cmd_1_2, &g_inst, "Jump to menu2"},{"cmd_1_3", cmd_1_3, &g_inst, "Jump to menu3"},{"exit", cmd_exit, &g_inst, "Exit program"},{"help", cmd_help, &g_inst, "Help message"},{"?",    cmd_help, &g_inst, "Help message"},};static command_t g_menu2_cmd[] = {{"cmd_2_1", cmd_2_1, &g_inst, "Jump to menu1"},{"cmd_2_3", cmd_2_3, &g_inst, "Jump to menu3"},{"exit", cmd_exit, &g_inst, "Exit program"},{"help", cmd_help, &g_inst, "Help message"},{"?",    cmd_help, &g_inst, "Help message"},};
然后就考虑菜单之间的切换了,这里是利用 menu_idx 对菜单状态进行一个维护,即切换菜单时修改 menu_idx,对应的上下文随着改变

所以在 cmd1_2\2_3\3_1 里面是对 menu_idx 进行修改的;

另外的侧重点就是,如何使用readline执行命令、如何使用readline自动补齐;

初始化、循环获取输入行的入口函数:

static void __do_init(){g_inst.menu_idx = MENU_1;g_inst.menu[MENU_1].pcmd = g_menu1_cmd;g_inst.menu[MENU_2].pcmd = g_menu2_cmd;g_inst.menu[MENU_3].pcmd = g_menu3_cmd;g_inst.menu[MENU_1].cmd_size = sizeof(g_menu1_cmd) / sizeof(command_t);g_inst.menu[MENU_2].cmd_size = sizeof(g_menu2_cmd) / sizeof(command_t);g_inst.menu[MENU_3].cmd_size = sizeof(g_menu3_cmd) / sizeof(command_t);}void readline_init(){/* Allow conditional parsing of the ~/.inputrc file. */rl_readline_name = "readline_history";/* Tell the completer that we want a crack first. */rl_attempted_completion_function = readline_completion;__do_init();}void readline_loop(){char *pline = NULL;char *ps = NULL;for ( g_inst.enable = 1; g_inst.enable; ) {pline = readline(g_inst.menu[g_inst.menu_idx].prompt);if ( !pline ) {break;}ps = __do_stripwhite(pline);if ( ps ) {add_history(ps);__do_cmd_execute(&g_inst.menu[g_inst.menu_idx], ps);}free(pline);}}int main(int argc, char *argv[]){readline_init();/* Bind our completer. */readline_loop();return EXIT_SUCCESS;}


获取输入行成功后,立刻进行命令匹配、执行命令回调函数:

static int __do_cmd_execute(menu_t *pmenu, char *line){int ix = 0;command_t *pcmd = NULL;char *word = NULL;/* Isolate the command word. */while ( line[ix] && whitespace(line[ix]) ) {ix++;}word = line + ix;while ( line[ix] && !whitespace(line[ix]) ) {ix++;}if ( line[ix] ) {line[ix++] = '\0';}pcmd = command_match(pmenu->pcmd, pmenu->cmd_size, word);if ( !pcmd ) {fprintf (stderr, "%s: Unknow command.\n", word);return FAILURE; }/* Get argument to command, if any. */while ( whitespace(line[ix]) ) {ix++;}word = line + ix;return ((*(pcmd->callback))(pcmd->args, word));}static char *__do_stripwhite(char *string){char *s, *t;for (s = string; whitespace (*s); s++);if ( *s == 0 ) {return s;}t = s + strlen (s) - 1;while ( t > s && whitespace (*t) ) {t--;}*++t = '\0';return s;}command_t *command_match(command_t *pcmd, size_t size, char *name){int ix = 0;if ( !name ) {return NULL;}for ( ix = 0; pcmd[ix].name, ix < size; ix++ ) {if ( !strcmp(name, pcmd[ix].name) ) {LOGD("Match: %s\n", name);return &pcmd[ix];}}return NULL;}

上述提到的命令执行的过程,下面则要说一下命令的自动补齐功能:

/* Generator function for command completion.  STATE lets us know whether   to start from scratch; without any state (ix.e. STATE == 0), then we   start at the top of the list. */static char *__do_cmd_generator(const char *text, int state){static int cmd_idx, len;char *name;menu_t *pmenu = &g_inst.menu[g_inst.menu_idx];/* If this is a new word to complete, initialize now.  This includes   saving the length of TEXT for efficiency, and initializing the index   variable to 0. */if ( !state ) {cmd_idx = 0;len = strlen(text);}/* Return the next name which partially matches from the command list. */while ( name = pmenu->pcmd[cmd_idx].name ) {if ( cmd_idx++ >= pmenu->cmd_size ) {break;}if ( strncmp(name, text, len) == 0 ) {/* Readline frees the strings when it has finished with them */return (strdup(name));}}/* If no names matched, then return NULL. */return NULL;}

四、总结

需要注意的是,readline 的用户接口函数 rl_completion_matches() 产生自动补齐的列表;

内部函数 rl_completion_matches() 使用程序提供的 generator 函数来产生补全列表,并返回这些匹配的数组进行显示;

在此之前需要将 generator 函数的地址放到 rl_completion_entry_function 变量中,例如上面的命令补全函数就是不同的 __do_cmd_generator(),注意返回值需申请新的指针空间;

同时,readline 库中有个变量 rl_attempted_completion_function,改变量类型是一个函数指针rl_completion_func_t *,我们可以将该变量设置我们自定义的产生匹配的函数,并绑定到 TAB 键的回调;



参考文章:

[1] http://www.cnitblog.com/addone/archive/2008/01/08/38581.html

[2] http://cnswww.cns.cwru.edu/php/chet/readline/readline.html

[3] http://www.cnblogs.com/hazir/p/instruction_to_readline.html

0 0
原创粉丝点击