程序参数解析C/C++库 The Lean Mean C++ Option Parser

燕智
2023-12-01

开发中我们经常使用程序参数,根据参数的不同来实现不同的功能。POSIX和GNU组织对此都制定了一些标准,为了我们程序更为通用标准,建议遵循这些行业内的规范,本文介绍的开源库The Lean Mean C++ Option Parser就可以很好满足我们的需求。

The Lean Mean C++ Option Parser在线文档:(下载头文件和相关demo)The Lean Mean C++ Option Parser: Main Page (sourceforge.net)

一、优点:

1.该库只有一个头文件,只需要包含“#include "optionparser.h"即可”;

2.该库是独立的,没有任何依赖关系,甚至不依赖C或C++标准库;

3.与getopt()和派生函数不同,它不会强制您按顺序循环使用选项,使用更加便捷

二、选项语法:
1.The Lean Mean C++ Option Parser 遵循POSIX getopt()约定,支持GNU风格的getopt_long()长选项以及Perl风格的单减长选项(getopt_long_only());
2.短可选项的格式:-X ,X是任意字符;
3.短可选项的可以组合,比如:-X -Y 和 -XY是一样的;
4.短可选项可以采用单独的或者附加方式取参数,比如:-X foo   -Xfoo 。如果注册X为长可选项 ,解析器就可以接受 -X=foo 的格式;
5.最后一个带参数的短可选项可以和其他的组合,比如: -ABCXfoo  -ABCX foo (foo 是-X 可选项的参数);
6.长可选项的格式    -可选项名称 ;
7.长可选项格式的可选项名称可以是任意名称,包含任意字符,即使“=”也是可以的,但是不建议这样做;
8.长可选项可以缩写,只要缩写是明确的。此外还可以设置缩写的最小长度;
9.长可选项可以“-”,“--”开始;
10.长可选项可以采用单独的(-option arg)或者附加的(-option=arg)参数格式。附加格式中,必须要使用“=” ;
11.空字符串可以作为附加格式的长可选选项的参数:“-option= ”。作为参数的空字符串和完全没有参数之间的区别需要注意;
12.允许将空字符串作为长可选项和短可选项的单独参数;
13.短可选项和长可选项都可以以“-”字符开头。例如:-X-X,     -X -X,     -long-X=-X .这三种情况下 的参数都是 -X;
14.如果使用内置的Arg::Optional,则必须附加可选项的参数;
15.特殊选项–(即没有名称)终止选项列表。后面的所有内容都是非选项参数,即使它以“-”字符开头。–本身不会出现在解析结果中;
16.第一个不以“-”开头并且不属于前面的选项的参数将终止选项列表,并且是第一个非选项参数。接下来所有命令行参数都被视为非选项参数,即使它们以“-”开头。
    注意:这种行为是由POSIX强制执行的,但GNU getopt()只有在明确请求时(例如通过设置POSIXLY_CORRECT)才会执行。您可以通过将true作为第一个参数传递给例如Parser::parse()来启用GNU行为。
17.看起来像选项(即“-”后面至少有1个字符)但实际上不是的参数不被视为非选项参数。它们被视为未知选项,并被收集到用于错误报告的未知选项列表中。这意味着,为了传递以减号字符开头的第一个非选项参数,需要使用– 特殊选项,例如:
    program -x -- --strange-filename
 在这个例子中, –strange-filename 是非选项参数。如果前面的‘-’  没有,它将被视为未知选项。

三、使用:

熟悉以下几个类,即可快速使用该库,下面对这些类型进行介绍:

1.Descriptor

描述一个选项、它的帮助文本(用法)以及应该如何解析它。下面是一个示例:

enum OptionIndex {CREATE, ...};
enum OptionType {DISABLE, ENABLE, OTHER};
const option::Descriptor usage[] = {
  { CREATE,                                            // index
    OTHER,                                             // type
    "c",                                               // shortopt
    "create",                                          // longopt
    Arg::None,                                         // check_arg
    "--create  Tells the program to create something." // help
  }
  , ...
};

index:解析器解析后该可选项所在数组的索引。建议使用枚举增强阅读性,如上示例。

描述符具有相同索引的命令行选项将按照它们在命令行上出现的顺序出现在相同的链表中。如果有多个长选项别名引用同一个选项,请为它们的描述符指定相同的索引。

如果你有意思完全相反的选项(例如–enable foo和–disable foo),你也应该给它们相同的索引,但通过不同的类型值来区分它们。这样,它们最终会出现在同一个列表中,你就可以只取列表的最后一个元素并使用其类型。通过这种方式,你可以稍后在命令行上的开关会覆盖以前的开关,而无需手动编码

type:用于区分相同index的可选项。

