本节介绍如何在特定情况下使用该库。
有时,标准的命令行语法是不够的。例如,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 支持
大多数解析器都有 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
locale::global(locale(""));
开头,这将根据用户选择的区域设置设置转换分面。
不过,最好检查您实现的 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"; }