命令行解析工具 Command Line Parser - Clips

clips 是一个基于C++11的命令行解析器,可方便的集成到源码工程中。

https://github.com/esdk-net/clips

特点

  • 提供少量的、简单的接口;
  • 方便创建基于子命令和嵌套命令的应用程序;
  • 可在单独的文件中定义命令处理函数和绑定命令,使用 CLIPS_INIT()
  • 仅有头文件;
  • 没有外部依赖;
  • 直接绑定变量(&varname),或者使用'cast<typename>'函数获取flag值;
  • 支持枚举值;
  • 支持简单的自定义类型;
  • 清晰的帮助信息;
  • 友好的错误信息;
  • 支持在WindowsLinuxmacOS平台下使用;

命令行

示例:

1
2
3
4
5
$ ./appname
$ ./appname pull --all
$ ./appname clone -v https::clips.repo.git clips
$ ./appname remote rename old_name new_name
$ ./appname remote set-url --delete dev https::clips.repo.git

帮助

可以查看子命令的详细帮助,参考下面:

1
2
3
4
5
6
7
8
9
10
# 帮助
$ ./appname -h
$ ./appname --help

# 嵌套命令的帮助
$ ./appname [cmds...] -h
$ ./appname [cmds...] --help

# 示例
$ ./appname a b c -h

帮助信息,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ./appname -h

desc

usage:
appname [cmds...] [args...] [--flags...]

cmds:
sub sub brief

flags:
-h, --help <bool> :(false) help
--eee <unsigned int> (1) eee desc
-f, --fff <int> (2) fff desc {0,1,2}

example:
appname sub arg0 arg1 --name=value

for more information about a cmd:
appname [cmds...] -h
appname [cmds...] --help

flags 列定义:

1
2
flags:
-n, --name <type> :extend(default) desc {enums}

注::(false)中的:表示,这个命令是extend的,可能是从上级命令中继承过来的。

工程

目录结构示例:

1
2
3
4
5
6
+ <project_path>
+ src
- main.cpp
- cmds_first.cpp
- cmds_second.cpp
+ <others...>

包含目录路径:

1
clips/include

在代码中包含文件头:

1
#include "clips/clips.hpp"

使用

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "clips/clips.hpp"

int main(int argc, char* argv[])
{
clips::desc("desc");

auto ret = clips::exec(argc, argv);
if (clips::ok != ret)
{
std::cout << ret << std::endl;
return 1;
}
return 0;
}

根函数

根函数实际上是内部一个预置的最顶级的根命令,这个命令不具名。

定义根函数:

1
2
3
4
5
6
uint32_t g_ddd = 0;
clips::error_t root(const clips::pcmd_t& pcmd, const clips::args_t& args)
{
std::cout << "exec root handler. ddd=" << ddd << std::endl;
return clips::ok;
}

添加flag, 和绑定处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

auto ret = clips::flag<uint32_t>("aaa", "a", 2, "aaa desc");
if (ret != clips::ok)
{
return ret;
}

ret = clips::pflag<uint32_t>(&g_ddd, "ddd", "", 5, "ddd desc", true);
if (ret != clips::ok)
{
return ret;
}

ret = clips::bind(root);
if (ret != clips::ok)
{
return ret;
}

执行命令:

1
$ ./appname [args...] [--flags...]

注:根函数不是必须的,可以为空(默认)。

子命令

实际上就是嵌套命令,内部的根函数对应的根命令的嵌套命令。只是因为提供了顶级接口,就和下面的嵌套命令做了区分。

定义和绑定子命令:

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
// 创建
auto sub = clips::make_cmd("sub");
sub->brief("sub 简洁描述");
sub->desc("sub 详细描述");

// 添加 flags
int fff = 0;
sub->pflag<int>(&fff, "fff", "f", 2, { 0, 1, 2 }, "fff desc");
sub->flag<uint32_t>("eee", "", 1u, "eee desc", true);

// 示例
sub->example("sub --eee 0 -f 0");

// 绑定函数
sub->bind([](const clips::pcmd_t& pcmd,
const clips::args_t& args) -> clips::error_t
{
std::cout << "exec sub." << std::endl;
return clips::ok;
}
);

// 绑定命令
auto ret = clips::bind(sub);
if (ret != clips::ok)
{
return ret;
}

执行命令:

1
$ ./appname sub [args...] [--flags...]

嵌套命令

嵌套命令是指命令中包含命令。

当需要在 sub 命令下包含子命令 nested, 则需要如下方法实现:

