路径位于:src/Util/CMD.cpp
类图
用户使用时,继承CMD,实现自己的命令类,通过CMD成员对象parser(OptionParser类)定义该命令支持的所有选项。CMD本身继承自map,每次解析后,CMD中存储当前用户输入的选项信息以及该命令具有默认值的选项信息。
实际的选项信息,存储在Option中,一个选项对应一个Option实例。OptionParser中存储了当前命令所支持的所有选项,std::map< Option >。
如果需要支持多条命令,如test_shell.cpp,则通过CMDRegister类来管理多个CMD实例。
模块
Option
选项类,存储一条选项相关信息。包括短选项名、长选项名、选项额外参数类型(无参、有参、可选参)、选项额外参数默认值、该选项是否必须给额外参数赋值、选项描述信息、选项对应回调。
使用示例
Option(
'l',
"listen",
Option::ArgRequired,
"10000",
false,
"服务器模式:监听端口",
nullptr);
Option(
'c',
"count",
Option::ArgRequired,
to_string(10).data(),
false,
"客户端模式:测试客户端个数",
nullptr);
Option(
's',
"server",
Option::ArgRequired,
"127.0.0.1:10000",
false,
"客户端模式:测试服务器地址",
[](const std::shared_ptr<ostream> &stream, const string &arg) {
if (arg.find(":") == string::npos) {
//中断后续选项的解析以及解析完毕回调等操作
throw std::runtime_error("\t地址必须指明端口号.");
}
//如果返回false则忽略后续选项的解析
return true;
})
原理
主要包括一个构造函数和一个重载的()回调函数
- 构造函数:
Option(char short_opt, const char *long_opt, enum ArgType type, const char *default_value, bool must_exist,
const char *des, const OptionHandler &cb) {
_short_opt = short_opt;
_long_opt = long_opt;
_type = type;
if (type != ArgNone) { //当type为ArgRequired、ArgOptional时
if (default_value) {
_default_value = std::make_shared<std::string>(default_value); //设置默认值
}
if (!_default_value && must_exist) {
_must_exist = true;
}
}
_des = des;
_cb = cb;
}
- 重载_cb
bool operator()(const std::shared_ptr<std::ostream> &stream, const std::string &arg) {
return _cb ? _cb(stream, arg) : true;
}
相关成员变量:
private:
friend class OptionParser;
bool _must_exist = false; //是否必须
char _short_opt; //比如: h、l
enum ArgType _type; // 比如: Option::ArgRequired
std::string _des; //描述,比如: "客户端模式:测试服务器地址",
std::string _long_opt; // 比如 "listen"、“help”
OptionHandler _cb;
std::shared_ptr<std::string> _default_value;
OptionParser
选项解析器。
使用示例
std::shared_ptr<OptionParser> _parser = std::make_shared<OptionParser>();
_parser.reset(new OptionParser(nullptr));
_parser = std::make_shared<OptionParser>([](const std::shared_ptr<std::ostream> &stream, mINI &) {
CMDRegister::Instance().printHelp(stream);
});
(*_parser) << Option(
'l',
"listen",
Option::ArgRequired,
"10000",
false,
"服务器模式:监听端口",
nullptr);
可以看出:有添加选项、删除选项、解析选项三个函数可以调用
原理
//构造函数,设置选项解析完成后执行的回调,以及是否允许非用户输入且没有参数的选项被解析到CMD中,具体使用参见operator ()
OptionParser(const OptionCompleted &cb = nullptr,bool enableEmptyArgs = true);
//添加选项
OptionParser &operator <<(Option &&option);
OptionParser &operator <<(const Option &option);
//删除选项
void delOption(const char *key);
//选项解析
void operator ()(mINI &allArg, int argc, char *argv[],const std::shared_ptr<ostream> &stream);
添加&&删除选项
1、关于添加选项,当(*_parser) << Option(....)
时将会调用。跟成员变量_map_options&&_map_char_index有关
public:
std::map<char, int> _map_char_index;
std::map<int, Option> _map_options;
- _map_char_index的key是短选项,value是根据一定规则生成的int类型数据
- _map_options的value是整个Option,key是根据一定规则生成的int类型数据,和_map_char_index的value一致
operator<<的功能就是将参数option压入_map_char_index和_map_options,没有其他。其实现如下:
OptionParser &operator<<(Option &&option) {
int index = 0xFF + (int) _map_options.size();
if (option._short_opt) {
_map_char_index.emplace(option._short_opt, index);
}
_map_options.emplace(index, std::forward<Option>(option));
return *this;
}
OptionParser &operator<<(const Option &option) {
int index = 0xFF + (int) _map_options.size();
if (option._short_opt) {
_map_char_index.emplace(option._short_opt, index);
}
_map_options.emplace(index, option);
return *this;
}
2、关于删除选项,当 _parser->delOption("listen");
时将会调用。根据**长选项_long_opt
**从_map_char_index、_map_options删除对应option
void delOption(const char *key) {
for (auto &pr : _map_options) {
if (pr.second._long_opt == key) {
if (pr.second._short_opt) {
_map_char_index.erase(pr.second._short_opt);
}
_map_options.erase(pr.first);
break;
}
}
}
解析选项
该类最关键的解析选项,相关函数是operator()操作
先学习下成员变量
1、成员变量 _on_completed
using OptionCompleted = std::function<void(const std::shared_ptr<std::ostream> &, mINI &)>;
private:
OptionCompleted _on_completed;
可以看出_on_completed是一个回调函数。它是在构造函数中设置的,默认设置为空
OptionParser(const OptionCompleted &cb = nullptr, bool enable_empty_args = true) {
_on_completed = cb;
......
当选项被解析完成时,将会调用:
void OptionParser::operator()(mINI &all_args, int argc, char *argv[], const std::shared_ptr<ostream> &stream) {
.....
if (_on_completed) {
_on_completed(stream, all_args);
}
}
operator()具体实现如下:
void OptionParser::operator()(mINI &all_args, int argc, char *argv[], const std::shared_ptr<ostream> &stream) {
/*根据支持的选项,生成getopt_long的参数*/
vector<struct option> vec_long_opt;
string str_short_opt;
do {
struct option tmp;
for (auto &pr : _map_options) {
auto &opt = pr.second;
//long opt
tmp.name = (char *) opt._long_opt.data();
tmp.has_arg = opt._type;
tmp.flag = nullptr;
tmp.val = pr.first;
vec_long_opt.emplace_back(tmp);
//short opt
if (!opt._short_opt) {
continue;
}
str_short_opt.push_back(opt._short_opt);
switch (opt._type) {
case Option::ArgRequired: str_short_opt.append(":"); break;
case Option::ArgOptional: str_short_opt.append("::"); break;
default: break;
}
}
tmp.flag = 0;
tmp.name = 0;
tmp.has_arg = 0;
tmp.val = 0;
vec_long_opt.emplace_back(tmp);
} while (0);
static mutex s_mtx_opt;
lock_guard<mutex> lck(s_mtx_opt);
/*查找用户输入的选项是否支持,如果支持,加入到allArg中,并调用选项的()操作*/
int index;
optind = 0;
opterr = 0;
while ((index = getopt_long(argc, argv, &str_short_opt[0], &vec_long_opt[0], nullptr)) != -1) {
stringstream ss;
ss << " 未识别的选项,输入\"-h\"获取帮助.";
if (index < 0xFF) {
//短参数
auto it = _map_char_index.find(index);
if (it == _map_char_index.end()) {
throw std::invalid_argument(ss.str());
}
index = it->second;
}
auto it = _map_options.find(index);
if (it == _map_options.end()) {
throw std::invalid_argument(ss.str());
}
auto &opt = it->second;
auto pr = all_args.emplace(opt._long_opt, optarg ? optarg : "");
if (!opt(stream, pr.first->second)) {
return;
}
optarg = nullptr;
}
/*将有默认值且非用户指定的选项,加入到allArg中,因为程序中后续可能要使用默认值
此时,allArg中包括了用户指定的选项以及具备默认值但非用户指定的选项*/
for (auto &pr : _map_options) {
if (pr.second._default_value && all_args.find(pr.second._long_opt) == all_args.end()) {
//有默认值,赋值默认值
all_args.emplace(pr.second._long_opt, *pr.second._default_value);
}
}
/*如果选项的值是必填的,但该选项不在allArg中,说明出错了。
也就是,规定该选项后边必须有值,但是用户没有指定该选项,且该选项也没有默认值,那么这个选项肯定无法执行了 */
for (auto &pr : _map_options) {
if (pr.second._must_exist) {
if (all_args.find(pr.second._long_opt) == all_args.end()) {
stringstream ss;
ss << " 参数\"" << pr.second._long_opt << "\"必须提供,输入\"-h\"选项获取帮助";
throw std::invalid_argument(ss.str());
}
}
}
/*没有解析到用户输入的选项,也没有解析到具有默认值的选项。allArg为空*/
if (all_args.empty() && _map_options.size() > 1 && !_enable_empty_args) {
_helper(stream, "");
return;
}
/*执行选项解析完成后的回调*/
if (_on_completed) {
_on_completed(stream, all_args);
}
}
CMD
使用示例
#include <iostream>
#include "Util/CMD.h"
#include "Util/logger.h"
using namespace std;
using namespace toolkit;
class CMD_main : public CMD {
public:
CMD_main() {
_parser.reset(new OptionParser(nullptr));
(*_parser) << Option('d',/*该选项简称,如果是\x00则说明无简称*/
"daemon",/*该选项全称,每个选项必须有全称;不得为null或空字符串*/
Option::ArgNone,/*该选项后面必须跟值*/
nullptr,/*该选项默认值*/
false,/*该选项是否必须赋值,如果没有默认值且为ArgRequired时用户必须提供该参数否则将抛异常*/
"是否以Daemon方式启动",/*该选项说明文字*/
nullptr);
(*_parser) << Option('l',/*该选项简称,如果是\x00则说明无简称*/
"level",/*该选项全称,每个选项必须有全称;不得为null或空字符串*/
Option::ArgRequired,/*该选项后面必须跟值*/
to_string(LTrace).data(),/*该选项默认值*/
false,/*该选项是否必须赋值,如果没有默认值且为ArgRequired时用户必须提供该参数否则将抛异常*/
"日志等级,LTrace~LError(0~4)",/*该选项说明文字*/
nullptr);
}
~CMD_main() override{}
const char *description() const override{
return "主程序命令参数";
}
};
int main(int argc,char *argv[]){
CMD_main cmd_main;
try {
cmd_main.operator()(argc, argv);
} catch (ExitException &) {
return 0;
} catch (std::exception &ex) {
cout << ex.what() << endl;
return -1;
}
bool bDaemon = cmd_main.hasKey("daemon");
LogLevel logLevel = (LogLevel) cmd_main["level"].as<int>();
string ssl_file = cmd_main["ssl"];
int threads = cmd_main["threads"];
return 0;
}
原理
命令类,继承自map,每次调用operator()操作后,CMD本身存储的是用户当前输入的选项以及非用户输入但具有默认值的选项,而该命令支持的所有选项,存储在_parser中(OptionParser)的option Map中。
//选项解析,每次解析会先把CMD中存储的上一次的解析结果清空
void operator ()(int argc, char *argv[],const std::shared_ptr<ostream> &stream = nullptr);
//选项是否存在(包括解析到用户输入的以及非用户输入但是有默认值的选项)
bool hasKey(const char *key);
/* 分割选项值 */
vector<variant> splitedVal(const char *key,const char *delim= ":");
//删除选项
void delOption(const char *key);
CMDRegister
命令管理单例类。
//删除所有命令
void clear();
//注册命令
void registCMD(const char *name,const std::shared_ptr<CMD> &cmd);
/*删除命令*/
void unregistCMD(const char *name);
/*通过命令名称获取命令*/
std::shared_ptr<CMD> operator[](const char *name);
//通过命令名称解析选项
void operator()(const char *name,int argc,char *argv[],const std::shared_ptr<ostream> &stream = nullptr);
//打印当前所有命令信息
void printHelp(const std::shared_ptr<ostream> &streamTmp = nullptr);
/*通过用户输入信息解析命令*/
void operator()(const string &line,const std::shared_ptr<ostream> &stream = nullptr);
使用示例
原理
1、首先这是一个单例类(注意,源码没有将将构造函数私有化,所以还是可以在栈上和堆上构造类对象的)
class CMDRegister {
public:
static CMDRegister &Instance();
}
CMDRegister &CMDRegister::Instance() {
static CMDRegister instance;
return instance;
}
2、它使用成员变量_cmd_map管理所有注册的CMD类,用_mtx实现线程安全
private:
std::recursive_mutex _mtx;
std::map<std::string, std::shared_ptr<CMD> > _cmd_map;
3、通过成员函数registCMD和unregistCMD来管理所有注册的CMD对象,功能是将参数线程安全将CMD对象加入_cmd_map或者从_cmd_map中删除,
void registCMD(const char *name, const std::shared_ptr<CMD> &cmd) {
std::lock_guard<std::recursive_mutex> lck(_mtx);
_cmd_map.emplace(name, cmd);
}
void unregistCMD(const char *name) {
std::lock_guard<std::recursive_mutex> lck(_mtx);
_cmd_map.erase(name);
}
还可以使用CMDRegister::Instance().operator[]("name")
查询注册的对象,注意,如果查询一个没有存在过的对象,将会抛出异常
std::shared_ptr<CMD> operator[](const char *name) {
std::lock_guard<std::recursive_mutex> lck(_mtx);
auto it = _cmd_map.find(name);
if (it == _cmd_map.end()) {
throw std::invalid_argument(std::string("命令不存在:") + name);
}
return it->second;
}
CMD的一些派生类
CMD_help
//帮助命令(help),该命令默认已注册
class CMD_help : public CMD {
public:
CMD_help() {
_parser = std::make_shared<OptionParser>([](const std::shared_ptr<std::ostream> &stream, mINI &) {
CMDRegister::Instance().printHelp(stream);
});
}
const char *description() const override {
return "打印帮助信息";
}
};