65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ getopt 的替代方案

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2023年1月2日

MIT

6分钟阅读

viewsIcon

20807

downloadIcon

312

命令行选项的解析器

引言

如今,命令行程序已不像过去那样流行。然而,有时,创建一个这样的程序比创建基于 GUI 的程序要容易得多。在 C 语言的世界里,自古以来,程序员就使用 getopt 函数来解析命令行参数。严格来说,getopt 并不是 C 语言的特性,它是 POSIX 的特性,因此,如果你使用的是 Microsoft Visual C++,它甚至不可用。

下面展示的代码是一个简单的 C++ 实现,用于替换传统的 C 语言 getopt 解析器,并提供了 POSIX 标准所需的大部分灵活性。

示例用法

假设我们需要解析如下命令行

testopt -y --params p1 p2 p3 -o 123 arg1 arg2 arg3

下面是能够实现此功能的程序

#include <mlib/options.h>
#include <iostream>

using namespace std;
using mlib::OptionsParser;

int main(int argc, char** argv)
{
  OptParser parser{
    "h|help \t show help message",
    "y| \t boolean flag",
    "n| \t another boolean flag",
    "p+param parameters \t one or more parameters",
    "o:option value \t optional value",
    "*stuff things \t option with zero or more arguments"
  };

这声明了解析器对象并定义了允许的选项。我们稍后将看到选项定义的具体语法。

  int nonopt;
  if (opt.parse (argc, argv, &nonopt) != 0)
  {
    cout << "Syntax error. Usage:" <<endl;
    cout << opt.synopsis () << endl << "Where:" << opt.description () << endl;
    exit (1);
  }

opt.parse() 函数解析命令行参数。nonopt 被设置为第一个非选项参数的索引。在我们的示例命令行中,nonopt 将是 8,即命令行中 arg1 的索引。

  string par;
  if (opt.getopt ("params", par))
  {
    cout << "params:" << par << endl;
  }
}

如果命令行中存在某个选项,opt.getopt() 函数将返回一个非零值。如果该选项可以带有参数,则它们将作为 string(在我们的示例中是 par)返回,并由用户定义的字符(默认是竖线 '|')分隔。

  if (opt.hasopt ('y'))
    cout << "Yes option set" << endl;

opt.hasopt() 函数返回 true,如果命令行中存在该选项。

OptParser 理解的命令行语法

根据 POSIX 标准,命令行包含三个部分:命令名、选项及其参数以及操作数。POSIX 标准仅描述了由 -(连字符)引导的短选项(单个字符),然而,通常也会有由 -- 引导的长选项。跟在最后一个选项及其参数后面的参数称为操作数。如果其中一个参数是 --,则选项处理将在此时停止,所有剩余的参数都将被视为操作数。选项可以带有一个或多个参数。如果一个选项有多个参数,则参数将在下一个选项开始时、遇到 -- 参数时或到达命令行末尾时结束。短选项可以组合在一个连字符后面,前提是它们没有参数(除了最后一个可能)。例如,与其写

command -a -b -c

不如写

command -abc

选项可以重复。在这种情况下,如果选项带参数,所有参数都会被累积。例如,以下两行是等效的

command -a arg1 arg2 -b
command -a arg1 -b -a arg2

通常使用一种类似 BNF 的符号来描述命令行语法,如下所示

command -a <arg_a> -b [<arg_b>...] -c|--clong <operand1> <operand2>

其中可选参数用方括号括起来,替代项用 '|' 表示。可以重复出现一次或多次的参数用 '...'(省略号)表示。

定义选项

每个有效选项都由一个具有以下语法的描述符字符串描述

[<short>] <flag> [<long>] [<spaces><parameter>] [\t<description>]

其中

  • <short> - 一个单字符,表示选项的短形式。
  • <flag> - 一个字符,指定选项后面可以有多少个参数
    • '|' - 无参数
    • ':' - 一个必需参数
    • '?' - 一个可选参数
    • '+' - 一个或多个参数
    • '*' - 零个或多个参数
  • <long> - 一个 string,指定选项的长形式
  • <parameter> - 一个 string,在语法摘要中用作参数名
  • <description> - 一个 string,用于选项说明。参数名和描述由一个制表符 \t 分隔。选项的长形式或短形式都可以省略。

OptParser API

前面已经提到了一些函数。

构造函数

  OptParser ()                                          (1)

默认构造函数创建一个带有空有效选项列表的解析器对象。

OptParser (std::vector<const char*> &list)              (2)