1
2
3
4
5
6
7
8
9
auto nested = clips::make_cmd("nested");
nested->brief("nested 简洁描述");
nested->desc("nested 详细描述");
// ...
auto ret = sub->bind(nested);
if (ret != clips::ok)
{
return ret;
}

执行命令:

1
$ ./appname sub nested [args...] [--flags...]

在单独文件中定义和绑定命令

当包含子命令和flag很多时,在 main.cpp 实现所有交互逻辑,会显得臃肿不清晰。将各命令在单独的逻辑中实现,既条理清晰,又增加了程序的可测试性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto your_func(const clips::pcmd_t& pcmd, const clips::args_t& args) -> clips::error_t
{
// ...
return clips::ok;
}

CLIPS_INIT()
{
// 这个函数将会在解析命令行参数之前被调用。

auto pcmd = clips::make_cmd("name");
// ...
pcmd->bind(your_func); // 也可以使用lambda函数
return clips::bind(pcmd);
}

CLIPS_INIT()定义的逻辑会在 clips::exec() 的一开始执行。当在多个文件中定义了 CLIPS_INIT() 时,其执行顺序和编译器有关,通常是文件名称的字典序。其中,根函数的绑定并不受该顺序的影响,可在任何位置定义,而其他命令因为是具名的,所以会受先后绑定的影响,后绑定的同名命令会导致函数返回重复定义的错误信息。

应用信息

应用名称

如果指定了应用名称,则在clips::exec()中解析时不会被重写;如果不指定应用名称,则在clips::exec()中解析时会使用argv[0]代替;

1
2
clips::name("name");
auto name = clips::name();

应用描述

1
2
clips::desc("desc");
auto desc = clips::desc();

原始命令参数

1
auto& argv = clips::argv();

Flag

flag一般只能通过命令接口添加。

添加

1
2
3
4
5
6
7
8
9
// 需要用 cast() 转换
auto err = pcmd->flag<int>("name", "n", 0, "desc");
auto value = flags["--name"]->cast<int>();
// 或
auto value = pcmd->cast<int>("--name");

// 绑定变量
int varname = 0; // 也可用 cast() 转换
auto err = pcmd->pflag<int>(&varname, "name", "n", 0, "desc");

名称

flag 的名称为 "name" 时,对应的命令行是 --name。名称不能为空。

快捷名称

flag 的快捷名称为 "n" 时,对应的命令行是 -n。快捷名称可以为空或一个字符。

类型名称

1
auto type_name = flags["--name"]->type_name();

继承

当设置 flag 可继承时,其分支之后的嵌套命令都可以访问。

flagpflag 函数的最后一个参数为 true 时,表示可继承,默认是不继承(不给出对应参数时),如下:

1
auto err = pcmd->flag<int>("name", "n", 0, "desc", true);

内部解析flag的时候,其查找顺序是先在当前命令中查找这个flag,找不到时,再从根命令自顶向下查找,排除非extendflag,直到找到或到当前命令为止。

可选项(枚举值)

当提供可选项(枚举值)时,会检查输入参数是否为枚举值之一。如果不合法,则clips::exec()会返回错误。

1
2
auto err = pcmd->flag<int>("name", "n", 0, {0, 1, 2}, "desc");
auto err = pcmd->pflag<int>(&varname, "name", "n", 0, {0, 1, 2}, "desc");

转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 是否可以转换为目标类型
if (flags["--name"]->castable<std::string>())
{
// 可以转换
}

// 转换为目标类型值
auto value = flags["--name"]->cast<std::string>();
// 或
auto value = pcmd->cast<std::string>("--name");

// 不能转换为目标类型时,会抛出异常
try
{
auto value = flags["--name"]->cast<std::string>();
// 或
auto value = pcmd->cast<std::string>("--abc");
}
catch (std::exception& e)
{
std::cout << e.what() << std::endl;
}

使用 error_t:

1
2
3
4
5
6
error_t err;
auto value = pcmd->cast<std::string>("--abc", &err);
if (err != clips::ok)
{
return err;
}

堆栈信息

您可能需要堆栈信息(也可从当前命令中获取),以在出错时帮助定位。

1
auto stack = flags["--name"]->stack();

默认值字符串

可查看默认值对应的字符串。

1
auto stack = flags["--name"]->default_value();

输入的字符串值

clips::exec()中的执行解析逻辑后,才可以得到用户输入的字符串值,否则为空。

1
auto text = flags["--name"]->text();

输入形式

