在C++中解析命令行参数?

211

如果程序指定以这种方式运行,那么在C++中解析命令行参数的最佳方法是什么:

prog [-abc] [input [output]]

标准库中是否有实现这个功能的方法,还是我需要编写自己的代码?


相关问题:


我认为Nunit源代码(C#)有一个很好的命令行处理类的示例... - Mitch Wheat
1
最简单的方法是使用其中一个参数解析库:getoptargparse - Andrejs Cainikovs
如果您无法使用库(例如boost),请至少使用std :: vector <std :: string> args(argv,argv + argc);,以便您可以解析字符串向量而不是字符数组。 - stefaanv
1
对于已经在他们的程序中使用OpenCV的人来说,cv::CommandLineParser也可能是一个不错的选择。[我的意思是以防你已经为其他目的在使用它,我并不是要将OpenCV包含进命令行解析器中。] - Dharma
1
最近使用现代C++为此编写了代码:https://github.com/juzzlin/Argengine - juzzlin
请查看 https://github.com/p-ranav/argparse 进行检出。 - droptop
42个回答

268

关于boost::program_options和GNU getopt的建议是不错的。

但是,对于简单的命令行选项,我倾向于使用std::find。

例如,在-f命令行参数之后读取文件名。您还可以检测是否传递了单词选项,如-h以获取帮助。

#include <algorithm>

char* getCmdOption(char ** begin, char ** end, const std::string & option)
{
    char ** itr = std::find(begin, end, option);
    if (itr != end && ++itr != end)
    {
        return *itr;
    }
    return 0;
}

bool cmdOptionExists(char** begin, char** end, const std::string& option)
{
    return std::find(begin, end, option) != end;
}

int main(int argc, char * argv[])
{
    if(cmdOptionExists(argv, argv+argc, "-h"))
    {
        // Do stuff
    }

    char * filename = getCmdOption(argv, argv + argc, "-f");

    if (filename)
    {
        // Do interesting things
        // ...
    }

    return 0;
}

使用这种方法时,要注意必须将std::strings用作std::find的值,否则将对指针值执行相等性检查。
我希望编辑此回答而不是添加新回答,因为它基于原始答案。我稍微重写了函数并将它们封装在一个类中,所以这里是代码。我认为这样使用它可能很实用:
class InputParser{
    public:
        InputParser (int &argc, char **argv){
            for (int i=1; i < argc; ++i)
                this->tokens.push_back(std::string(argv[i]));
        }
        /// @author iain
        const std::string& getCmdOption(const std::string &option) const{
            std::vector<std::string>::const_iterator itr;
            itr =  std::find(this->tokens.begin(), this->tokens.end(), option);
            if (itr != this->tokens.end() && ++itr != this->tokens.end()){
                return *itr;
            }
            static const std::string empty_string("");
            return empty_string;
        }
        /// @author iain
        bool cmdOptionExists(const std::string &option) const{
            return std::find(this->tokens.begin(), this->tokens.end(), option)
                   != this->tokens.end();
        }
    private:
        std::vector <std::string> tokens;
};

int main(int argc, char **argv){
    InputParser input(argc, argv);
    if(input.cmdOptionExists("-h")){
        // Do stuff
    }
    const std::string &filename = input.getCmdOption("-f");
    if (!filename.empty()){
        // Do interesting things ...
    }
    return 0;
}

5
这个功能可以直接使用,但请注意选项参数是const std::string&。很重要的一点是,传入std::find函数的值参数必须是一个std::string类型,这样才会使用std::string :: operator==(),而不是char * operator==()(后者仅比较指针值而不是字符串内容)。 - iain
7
这并不像期望的那样有效,例如像 tar 应用程序:tar -xf file,对吗?每个选项必须分开。grep -ln pattern file 不能被理解,应该是 grep -l -n pattern file - lmat - Reinstate Monica
11
如果你想要使用 POSIX 风格的命令行选项,那么你应该使用其他答案中提到的命令行处理库。正如这个回答所说,这适用于简单的命令行选项。 - iain
4
这很不错,但需要做两个小改进:首先,构造函数参数应该是const限定的。其次,getCmdOption的返回值应该是一个值,而不是一个引用,否则你会遇到https://dev59.com/j3M_5IYBdhLWcg3whzrx。除此之外,这是一个不错和简单的解决方案,我会使用它,谢谢。 - Tomáš Dvořák
3
  1. @iain,那段代码的许可证是什么?
  2. @TomášDvořák 对于返回对临时对象的引用是正确的,当 getCmdOption 返回空字符串时。 InputParser 应该拥有一个名为 std::string empty_string 的成员,并在选项未找到时返回其引用。
- lrineau
显示剩余6条评论

90

16
不错的选择。或者,如果由于某些原因无法使用boost,那么标准的基于C语言的"getopt"函数也能完成任务。 - hookenz
12
boost::program_options 的文档可以更加完善。特别是在找到如何使用文件来保存选项时会有困难,这是一个关键的功能。 - gatopeich
83
如果仅仅是为了解析命令行选项而引入Boost到代码库中,有点“杀鸡焉用牛刀”的感觉。如果已经有了Boost,那么可以使用它。否则建议考虑使用类似gopt的东西。总体来说没有什么不好的,但是Boost有点过于复杂,而且版本与g++版本紧密绑定。 - Stephen
28
此外,boost::program_options 不是一个纯头文件库。您需要构建 boost。这非常麻烦。 - ABCD
9
这个任务使用Boost似乎有点过头了。 - user1261537
显示剩余2条评论

64
我可以建议使用Templatized C++ Command Line Parser Library(GitHub上有一些分支可供选择),该API非常简单易懂,其网站上的引述如下:

该库完全以头文件的形式实现,易于与其他软件一起使用和分发。它在MIT许可证下授权,可无忧地进行分发。

这是手册中的一个示例,此处为了简洁起见进行了颜色处理:

#include <string>
#include <iostream>
#include <algorithm>
#include <tclap/CmdLine.h>

int main(int argc, char** argv)
{

    // Wrap everything in a try block.  Do this every time,
    // because exceptions will be thrown for problems.
    try {

    // Define the command line object, and insert a message
    // that describes the program. The "Command description message"
    // is printed last in the help text. The second argument is the
    // delimiter (usually space) and the last one is the version number.
    // The CmdLine object parses the argv array based on the Arg objects
    // that it contains.
    TCLAP::CmdLine cmd("Command description message", ' ', "0.9");

    // Define a value argument and add it to the command line.
    // A value arg defines a flag and a type of value that it expects,
    // such as "-n Bishop".
    TCLAP::ValueArg<std::string> nameArg("n","name","Name to print",true,"homer","string");

    // Add the argument nameArg to the CmdLine object. The CmdLine object
    // uses this Arg to parse the command line.
    cmd.add( nameArg );

    // Define a switch and add it to the command line.
    // A switch arg is a boolean argument and only defines a flag that
    // indicates true or false.  In this example the SwitchArg adds itself
    // to the CmdLine object as part of the constructor.  This eliminates
    // the need to call the cmd.add() method.  All args have support in
    // their constructors to add themselves directly to the CmdLine object.
    // It doesn't matter which idiom you choose, they accomplish the same thing.
    TCLAP::SwitchArg reverseSwitch("r","reverse","Print name backwards", cmd, false);

    // Parse the argv array.
    cmd.parse( argc, argv );

    // Get the value parsed by each arg.
    std::string name = nameArg.getValue();
    bool reverseName = reverseSwitch.getValue();

    // Do what you intend.
    if ( reverseName )
    {
            std::reverse(name.begin(),name.end());
            std::cout << "My name (spelled backwards) is: " << name << std::endl;
    }
    else
            std::cout << "My name is: " << name << std::endl;


    } catch (TCLAP::ArgException &e)  // catch any exceptions
    { std::cerr << "error: " << e.error() << " for arg " << e.argId() << std::endl; }
}

3
对我来说,这个选项是最简单的,但它会在我的程序中添加一个子目录,并包含许多头文件。相应地需要编辑包含路径。 - Richard
1
这些年来,我使用过各种解决方案,包括自己编写的。我和其他人一样赞扬TCLAP的优点。它易于集成并满足我的需求。 - Moshe Rubin
看起来这个项目已经转移到了sourceforge - Joe
1
@JohnShedletsky 你确定吗?虽然我不再使用这个库了,但在手册中展示了长格式和短格式参数。 - naufraghi
1
我没有对这个进行投票否定,但我个人不喜欢像这样小的东西依赖于异常。 - Mike Weir
显示剩余5条评论

44

14
这似乎是C++最明显的选择,但它的文档不够完整。请尝试在其中找到如何将选项存储和从文件中检索出来,这是一个必要的功能。我不喜欢使用它的代码外观,特别是options.add_options()(option1)(option2)...这样的措辞,我认为这是对C++语法的滥用。 - gatopeich
8
使用Boost.Program_options编译代码似乎不是很直接,需要链接选项等等,除了包含头文件之外。 - Richard
2
你可以用更少的钱得到几乎相同的东西。如果你想要像 --long-option 这样的东西,自己做起来相当简单。 - Luis Machuca
在一个极端情况下,对于非常简单的程序或者你直接使用argv[]数组工作。另一种情况是,为了完全灵活地处理参数,你可以直接使用argv数组(你可以做prog -1 firstinput -2 second input -obj {constructor arguments ..})。否则,使用boost、tclap或其他许多工具。 - Kemin Zhou
boost::program_options 过度设计,使用难度大,文档不足。这是少数几个 Boost 库之一,非常需要完全重新设计和重写。如果可以避免使用,请不要使用它。 - András Aszódi

37

你可以使用GNU GetOpt(LGPL)或其中一个C ++端口,例如getoptpp(GPL)。

使用GetOpt的简单示例,以实现您想要的内容(prog [-ab] input),如下所示:

// C Libraries:
#include <string>
#include <iostream>
#include <unistd.h>

// Namespaces:
using namespace std;

int main(int argc, char** argv) {
    int opt;
    string input = "";
    bool flagA = false;
    bool flagB = false;

    // Retrieve the (non-option) argument:
    if ( (argc <= 1) || (argv[argc-1] == NULL) || (argv[argc-1][0] == '-') ) {  // there is NO input...
        cerr << "No argument provided!" << endl;
        //return 1;
    }
    else {  // there is an input...
        input = argv[argc-1];
    }

    // Debug:
    cout << "input = " << input << endl;

    // Shut GetOpt error messages down (return '?'): 
    opterr = 0;

    // Retrieve the options:
    while ( (opt = getopt(argc, argv, "ab")) != -1 ) {  // for each option...
        switch ( opt ) {
            case 'a':
                    flagA = true;
                break;
            case 'b':
                    flagB = true;
                break;
            case '?':  // unknown option...
                    cerr << "Unknown option: '" << char(optopt) << "'!" << endl;
                break;
        }
    }

    // Debug:
    cout << "flagA = " << flagA << endl;
    cout << "flagB = " << flagB << endl;

    return 0;
}

5
GNU getopt 是 GPL 许可的,而 getoptpp 也是 GPL 许可的,因此对于非开源软件来说,使用 Boost variant 可能更好。 - sorin
@SorinSbarnea,我不是很确定,但我认为许可证实际上是LGPLv2 - Matthew Flaschen
抱歉,但项目页面上的Google Code许可证明确声明了GPL。 - sorin
2
@SorinSbarnea,你看了我的链接吗?我应该表达得更清楚,我指的是getopt和getopt-gnu,而不是getoptpp。 - Matthew Flaschen
1
Getopt对于C++程序来说太低级了。我强烈反对在C++程序中使用它。 - Thomas Eding

27

GNU GetOpt

使用 GetOpt 的简单示例:

// C/C++ Libraries:
#include <string>
#include <iostream>
#include <unistd.h>

// Namespaces:
using namespace std;

int main(int argc, char** argv) {
    int opt;
    bool flagA = false;
    bool flagB = false;

    // Shut GetOpt error messages down (return '?'): 
    opterr = 0;

    // Retrieve the options:
    while ( (opt = getopt(argc, argv, "ab")) != -1 ) {  // for each option...
        switch ( opt ) {
            case 'a':
                    flagA = true;
                break;
            case 'b':
                    flagB = true;
                break;
            case '?':  // unknown option...
                    cerr << "Unknown option: '" << char(optopt) << "'!" << endl;
                break;
        }
    }

    // Debug:
    cout << "flagA = " << flagA << endl;
    cout << "flagB = " << flagB << endl;

    return 0;
}

如果您有需要接受参数的选项,您还可以使用optarg


15
我理解这个库在C代码中的用途,但我个人认为,在任何我写过的C++应用程序中,这种方式都太低层次了,不太可接受。如果您不需要纯C,请找一个更好的库。 - Thomas Eding
1
还有一个带值的getopt GNU示例,例如myexe -c myvalue。您可以尝试搜索“使用getopt解析参数的示例”。 - JayS
这是GNU自己提供的一个示例:example - finnmglas

23

另一个选择是The Lean Mean C++ Option Parser:

http://optionparser.sourceforge.net

它是一个仅包含单个头文件的头文件库,与所有其他建议不同的是,它也是自由的,即它没有任何依赖关系。特别是它没有STL的依赖性。它甚至不使用异常或任何需要库支持的东西。这意味着它可以与纯C或其他语言链接,而不会引入“外来”库。

与boost::program_options一样,它的API提供了方便的直接访问选项的方式,即您可以编写像这样的代码:

if (options[HELP]) ... ;

int verbosity = options[VERBOSE].count();

但与boost::program_options不同的是,这只是使用枚举索引的数组。这提供了关联容器的便利性,而不会增加负担。

它有很好的文档,并且具有公司友好的许可证(MIT)。

TLMC++OP包括一个漂亮的格式化程序,用于使用消息的换行和列对齐,这在本地化程序时非常有用,因为它确保输出在具有更长消息的语言中看起来很好。它还可以节省手动格式化使用情况的麻烦,以适应80列。


1
选项解析器的未来参考链接:http://sourceforge.net/projects/optionparser/ - Samaursa
在我看来,它非常容易出错。例如,在使用它时,我不断收到由库代码引起的 segfault 错误。 - Asalle

21
for (int i = 1; i < argc; i++) {

    if (strcmp(argv[i],"-i")==0) {
        filename = argv[i+1];
        printf("filename: %s",filename);
    } else if (strcmp(argv[i],"-c")==0) {
        convergence = atoi(argv[i + 1]);
        printf("\nconvergence: %d",convergence);
    } else if (strcmp(argv[i],"-a")==0) {
        accuracy = atoi(argv[i + 1]);
        printf("\naccuracy:%d",accuracy);
    } else if (strcmp(argv[i],"-t")==0) {
        targetBitRate = atof(argv[i + 1]);
        printf("\ntargetBitRate:%f",targetBitRate);
    } else if (strcmp(argv[i],"-f")==0) {
        frameRate = atoi(argv[i + 1]);
        printf("\nframeRate:%d",frameRate);
    }

}

4
这会无视边界检查地提取数组元素。 - Otto Allmendinger
2
-1: 由于没有边界检查 - Sebastian Mach
9
对于argv[i+1]的引用可能会轻易地超出argv数组的边界。考虑以"-i"作为最后一个参数运行程序。 - Keith Thompson
2
-1 是一种“自己动手”的答案,而问题明确要求使用STL中的“库”来完成任务。另外,正如Keith Thompson和mmutz所指出的那样,边界检查中存在一些错误。 - Robert Munafo
31
我觉得评论非常苛刻。我认为展示一个不使用库进行的示例也是公平的。这个答案是补充性的+1,抱歉。 - user18490
显示剩余3条评论

20

4
我已经使用过 getopt、谷歌的 gflags、Boost 的 program_options,而 tclap 真是太棒了。我对 tclap 赞不绝口,特别是考虑到其他可用的替代品。我的抱怨只是它的帮助格式与我习惯的略有不同。 - Sean

18

如果您只想自己处理命令行选项,最简单的方法是将以下内容放入:

vector<string> args(argv + 1, argv + argc);

在你的main()函数的顶部。这将所有命令行参数复制到一个std::string向量中。然后,您可以使用==轻松比较字符串,而不是无尽的strcmp()调用。例如:

int main(int argc, char **argv) {
    vector<string> args(argv + 1, argv + argc);
    string infname, outfname;

    // Loop over command-line args
    // (Actually I usually use an ordinary integer loop variable and compare
    // args[i] instead of *i -- don't tell anyone! ;)
    for (auto i = args.begin(); i != args.end(); ++i) {
        if (*i == "-h" || *i == "--help") {
            cout << "Syntax: foomatic -i <infile> -o <outfile>" << endl;
            return 0;
        } else if (*i == "-i") {
            infname = *++i;
        } else if (*i == "-o") {
            outfname = *++i;
        }
    }
}

[编辑:我意识到我正在复制程序的名称argv[0]args中,已经修复。]


添加了一个简单的例子。实际上,vector<string> 带给你的只是使用 == 进行简单比较和方便的按值复制。 - j_random_hacker
这并没有帮助做任何使用argc/argv无法完成的事情。 - brofield
@brofield:当然,它并没有改变世界。但我发现“==”和值语义足以简化事情,所以我一直在使用它。 - j_random_hacker
@j_random_hacker - 如果我想记住参数的顺序怎么办?例如,如果用户键入 mycommand.exe -h file.csv,我想告诉他们他们没有正确使用实用程序以及为什么(如果他们只是使用版本,则不应提供文件名)。这个例子相当简单,但我可以想到更复杂的标志。最终结果将是:有时顺序很重要,有时不重要。那么...我该怎么办呢?如果您对我的问题有疑问,请告诉我。 - Hamish Grubijan
@Hamish:我有点困惑——将字符串加载到向量中并不会“丢失”它们的顺序。您仍然可以使用args[i]访问第i个参数(事实上,我经常这样做,正如我代码片段中的注释所说)。如果您只需要一次处理一个参数,则迭代器样式会更方便一些。这回答了您的问题吗? - j_random_hacker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接