初始化解析器并设置有效选项列表。

OptParser (const char **list)                           (3)

初始化解析器并设置选项描述符列表。参数是一个以 NULL 指针结尾的 C string 列表。

OptParser (std::initializer_list<const char*> list)     (4)

初始化解析器并设置有效选项列表。初始化列表不需要 NULL 终止符。请参见开头的示例代码。

成员函数

void add_option (const char* descr)                         (1)

向有效选项列表中添加一个新的选项描述符。

void set_options (std::vector <const char*> &list)          (2)

设置有效选项列表。将删除所有先前的选项并添加新的选项。

int parse (int argc, const char* const* argv, int* stop=0)  (3)

解析命令行参数。stop 是一个指向整数值的指针,如果该指针非 null,则接收 argv 数组中第一个非选项参数的索引。如果没有非选项参数,则 stop == argc

如果成功,函数返回 0。否则,它返回一个错误代码

  • 1 = 未知选项
  • 2 = 缺少必需参数
  • 3 = 无效的多个选项 string 如果发生错误,stop 参数(如果非 null)是 argv 数组中触发错误的参数的索引。
int getopt (char option, std::string& optarg, char sep='|') const               (4)
int getopt (const std::string& option, std::string& optarg, char sep='|') const (5)
int getopt (char option, std::vector<std::string>& optarg) const                (6)
int getopt (const std::string& option, std::vector<std::string>& optarg) const  (7)

从命令行返回一个特定的选项。该函数返回命令行中选项的出现次数。对于 (4) 和 (5),optarg 参数接收一个 string,其中包含所有选项的参数,并用 sep 字符分隔。 (6) 和 (7) 以 string 向量的形式返回选项参数。选项可以使用短形式(在 (4) 和 (6) 中)或长形式(在 (5) 和 (7) 中)指定。

bool  hasopt (const std::string &option) const          (8)
bool  hasopt (char option) const                        (9)

检查命令行中是否存在某个选项。选项可以使用短形式(9)或长形式(8)指定。

bool  next (std::string& opt, std::string& optarg, char sep='|')  (10)
bool  next (std::string& opt, std::vector<std::string>& optarg)   (11)

返回命令行中的下一个选项。解析器维护一个内部迭代器,该迭代器在解析命令行时被初始化为第一个可用选项。每次调用 next 函数时,迭代器都会递增,函数将返回下一个选项及其参数。如果没有更多选项,函数将返回 false

形式 (10) 返回一个 string,其中包含所有选项的参数,并用 sep 字符分隔。形式 (11) 以 string 向量的形式返回参数。

const std::string synopsis () const (12)

生成一个格式良好的语法 string。对于前面显示的示例,语法 string

appname -h|--help -y -n -p|--param <parameters>... -o|--option <value> --stuff [things ...]

其中 appname 是可执行文件的实际名称。

const std::string description (size_t indent_size=2) const  (13)

生成一个格式良好的描述 string。对于前面显示的示例,描述 string

  -h|--help                   show help message
  -y                          boolean flag
  -n                          another boolean flag
  -p|--param <parameters>...  one or more parameters
  -o|--option <value>         optional value
  --stuff [things ...]        option with zero or more arguments
const std::string& appname () const  (14)

返回程序名称。这是 argv[0] 的内容,去掉了目录路径和扩展名。

以防您好奇

  • 如何处理带引号的字符串?它们不被处理。OptParser 依赖操作系统来解析命令行参数。
  • 如果参数被组合,为什么您返回选项出现的次数?因为它允许您执行更精细的操作。例如,如果应用程序有一个用于详细模式的选项 '-v',您可以通过重复该选项来增加详细程度。'-v' 将是详细模式,'-vv' 将更详细,'-vvv' 将是极其详细,依此类推。
  • 一些命令行解析器允许您使用等号分隔参数值(例如 -o=123)。您的可以做到吗?不可以,首先,这种语法超出了 POSIX 规范。此外,它会引发有关参数引用的棘手问题。
  • OptParser 的效率如何?嗯,效率不高。为了满足其所有存储需求,OptParser 使用 stringstring 向量。通常,选项解析是一次性活动,其对执行时间的影响很小。效率不是设计目标。

替代解决方案

还有其他提供类似功能的软件包。如果您想查看替代方案,可以看看

我没有尝试过所有这些,但我很想听听其他人的意见。

历史

  • 2023 年 1 月 2 日:初始版本
© . All rights reserved.