实现一个通用命令行组件
简介
最近几天, 在 deepseek 的帮助下, 倒腾了一个通用的命令行组件. 它具有以下特点:
1.易集成. 开发者只需要将2个源码集成到已有系统进行编译即可. 2.易使用. 所有命令可联想, 可通过缩写输入命令.
源代码仓库: https://github.com/switch-router-nat/cliii
使用举例
我们以注册3个命令的例子为例:
CLI_COMMAND (test_show_instance_command) = {
.path = "show instance",
.help = "Usage: show instance [id INDEX]",
.function = test_show_instance_command_fn,
};
CLI_COMMAND (test_show_link_command) = {
.path = "show link",
.help = "Usage: show link <link-id>",
.function = test_show_link_command_fn,
};
CLI_COMMAND (test_reload_config_command) = {
.path = "reload config",
.help = "Usage: reload config",
.function = test_reload_config_command_fn,
};
使用单独的登陆工具cli-ctl
登录后, 即可在提示词后, 输入对应命令(可以为简写形式). 也可输入 help 打开提示.
yc@debian:~/workspace/cliii$ ./cli-ctl
Connected to server. Type commands after 'prompt > ' prompt.
Type 'quit' to exit.
Press ←/→ to move cursor.
Press Ctrl+C to terminate at any time.
prompt >
prompt > show instance id 5
id 5 is shown
prompt > sh ins id 6
id 6 is shown
prompt > help
reload
show
prompt > show help
link
instance
prompt > show instance help
Usage: show instance [id INDEX]
实现原理
通信方式
整个组件分为两部分: 其中一个部分cli
集成到需要命令行控制的系统中,
cli
: 主要实现. 源码的方式集成到需要命令行控制的程序中。程序需提供 epoll 或类似的方式监听一个 unix socket, 接收cli-ctl
的输入, 并将其输入转发到cli
组件cli-ctl
: 一个登录命令行的二进制, 单独运行. 将用户的输入通过 unix socket 发送到cli-ctl
, 并将其返回展示给用户
已注册命令的组织
cli
首先要考虑的一件事就是需要将所有已注册的命令组织起来, 它需要根据用户输入”快速”匹配, 并能实现”分级”搜索
若采用单独的数组方式存储, 则在命令数较多时, 由于始终需要挨个遍历, 则无法做到”快速匹配”.
若采用单独的哈希表方式存储, 则无法实现”分级搜索”. 例如当用户输入仅输入”show”时, 我希望能得到所有以”show”开头的命令, 而显然哈希表方式存储无法实现(它只能搜索完整命令)
因此, 我们采用哈希表+分级位图的方式组织已注册的命令.
其中哈希表用于命令注册时的去重, 防止相同重复注册, 这个比较简单, 就不赘言.
而分级位图则用于快速匹配用户输入.分级的意思是我们会将用户注册命令以空格形式分割, 将所有”不完整”部分也注册到系统中.
举个例子: 以show instance
为例, 在注册这个命令时,cli
还会注册show
命令, 此时show
命令称为show instance
的parent命令. 我们会将instance
添加到show
的sub command中.
随后, 在注册show link
时,由于show
命令已经注册, 此时只需将link
添加到show
的sub command中. 如此, 便实现了基本的分级.
ROOT
|
+-- show
| +-- instance
| +-- link
+-- reload
+-- config
只有了分级还不够. 我们还利用位图实现快速匹配. 还是以上面的例子为例, 我们为show
命令创建位图
具体来说就是为每个sub command的可能的每个字符位置创建一组位图, 位图的数量为这个位置所有sub command的字符范围.
比如这里instance
有8个字符(假设其 sub command 索引为 0),link
有4个字符(假设其 sub command 索引为 1), 因此我们需要考虑8个位置(以最长的sub command为准)
第一个位置. 有两种字符: i 和 l, 则我们创建的2个位图 , 其中第一个位图(编号0)中记录了满足第1位为'i'的 sub command 命令索引, 即bit 0置位. 第二个位图(编号3)记录了满足第1位为'l'的 sub command 命令索引, 即bit 1置位
第二个位置. 有两种字符: n 和 i, 则我们创建的2个位图 , 其中第一个位图(编号0)中记录了满足第2位为'i'的 sub command 命令索引, 即bit 1置位. 第二个位图(编号5)记录了满足第2位为'n'的 sub command 命令索引, 即bit 0置位
第三个位置. 有两种字符: s 和 n, 则我们创建的2个位图 , 其中第一个位图(编号0)中记录了满足第3位为'n'的 sub command 命令索引, 即bit 1置位. 第二个位图(编号5)记录了满足第3位为's'的 sub command 命令索引, 即bit 0置位
第四个位置. 有两种字符: t 和 k, 则我们创建的2个位图 , 其中第一个位图(编号0)中记录了满足第4位为'k'的 sub command 命令索引, 即bit 1置位. 第二个位图(编号9)记录了满足第4位为't'的 sub command 命令索引, 即bit 0置位
第五个位置. 有一种字符: a, 则我们创建的1个位图 , 位图(编号0)中记录了满足第5位为'a'的 sub command 命令索引, 即bit 0置位
第六个位置. 有一种字符: n, 则我们创建的1个位图 , 位图(编号0)中记录了满足第6位为'n'的 sub command 命令索引, 即bit 0置位
第七个位置. 有一种字符: c, 则我们创建的1个位图 , 位图(编号0)中记录了满足第7位为'c'的 sub command 命令索引, 即bit 0置位
第八个位置. 有一种字符: e, 则我们创建的1个位图 , 位图(编号0)中记录了满足第8位为'e'的 sub command 命令索引, 即bit 0置位
如此一来, 当我们读取到用户的输入时, 就可以很快找到满足条件的 sub command
cli-ctl 的一些细节
终端模式
cli-ctl
需要运行在非规范模式(Non-canonical mode), 这样的代价是需要主动行编辑行的重绘, 但好处是可以响应左右方向键.
prompt
最初实现的时候, prompt
提示词是由cli
部分发送, 不过后来发现这部分由cli-ctl
自行输出更为方便. 不过代价就是只能使用固定的 prompt, 无法做到像 linux 终端一样的目录切换这种操作.
可提高的地方
目前为止, 这只是一个简单的实现, 至少还有以下几方面没有考虑:
- 上下方向箭实现历史命令回溯
- Tab 箭实现命令补全
- 命令执行的多线程安全问题