这段时间一直在做测试的工程(不是测试的工作),为了应付不同的测试场景,代码使用了解释器风格,至于实现,则使用了多年前写的命令终端代码。那会刚毕业不久,写的代码还是有提升空间。现在重新拾起,打破一般认知中的看不懂6个月前写的代码的刻板印象。
存在问题
原来的工程使用C代码编写,并不严格区分测试代码和实现代码,其中最大的问题是将命令列表做成全局变量并依赖于外部的定义,这样耦合程序非常高。因此需要分离出来。
原工程的文件命名也不太好,如common.h
这样的文件,在与其它工程整合时容易冲突,此次一并修改了。
命令解耦
依赖定义全局的命令列表,但只是指针,添加注册命令接口,由外部使用者调用。将默认的帮助命令调整至内部实现,外部直接使用。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static cmd_tbl_t* cmd_table; static int cmd_table_len = 0;
/* register command */ void register_command(cmd_tbl_t* table, int len) { cmd_table = table; cmd_table_len = len; }
int do_help_default(int argc, char* argv[]) { _do_help_default(cmd_table, argc, argv); return 0; }
|
在原有测试代码基础上,添加初始化命令函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| /* 定义命令列表 */ cmd_tbl_t my_cmd_table[] = { // do_help_default为默认函数,可重新实现 {"help", CONFIG_SYS_MAXARGS, do_help_default, "print help info."}, {"print", 2, do_print, "print the env."}, {"exit", 1, do_exit, "exit..."}, {"quit", 1, do_exit, "exit..."}, };
// 初始化,注册命令 void cmd_init() { int len = sizeof(my_cmd_table) / sizeof(my_cmd_table[0]); register_command(my_cmd_table, len); }
|
这样,在主体函数开始处调用cmd_init();
即可。如此一来,结构清晰,逻辑也清晰。
历史命令优化
命令终端支持历史命令,由HIST_MAX
决定数量。默认为 10 个。历史命令使用hist_list
存储,添加命令函数如下:
1 2 3 4 5 6 7 8 9 10 11 12
| static void cread_add_to_hist(char *line) { strcpy(hist_list[hist_add_idx], line);
if (++hist_add_idx >= HIST_MAX) hist_add_idx = 0;
if (hist_add_idx > hist_max) hist_max = hist_add_idx;
hist_num++; }
|
实现很简单,先添加(因为索引从0开始),再累加并与最大值比较。达到最大值后,替换存储的第0个命令。在实际执行中,有时会出现上下命令相同的情况,此时无须再次保存,以节省空间。
修改后代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static void cread_add_to_hist(char *line) { if (hist_add_idx > 1 && !strcmp(hist_list[hist_add_idx-1], line)) return;
strcpy(hist_list[hist_add_idx], line);
if (++hist_add_idx >= HIST_MAX) hist_add_idx = 0;
if (hist_add_idx > hist_max) hist_max = hist_add_idx;
hist_num++; }
|
非命令行模式
截至目前,“命令终端”只有命令行模式,即执行程序初始化后,只会进入命令提示符界面,等待用户输入命令,再解析、执行。有时候,某些场景需要直接执行命令,即自动执行用户输入的命令,亦即将用户命令作为程序的参数。比如,将:
1 2
| NotAShell> print abc NotAShell> print 100 200
|
改为
1
| ./a_all.out "print abc; print 100 200;"
|
的形式,直接执行一次程序即可得到结果,不用手工输入命令。
实现源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| // 去掉前后空格——中间的不去掉 std::string& trim(std::string &str) { trueif (str.empty()) true{ truetruereturn str; true} truestr.erase(0, str.find_first_not_of(" ")); //去除左边空格 truestr.erase(str.find_last_not_of(" ") + 1);//去除右边空格 truereturn str; }
// 分割string,能自动去掉分隔符前后的空格 std::vector <std::string> splitString(const std::string & s, const std::string & delim) { std::vector <std::string> elems; std::string tmp; size_t pos = 0; size_t len = s.length(); size_t delim_len = delim.length(); if (delim_len == 0) return elems; while (pos < len) { int find_pos = s.find(delim, pos); if (find_pos < 0) { tmp = s.substr(pos, len - pos); elems.push_back(trim(tmp)); break; } tmp = s.substr(pos, find_pos - pos); elems.push_back(trim(tmp)); pos = find_pos + delim_len; } return elems; }
/////////////////////////
/* 本函数功能: 用于测试组装命令的场景。 使用如下: ./a_all.out "print abc; print 100 200;" 即只有一个参数。为了复用已有命令终端,将参数分析还原为argc argv形式,再调用 */
int readline_cmd_allone(int argc, char ** argv) { if (argc < 2) return -1;
// 命令参数最大为10个 #define MAX_ARGC 10 std::vector<std::string> v = splitString(argv[1], ";"); truecmd_tbl_t *ptable = NULL; char* myargv[MAX_ARGC] = {NULL}; int myargc = 0; truefor(unsigned int i=0; i<v.size(); i++) true{ // printf("split: [%s] %d\n", v[i].c_str(), v[i].empty()); truetrueif(v[i].empty()) truetrue{ truetruetruecontinue; truetrue} char cmd[128]; truetruememcpy(cmd, v[i].c_str(), 128);
truetrue// 是否再转成argv的形式?? truetrue std::vector<std::string> vv = splitString(cmd, " "); myargc = (int)vv.size(); myargc = myargc > MAX_ARGC ? MAX_ARGC : myargc; for (int j = 0; j < myargc; j++) { myargv[j] = (char*)vv[j].c_str(); }
// for (int j = 0; j < myargc; j++) // { // printf("myargv[%d]: %s\n", j, myargv[j]); // }
ptable = find_table(myargv[0]); truetrueif (ptable == NULL) truetrue{ truetruetrueprintf("cmd name: [%s] not found\n", myargv[0]); truetruetruecontinue; truetrue} truetrueprintf("name: %s\n", ptable->name);
truetrueptable->cmd(myargc, myargv); } return 0; }
|
主函数变更如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| int main(int argc, char* argv[]) { char* p; truechar* cmdname = *argv; if ((p = strrchr (cmdname, '/')) != NULL) { truetruecmdname = p + 1; true}
trueif (strcmp(cmdname, "a.out") == 0) { truetrueif (readline_cmd(argc, argv) != 0) truetruetruereturn -1;
truetruereturn 0; true} else if (strcmp(cmdname, "a_all.out") == 0) { truetrueif (readline_cmd_allone(argc, argv) != 0) truetruetruereturn -1;
truetruereturn 0; true}
return 0; }
|
代码以a.out
和a_all.out
为执行文件名称作为示例。不管哪种形式,都可以直接复用已有的模块。笔者实际使用的场景,是一个用于自测的程序,有时,需要手动修改参数进行测试,有时需要将程序放到后台执行(因为耗时较长)。
本工程源码可在 github 上找到:https://github.com/latelee/creadline