1
2
3
4
5
6
7
8
9
10
11
--name         # bool 型
-n # bool 型
--name true # bool 型
-n false # bool 型
--name 1 # bool 型
-n 0 # bool 型
--name value # 空格区隔名称和值
-n value # 空格区隔名称和值
--name=value # 等号区隔名称和值
-n=value # 等号区隔名称和值
-n '-1' # 单引号防止命令行解析规则导致的转义

自定义类型

需要实现流处理操作符 <<>> 的重载.

定义:

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
class custom_t
{
public:
custom_t()
{
}

~custom_t()
{
}

custom_t(const custom_t& cpy)
: num_(cpy.num_)
, msg_(cpy.msg_)
{
}

custom_t(custom_t&& mv) noexcept
: num_(mv.num_)
, msg_(std::move(mv.msg_))
{
}

custom_t& operator=(const custom_t& rhs)
{
num_ = rhs.num_;
msg_ = rhs.msg_;
return *this;
}

int num_{ 0 };
std::string msg_;
};

std::ostream& operator<<(std::ostream& os, const custom_t& obj)
{
os << "{" << obj.num_ << "," << obj.msg_ << "}";
return os;
}

std::istream& operator>>(std::istream& is, custom_t& obj)
{
unsigned char c;
std::string tmp;
is >> obj.num_ >> c >> obj.msg_;
return is;
}

Flag:

1
err = pcmd->flag<custom_t>("custom", "", {}, "custom_t type");

转换:

1
std::cout << pcmd->cast<custom_t>("--custom") << std::endl;

执行:

1
$ ./appname sub --custom=1,msg

命令

创建命令

建议的三种创建方式:

1
2
3
4
5
// 不带参数
auto pcmd = clips::make_cmd();

// 给定名称
auto pcmd = clips::make_cmd("name");

基本信息

1
2
3
4
5
6
7
8
9
10
11
/// name 名称
pcmd->name("name");
auto name = pcmd->name();

/// brief 简短描述
pcmd->brief("brief");
auto brief = pcmd->brief();

/// desc 完整描述
pcmd->desc("desc");
auto desc = pcmd->desc();

提供示例

示例只是一段文本,通常需要写如何执行这个命令。

1
2
pcmd->example("example");
auto example = pcmd->example();

添加flag

1
2
3
4
5
6
7
8
// 需要用 cast() 转换
auto err = pcmd->flag<int>("name", "n", 0, "desc");
auto value = flags["--name"]->cast<int>();
auto value = pcmd->cast<int>("--name");

// 绑定变量
int varname = 0; // 也可用 cast() 转换
auto err = pcmd->pflag<int>(&varname, "name", "n", 0, "desc");

注意变量的生命周期。

绑定函数

1
2
3
4
5
6
auto your_func(const clips::pcmd_t& cmd, const clips::args_t& args) -> clips::error_t
{
// ...
return clips::ok;
}
auto err = pcmd->bind(your_func); // 或者使用lambda函数

注:命令函数不是必须的,可以为空(默认)。

嵌套命令

1
2
3
auto psub = clips::make_cmd("sub");
// ...
auto err = pcmd->bind(psub);

命令参数

定义如下:

1
$ ./appname sub nested [args...] [--flags...]

示例如下:

1
2
3
4
$ ./appname sub adfa # ok, adfa 为参数 args[0]
$ ./appname sub # ok, args 大小为 0
$ ./appname sub nested adfa # ok, args[0] 为 adfa
$ ./appname sub nested # ok, args 大小为 0

错误信息

创建

1
2
3
auto err = clips::make_error();
auto err = clips::make_error("msg");
auto err = clips::make_error("msg", "stack");

接口

错误信息:

1
2
err.msg("parse failed.");
auto msg = err.msg();

堆栈信息:

1
2
err.stack("appname sub");
auto stack = err.stack();

流式打印:

1
std::cout << err << std::endl;

比较

只关注错误信息是否相同,不关心堆栈信息。

  • err == clips::ok 成功;
  • err != clips::ok 失败;

不支持

  • 非标准flag, 如 -long--h

TODO

  • 帮助信息 help
    • 文本定制
    • 多语言国际化 i18n --lang {default, en-us, zh-cn}
  • 类型安全 type safety
    • 更多类型检查 type checking
  • 兼容 POSIX flags
    • 组合flag 支持, 如 -abc
  • 智能推荐

测试

使用 Catch2 框架对 clips 进行单元测试, 推荐前往下面的网址,了解这个优秀的单元测试框架:

https://github.com/catchorg/Catch2