shortopt:此字符串中的每个字符都将被接受为短选项字符。该字符串不得包含减号字符“-”,否则将出现未定义的行为。如果此描述符不应包含短选项字符,请使用空字符串“”。此处不允许使用NULL!

longopt:长可选项名称。如果此描述符不应具有长选项名称,请使用空字符串“”。此处不允许使用NULL!虽然shortopt允许多个短选项字符,但每个描述符只能有一个长选项名称。如果有多个长选项名称引用同一个选项,请使用具有相同索引和类型的单独描述符。您可以在这样的别名描述符中重复短选项字符,但没有必要这样做。

check_arg:对于每个匹配shortopt或longopt的选项,将调用此函数来检查该选项的潜在参数。

2.Arg

每个选项参数都有一个如此函数设置进Descriptor,用于检查选项参数有效性。

typedef ArgStatus (*CheckArg)(const Option& option, bool msg);

它用于检查选项是否可以接受潜在的参数。即使没有参数,该函数也会被调用。在这种情况下,option.arg将为NULL。

如果msg为true,并且函数确定参数是不可接受的,并且这是一个致命错误,那么它应该在返回ARG_ILEGAL之前向用户输出一条消息。如果msg为false,则该函数应保持静音(否则将收到重复的消息)。

ArgStatus的定义如下:

ARG_NONE 选项没有参数。
ARG_OK参数被选项接受。
ARG_IGNORE该参数是不可接受的,但这不是致命的,因为该选项的参数是可选的。
ARG_ILLEGAL参数不可以接受,会有致命错误。

目前已经预制了几个校验规则,需要更为精准的校验规则可以自己定制,例如:

struct Arg: public option::Arg
{
  static void printError(const char* msg1, const option::Option& opt, const char* msg2)
  {
    fprintf(stderr, "ERROR: %s", msg1);
    fwrite(opt.name, opt.namelen, 1, stderr);
    fprintf(stderr, "%s", msg2);
  }
  static option::ArgStatus Unknown(const option::Option& option, bool msg)
  {
    if (msg) printError("Unknown option '", option, "'\n");
    return option::ARG_ILLEGAL;
  }
  static option::ArgStatus Required(const option::Option& option, bool msg)
  {
    if (option.arg != 0)
      return option::ARG_OK;
    if (msg) printError("Option '", option, "' requires an argument\n");
    return option::ARG_ILLEGAL;
  }
  static option::ArgStatus NonEmpty(const option::Option& option, bool msg)
  {
    if (option.arg != 0 && option.arg[0] != 0)
      return option::ARG_OK;
    if (msg) printError("Option '", option, "' requires a non-empty argument\n");
    return option::ARG_ILLEGAL;
  }
  static option::ArgStatus Numeric(const option::Option& option, bool msg)
  {
    char* endptr = 0;
    if (option.arg != 0 && strtol(option.arg, &endptr, 10)){};
    if (endptr != option.arg && *endptr == 0)
      return option::ARG_OK;
    if (msg) printError("Option '", option, "' requires a numeric argument\n");
    return option::ARG_ILLEGAL;
  }
};

3.Stats

确定用于Parser的缓冲区和选项数组的最小长度。因为Parser不使用动态内存,所以必须预先分配其输出数组。如果您不想使用固定大小的数组(可能会变得太小,导致命令行参数被丢弃),可以使用Stats来确定正确的大小。

buffer_max:真实参数个数;

options_max:依赖Descriptor,比Descriptor中的Index数量多一个。

4.Parser

检查参数的有效性,并将其解析为更易于使用的数据结构。

