“命令终端”的实现4-优化之解耦

这段时间一直在做测试的工程(不是测试的工作),为了应付不同的测试场景,代码使用了解释器风格,至于实现,则使用了多年前写的命令终端代码。那会刚毕业不久,写的代码还是有提升空间。现在重新拾起,打破一般认知中的看不懂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)
{
// 判断是否为相同的命令(第0条命令没有相同的说法)
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.outa_all.out为执行文件名称作为示例。不管哪种形式,都可以直接复用已有的模块。笔者实际使用的场景,是一个用于自测的程序,有时,需要手动修改参数进行测试,有时需要将程序放到后台执行(因为耗时较长)。

本工程源码可在 github 上找到:https://github.com/latelee/creadline