本节介绍如何在特定情况下使用该库。
有时,标准的命令行语法是不够的。 例如,gcc 编译器具有 “-frtti” 和 “-fno-rtti” 选项,而此语法不被直接支持。
对于这种情况,该库允许用户提供一个附加解析器——一个函数,它将在库进行任何处理之前,在每个命令行元素上被调用。 如果附加解析器识别出该语法,它将返回选项名称和值,这些名称和值将被直接使用。 上面的例子可以通过以下代码处理
pair<string, string> reg_foo(const string& s) { if (s.find("-f") == 0) { if (s.substr(2, 3) == "no-") return make_pair(s.substr(5), string("false")); else return make_pair(s.substr(2), string("true")); } else { return make_pair(string(), string()); } }
这是附加解析器的定义。 当解析命令行时,我们传递附加解析器
store(command_line_parser(ac, av).options(desc).extra_parser(reg_foo) .run(), vm);
完整的示例可以在 “example/custom_syntax.cpp” 文件中找到。
某些操作系统对命令行长度有非常低的限制。 解决这些限制的常用方法是使用响应文件。 响应文件只是一个配置文件,它使用与命令行相同的语法。 如果命令行指定要使用的响应文件的名称,则除了命令行之外,还会加载和解析该文件。 该库不直接支持响应文件,因此您需要编写一些额外的代码。
首先,您需要为响应文件定义一个选项
("response-file", value<string>(), "can be specified with '@name', too")
其次,您需要一个附加解析器来支持指定响应文件的标准语法:“@file”
pair<string, string> at_option_parser(string const&s) { if ('@' == s[0]) return std::make_pair(string("response-file"), s.substr(1)); else return pair<string, string>(); }
最后,当找到 “response-file” 选项时,您必须加载该文件并将其传递给命令行解析器。 这部分是最难的。 我们将使用 Boost.Tokenizer 库,它虽然可以工作,但有一些限制。 您也可以考虑 Boost.StringAlgo。 代码如下
if (vm.count("response-file")) { // Load the file and tokenize it ifstream ifs(vm["response-file"].as<string>().c_str()); if (!ifs) { cout << "Could not open the response file\n"; return 1; } // Read the whole file into a string stringstream ss; ss << ifs.rdbuf(); // Split the file content char_separator<char> sep(" \n\r"); std::string ResponsefileContents( ss.str() ); tokenizer<char_separator<char> > tok(ResponsefileContents, sep); vector<string> args; copy(tok.begin(), tok.end(), back_inserter(args)); // Parse the file and store the options store(command_line_parser(args).options(desc).run(), vm); }
完整的示例可以在 “example/response_file.cpp” 文件中找到。
在 Windows 操作系统上,GUI 应用程序接收的命令行是单个字符串,而不是拆分为元素。 因此,命令行解析器不能直接使用。 至少在某些编译器上,可以获得拆分的命令行,但尚不清楚所有编译器是否在所有版本的操作系统上都支持相同的机制。 split_winmain
函数是该库提供的可移植机制。
这是一个使用示例
vector<string> args = split_winmain(lpCmdLine); store(command_line_parser(args).options(desc).run(), vm);
split_winmain
函数为 wchar_t
字符串重载,因此也可以在 Unicode 应用程序中使用。
使用 options_description
类的单个实例来包含程序的所有选项可能会有问题
某些选项仅对特定来源有意义,例如,配置文件。
用户可能希望在生成的帮助消息中看到一些结构。
某些选项根本不应出现在生成的帮助消息中。
为了解决上述问题,该库允许程序员创建 options_description
类的多个实例,这些实例可以以不同的组合方式合并。 以下示例将定义三组选项:命令行特定的选项,以及两个特定程序模块的选项组,其中只有一个选项组会显示在生成的帮助消息中。
每个组都使用标准语法定义。 但是,您应该为每个 options_description
实例使用合理的名称
options_description general("General options"); general.add_options() ("help", "produce a help message") ("help-module", value<string>(), "produce a help for a given module") ("version", "output the version number") ; options_description gui("GUI options"); gui.add_options() ("display", value<string>(), "display to use") ; options_description backend("Backend options"); backend.add_options() ("num-threads", value<int>(), "the initial number of threads") ;
在声明选项组之后,我们将它们合并为两种组合。 第一个将包含所有选项,并用于解析。 第二个将用于 “--help” 选项。
// Declare an options description instance which will include // all the options options_description all("Allowed options"); all.add(general).add(gui).add(backend); // Declare an options description instance which will be shown // to the user options_description visible("Allowed options"); visible.add(general).add(gui);
剩下的是解析和处理选项
variables_map vm; store(parse_command_line(ac, av, all), vm); if (vm.count("help")) { cout << visible; return 0; } if (vm.count("help-module")) { const string& s = vm["help-module"].as<string>(); if (s == "gui") { cout << gui; } else if (s == "backend") { cout << backend; } else { cout << "Unknown module '" << s << "' in the --help-module option\n"; return 1; } return 0; } if (vm.count("num-threads")) { cout << "The 'num-threads' options was set to " << vm["num-threads"].as<int>() << "\n"; }
当解析命令行时,允许所有选项。 但是,“--help” 消息不包含 “Backend options” 组——该组中的选项是隐藏的。 用户可以通过传递 “--help-module backend” 选项来显式强制显示该选项组。 完整的示例可以在 “example/option_groups.cpp” 文件中找到。
默认情况下,选项值从字符串到 C++ 类型的转换是使用 iostreams 完成的,这有时不太方便。 该库允许用户自定义特定类的转换。 为了做到这一点,用户应该提供 validate
函数的合适重载。
让我们首先定义一个简单的类
struct magic_number { public: magic_number(int n) : n(n) {} int n; };
然后重载 validate
函数
void validate(boost::any& v, const std::vector<std::string>& values, magic_number* target_type, int) { static regex r("\\d\\d\\d-(\\d\\d\\d)"); using namespace boost::program_options; // Make sure no previous assignment to 'a' was made. validators::check_first_occurrence(v); // Extract the first string from 'values'. If there is more than // one string, it's an error, and exception will be thrown. const string& s = validators::get_single_string(values); // Do regex match and convert the interesting part to // int. smatch match; if (regex_match(s, match, r)) { v = any(magic_number(lexical_cast<int>(match[1]))); } else { throw validation_error(validation_error::invalid_option_value); } }
该函数接受四个参数。 第一个是值的存储位置,在本例中,它要么为空,要么包含 magic_number
类的实例。 第二个是在选项的下一次出现中找到的字符串列表。 剩余的两个参数是解决某些编译器上缺少部分模板特化和部分函数模板排序的问题所需要的。
该函数首先检查我们是否没有尝试为同一个选项赋值两次。 然后它检查是否只传入了单个字符串。 接下来,借助 Boost.Regex 库验证该字符串。 如果该测试通过,则解析后的值将存储到 v
变量中。
完整的示例可以在 “example/regex.cpp” 文件中找到。
要将该库与 Unicode 一起使用,您需要
对 Unicode 输入使用 Unicode 感知的解析器
对需要它的选项要求 Unicode 支持
大多数解析器都有 Unicode 版本。 例如,parse_command_line
函数有一个重载,它接受 wchar_t
字符串,而不是普通的 char
。
即使某些解析器是 Unicode 感知的,但这并不意味着您需要更改所有选项的定义。 实际上,对于许多选项(如整数选项),这样做是没有意义的。 要使用 Unicode,您需要一些 Unicode 感知的选项。 它们与普通选项的不同之处在于,它们接受 wstring
输入,并使用宽字符流对其进行处理。 创建 Unicode 感知的选项很容易:只需使用 wvalue
函数而不是常规的 value
函数。
当 ascii 解析器将数据传递给 ascii 选项,或者 Unicode 解析器将数据传递给 Unicode 选项时,数据根本不会更改。 因此,ascii 选项将看到本地 8 位编码的字符串,而 Unicode 选项将看到作为 Unicode 输入传递的任何字符串。
当 Unicode 数据传递给 ascii 选项,反之亦然时会发生什么? 该库会自动执行从 Unicode 到本地 8 位编码的转换。 例如,如果命令行是 ascii,但您使用 wstring
选项,则 ascii 输入将转换为 Unicode。
为了执行转换,该库使用来自全局区域设置的 codecvt<wchar_t, char>
区域设置 facet。 如果您想使用使用本地 8 位编码(而不是 7 位 ascii 子集)的字符串,您的应用程序应该以
locale::global(locale(""));
开头,这将根据用户选择的区域设置设置转换 facet。
明智的做法是检查您的实现上 C++ 区域设置支持的状态。 快速测试包括三个步骤
转到 “test” 目录并构建 “test_convert” 二进制文件。
在环境中设置一些非 ascii 区域设置。 在 Linux 上,可以运行,例如
$ export LC_CTYPE=ru_RU.KOI8-R
使用所选编码中的任何非 ascii 字符串作为其参数运行 “test_convert” 二进制文件。 如果您看到 Unicode 代码点列表,则一切正常。 否则,您系统上的区域设置支持可能已损坏。
通常,该库会在遇到未知选项名称时抛出异常。 此行为可以更改。 例如,您的应用程序只有一部分使用 Program_options,并且您希望将无法识别的选项传递给程序的另一部分,甚至传递给另一个应用程序。
要允许命令行上存在未注册的选项,您需要使用 basic_command_line_parser
类进行解析(而不是 parse_command_line
),并调用该类的 allow_unregistered
方法
parsed_options parsed = command_line_parser(argc, argv).options(desc).allow_unregistered().run();
对于每个看起来像选项但没有已知名称的标记,basic_option
的实例将被添加到结果中。 该实例的 string_key
和 value
字段将包含标记的语法解析结果,unregistered
字段将设置为 true
,并且 original_tokens
字段将包含标记在命令行中出现时的样子。
如果您想进一步传递无法识别的选项,可以使用 collect_unrecognized
函数。 该函数将收集所有无法识别的值的原始标记,以及可选地,所有找到的位置选项。 例如,如果您的代码处理一些选项,但不处理任何位置选项,您可以像这样使用该函数
vector<string> to_pass_further = collect_unrecognized(parsed.options, include_positional);
到目前为止,我们已经使用 variables_map
类上的 count
方法测试选项是否已设置; 由于您正在重复选项的(字符串字面量)名称,这很容易出现拼写错误和/或因在一个地方重命名选项但在另一处没有重命名而导致的错误
po::options_description desc("Allowed options"); desc.add_options() ("compression", po::value<int>(), "set compression level") ; po::variables_map vm; po::store(po::parse_command_line(ac, av, desc), vm); po::notify(vm); if (vm.count("compression")) { cout << "Compression level was set to " << vm["compression"].as<int>() << ".\n"; } else { cout << "Compression level was not set.\n"; }
相反,您可以使用 boost::optional
类型的变量; Program_options 为 Boost.Optional 提供了特殊支持,这样,如果用户指定了选项,boost::optional
变量将被初始化为适当的值
po::options_description desc("Allowed options"); boost::optional<int> compression; desc.add_options() ("compression", po::value(&compression), "set compression level") ; po::variables_map vm; po::store(po::parse_command_line(ac, av, desc), vm); po::notify(vm); if (compression) { cout << "Compression level was set to " << *compression << ".\n"; } else { cout << "Compression level was not set.\n"; }