Linux下使用readline庫編程實現多級CLI菜單

一、背景

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章