背景
源码版本ClickHouse 21.8。在命令行输入配置项不全也可以匹配到正确的配置项(模糊匹配)。对此进行代码分析。
源码阅读
在programs/main.cpp中main函数为主入口,通过程序名称判断进入哪个子程序入口。这里执行程序名为-client所以进入programs/client/Client.cpp的mainEntryClickHouseClient函数。
int main(int argc_, char ** argv_)
{
inside_main = true;
SCOPE_EXIT({ inside_main = false; });
/// ... 不重要的代码
MainFunc main_func = printHelp;
for (auto & application : clickhouse_applications)
{
/// 根据程序名称获取对应子程序入口,程序名称-client则进入Client.cpp的功能入口
if (isClickhouseApp(application.first, argv))
{
main_func = application.second;
break;
}
}
return main_func(static_cast<int>(argv.size()), argv.data());
}
programs/client/Client.cpp的mainEntryClickHouseClient函数中,对于命令行参数的读取部分在init函数。
int mainEntryClickHouseClient(int argc, char ** argv)
{
try
{
DB::Client client;
/// 在init函数中处理输入参数
client.init(argc, argv);
return client.run();
}
catch (const DB::Exception & e)
{
/// 不重要的代码
}
}
void init(int argc, char ** argv)
{
/// ...
/// 读取配置描述
po::options_description main_description = createOptionsDescription("Main options", terminal_width);
main_description.add_options()
("help", "produce help message")
/// ...
;
Settings cmd_settings;
cmd_settings.addProgramOptions(main_description);
/// ...
/// 解析命令行输入的配置项
po::parsed_options parsed = po::command_line_parser(common_arguments).options(main_description).run();
auto unrecognized_options = po::collect_unrecognized(parsed.options, po::collect_unrecognized_mode::include_positional);
// 配置项未识别则抛异常
if (unrecognized_options.size() > 1)
{
throw Exception("Unrecognized option '" + unrecognized_options[1] + "'", ErrorCodes::UNRECOGNIZED_ARGUMENTS);
}
po::variables_map options;
po::store(parsed, options);
po::notify(options);
/// ...
}
在contrib/boost/boost/program_options/detail/parsers.hpp的run函数中执行解析
template<class charT>
basic_parsed_options<charT>
basic_command_line_parser<charT>::run()
{
parsed_options result(m_desc, detail::cmdline::get_canonical_option_prefix());
/// 执行具体解析方法
result.options = detail::cmdline::run();
return basic_parsed_options<charT>(result);
}
在contrib/boost/libs/program_options/src/cmdline.cpp的run函数中执行具体的解析
vector<option>
cmdline::run()
{
// 添加解析方法
if (m_style_parser)
style_parsers.push_back(m_style_parser);
/// ...
style_parsers.push_back(boost::bind(&cmdline::parse_terminator, this, _1));
vector<option> result;
vector<string>& args = m_args;
/// 逐个参数解析
while(!args.empty())
{
bool ok = false;
/// 逐个解析函数尝试解析单个参数
for(unsigned i = 0; i < style_parsers.size(); ++i)
{
unsigned current_size = static_cast<unsigned>(args.size());
vector<option> next = style_parsers[i](args);
// 解析到内容,比如输入的是--xxx则解析到next的string_key为xxx
if (!next.empty())
{
vector<string> e;
for(unsigned k = 0; k < next.size()-1; ++k) {
/// 匹配参数描述
finish_option(next[k], e, style_parsers);
}
finish_option(next.back(), args, style_parsers);
for (unsigned j = 0; j < next.size(); ++j)
result.push_back(next[j]);
}
if (args.size() != current_size) {
ok = true;
break;
}
}
/// 如果为解析成功则,将该参数保存后在参数列表中擦除,以便解析下一个参数
if (!ok) {
option opt;
/// ...
args.erase(args.begin());
}
}
/// ...
return result;
}
void
cmdline::finish_option(option& opt,
vector<string>& other_tokens,
const vector<style_parser>& style_parsers)
{
if (opt.string_key.empty())
return;
/// ...
try
{
// 在描述中匹配配置项
const option_description* xd = m_desc->find_nothrow(opt.string_key,
is_style_active(allow_guessing),
is_style_active(long_case_insensitive),
is_style_active(short_case_insensitive));
if (!xd)
{
/// ... 匹配失败,抛异常
}
const option_description& d = *xd;
// 重命名,因为可能有些是模糊匹配,配置名称需要重命名为全名,比如user可以匹配到username,则将配置项user重命名为username。
opt.string_key = d.key(opt.string_key);
// ...
}
在contrib/boost/libs/program_options/src/options_description.cpp的find_nothrow函数中匹配描述项
const option_description*
options_description::find_nothrow(const std::string& name,
bool approx,
bool long_ignore_case,
bool short_ignore_case) const
{
/// ...
for(unsigned i = 0; i < m_options.size(); ++i)
{
/// 开始匹配配置项
option_description::match_result r =
m_options[i]->match(name, approx, long_ignore_case, short_ignore_case);
/// 不匹配直接下一个
if (r == option_description::no_match)
continue;
/// 全匹配
if (r == option_description::full_match)
{
full_matches.push_back(m_options[i]->key(name));
found = m_options[i];
had_full_match = true;
}
else
{
// 模糊匹配
approximate_matches.push_back(m_options[i]->key(name));
if (!had_full_match)
found = m_options[i];
}
}
/// 多个全匹配,则抛异常
if (full_matches.size() > 1)
boost::throw_exception(ambiguous_option(full_matches));
/// 多个匹配,程序无法知道到底是匹配哪个配置项也抛异常
if (full_matches.empty() && approximate_matches.size() > 1)
boost::throw_exception(ambiguous_option(approximate_matches));
return found.get();
}
/// 三种匹配结果:不匹配,全匹配,模糊匹配(配置项的全字符与描述字段的前一截字符匹配,或者有通配符*)
option_description::match_result
option_description::match(const std::string& option,
bool approx,
bool long_ignore_case,
bool short_ignore_case) const
{
match_result result = no_match;
std::string local_option = (long_ignore_case ? tolower_(option) : option);
for(std::vector<std::string>::const_iterator it(m_long_names.begin()); it != m_long_names.end(); it++)
{
std::string local_long_name((long_ignore_case ? tolower_(*it) : *it));
if (!local_long_name.empty()) {
if ((result == no_match) && (*local_long_name.rbegin() == '*'))
{
// The name ends with '*'. Any specified name with the given
// prefix is OK.
if (local_option.find(local_long_name.substr(0, local_long_name.length()-1))
== 0)
result = approximate_match;
}
if (local_long_name == local_option)
{
result = full_match;
break;
}
else if (approx)
{
if (local_long_name.find(local_option) == 0)
{
result = approximate_match;
}
}
}
}
if (result != full_match)
{
std::string local_short_name(short_ignore_case ? tolower_(m_short_name) : m_short_name);
if (local_short_name == local_option)
{
result = full_match;
}
}
return result;
}