int main(int argc, char* argv[])
{
  argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present
  option::Stats  stats(usage, argc, argv);
  option::Option options[stats.options_max], buffer[stats.buffer_max];
  option::Parser parse(usage, argc, argv, options, buffer);
  if (parse.error())
    return 1;
  if (options[HELP])
  ...

optionsCount():返回有效选项数量;

nonOptionsCount():返回非选项参数数量;

error():返回是否存在解析错误。选项的非法参数(即CheckArg返回ARG_ILEGAL)是一个不可恢复的错误,会中止解析。只有当CheckArg函数返回ARG_ILEGAL时,未知选项才是错误。否则,它们将被收集。

5.Parser

命令行中已解析的选项及其参数(如果有)。

Parser将具有相同Descriptor::索引的所有已解析选项链接在一起,形成一个链表。这使您能够轻松实现处理重复选项和启用/禁用对的所有常见方法。

desc:指向选项的Descriptor;

name:选项的名称;

arg:选项的参数;

namelen:选项名称长度。

示例:

//Test for presence of a switch in the argument vector:
if ( options[QUIET] ) ... 
//Evaluate –enable-foo/–disable-foo pair where the last one used wins:
if ( options[FOO].last()->type() == DISABLE ) ... 
//Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose):
int verbosity = options[VERBOSE].count(); 
//Iterate over all –file=<fname> arguments:
for (Option* opt = options[FILE]; opt; opt = opt->next())
 fname = opt->arg; ... 

四、整体示例:

#include "optionparser.h"
#include <sstream>

struct Arg : public option::Arg
{
    static void print_error(
            const char* msg1,
            const option::Option& opt,
            const char* msg2)
    {
        fprintf(stderr, "%s", msg1);
        fwrite(opt.name, opt.namelen, 1, stderr);
        fprintf(stderr, "%s", msg2);
    }

    static option::ArgStatus Unknown(
            const option::Option& option,
            bool msg)
    {
        if (msg)
        {
            print_error("Unknown option '", option, "'\n");
        }
        return option::ARG_ILLEGAL;
    }

    static option::ArgStatus Required(
            const option::Option& option,
            bool msg)
    {
        if (option.arg != 0 && option.arg[0] != 0)
        {
            return option::ARG_OK;
        }

        if (msg)
        {
            print_error("Option '", option, "' requires an argument\n");
        }
        return option::ARG_ILLEGAL;
    }

    static option::ArgStatus Numeric(
            const option::Option& option,
            bool msg)
    {
        char* endptr = 0;
        if ( option.arg != nullptr )
        {
            strtol(option.arg, &endptr, 10);
            if (endptr != option.arg && *endptr == 0)
            {
                return option::ARG_OK;
            }
        }

        if (msg)
        {
            print_error("Option '", option, "' requires a numeric argument\n");
        }
        return option::ARG_ILLEGAL;
    }

    template<long min = 0, long max = std::numeric_limits<long>::max()>
    static option::ArgStatus NumericRange(
            const option::Option& option,
            bool msg)
    {
        static_assert(min <= max, "NumericRange: invalid range provided.");

        char* endptr = 0;
        if ( option.arg != nullptr )
        {
            long value = strtol(option.arg, &endptr, 10);
            if ( endptr != option.arg && *endptr == 0 &&
                    value >= min && value <= max)
            {
                return option::ARG_OK;
            }
        }

        if (msg)
        {
            std::ostringstream os;
            os << "' requires a numeric argument in range ["
               << min << ", " << max << "]" << std::endl;
            print_error("Option '", option, os.str().c_str());
        }

        return option::ARG_ILLEGAL;
    }

    static option::ArgStatus String(
            const option::Option& option,
            bool msg)
    {
        if (option.arg != 0)
        {
            return option::ARG_OK;
        }
        if (msg)
        {
            print_error("Option '", option, "' requires an argument\n");
        }
        return option::ARG_ILLEGAL;
    }

};

enum  optionIndex
{
    UNKNOWN_OPT,
    HELP,
    SAMPLES,
    INTERVAL,
    ENVIRONMENT,
};

const option::Descriptor usage[] = {
    { UNKNOWN_OPT, 0, "", "",                Arg::None,
      "Usage: HelloWorldExample <publisher|subscriber>\n\nGeneral options:" },
    { HELP,    0, "h", "help",               Arg::None,      "  -h \t--help  \tProduce help message." },
    { UNKNOWN_OPT, 0, "", "",                Arg::None,      "\nPublisher options:"},
    { SAMPLES, 0, "s", "samples",            Arg::NumericRange<>,
      "  -s <num>, \t--samples=<num>  \tNumber of samples (0, default, infinite)." },
    { INTERVAL, 0, "i", "interval",          Arg::NumericRange<>,
      "  -i <num>, \t--interval=<num>  \tTime between samples in milliseconds (Default: 100)." },
    { ENVIRONMENT, 0, "e", "env",            Arg::None,       "  -e \t--env   \tLoad QoS from environment." },
    { 0, 0, 0, 0, 0, 0 }
};


int main(
        int argc,
        char** argv)
{
    argc -= (argc > 0);
    argv += (argc > 0); // skip program name argv[0] if present
    option::Stats stats(true, usage, argc, argv);
    std::vector<option::Option> options(stats.options_max);
    std::vector<option::Option> buffer(stats.buffer_max);
    option::Parser parse(true, usage, argc, argv, &options[0], &buffer[0]);
    
    try
    {
        if (parse.error())
        {
            throw 1;
        }

        if (options[HELP] || options[UNKNOWN_OPT])
        {
            throw 1;
        }
        
        const char* type_name = parse.nonOption(0);

        if (parse.optionsCount() && type_name >= buffer[0].name)
        {
            throw 2;
        }
    }
    catch(int error)
    {
        //deal error
    }

    
    return 0;
}

 类似资料: