C++で getopt_long関数を使ってコマンドラインオプションをパースする

C++用のコマンドラインオプションパーサーライブラリはこれといった決定版が無く、仕方ないので標準の getopt_long関数を使う

2021年8月22日 嶋田大貴

(boostはこの世に存在しないが stdになぜか輸入だけはされてくる世界線に住んでいる者にとって)C++でなにかコマンドラインツールを作ろうとするとまず真っ先にひっかかる問題、それはコマンドラインオプションのパース処理である。いろんな人が作っているが、それぞれ出来ることが違うので「ハイこれを選んでおけばとにかくOK」というのが無い。仕方ないので glibcのgetopt_long関数をなるべく(コピペで投入しても罪悪感を覚えない程度の)短い追加コードで C++フレンドリーにしてやろうと悪あがきをした結果の産物をここに捨てておく1

指定するコールバック関数のタイプで各オプションにおける引数の要件(引数なし、任意、必須の3種類)を自動的に決定できるのが特徴なのだが、ラムダ [](const std::optional<std::string>& optarg){}std::function<void(const std::optional<std::string>&)>std::function<void(const std::string&)> のどちらに該当するのか型推論では区別できない(?というのが正しいのかわからないがとにかくコンパイルエラーになる)ようなので std::optional 付きの方は明示的にキャストしてやる必要がある。

/**
 * 標準の getopt_long 関数を C++から使いやすくするやつ
 */

#include <memory.h>
#include <getopt.h>

#include <string>
#include <vector>
#include <map>
#include <functional>
#include <optional>
#include <variant>

std::vector<std::string> getopt(
    int argc, char* argv[], 
    const std::vector<std::tuple<
        std::optional<char>/*shortopt*/,
        std::optional<std::string>/*longopt*/,
        std::variant<
            std::function<void(void)>, // 0: no arg
            std::function<void(const std::optional<std::string>&)>, // 1: optional string arg
            std::function<void(const std::string&)> // 2: required string arg
        >/*func*/
    >>& opts)
{
    std::string shortopts;
    std::vector<struct option> longopts;
    std::map<std::string,std::variant<
        std::function<void(void)>,
        std::function<void(const std::optional<std::string>&)>,
        std::function<void(const std::string&)>
    >> funcs;
    for (const auto& opt:opts) {
        if (std::get<0>(opt).has_value()) {
            char shortopt = std::get<0>(opt).value();
            const auto& func = std::get<2>(opt);
            shortopts += shortopt;
            if (std::holds_alternative<std::function<void(const std::optional<std::string>&)>>(func)) shortopts += "::";
            else if (std::holds_alternative<std::function<void(const std::string&)>>(func)) shortopts += ":";
            funcs[std::string(1, shortopt)] = func;
        }
        if (std::get<1>(opt).has_value()) {
            const auto& longopt = std::get<1>(opt).value();
            const auto& shortopt = std::get<0>(opt);
            const auto& func = std::get<2>(opt);
            auto arg_required = std::holds_alternative<std::function<void(const std::optional<std::string>&)>>(func)? optional_argument
                : ((std::holds_alternative<std::function<void(const std::string&)>>(func))? required_argument : no_argument);
            longopts.push_back((struct option) {
                longopt.c_str(),
                arg_required,
                0,
                shortopt.has_value()? shortopt.value() : 0
            });
            funcs[longopt] = func;
        }
    }

    struct option* clongopts = new struct option[longopts.size() + 1];
    struct option* p = clongopts;
    for (const auto& lo:longopts) { 
        memcpy(p, &lo, sizeof(*p));
        p++;
    }
    memset(p, 0, sizeof(*p));
    int c;
    int longindex = 0;
    while ((c = getopt_long(argc, argv, shortopts.c_str(), clongopts, &longindex)) >= 0) {
        const auto func = funcs.find(c == 0? clongopts[longindex].name : std::string(1,(char)c));
        if (func != funcs.end()) {
            if (std::holds_alternative<std::function<void(const std::optional<std::string>&)>>(func->second)) {
                std::get<1>(func->second)(optarg? std::optional<std::string>(optarg) : std::nullopt);
            } else if (std::holds_alternative<std::function<void(const std::string&)>>(func->second)) {
                std::get<2>(func->second)(optarg? optarg : "");
            } else {
                std::get<0>(func->second)();
            }
        }
    }
    delete []clongopts;

    std::vector<std::string> non_option_args;
    for (int i = optind; i < argc; i++) {
        non_option_args.push_back(argv[i]);
    }

    return non_option_args;
}

#ifdef __MAIN__
// テスト用の main関数
// g++ -std=c++20 -D__MAIN__ getopt.cpp
#include <iostream>

int main(int argc, char* argv[])
{
    auto args = getopt(argc, argv, {
        // -a --aaaa with no arg
        {'a', "aaaa", []() {
            std::cout << "option -a(-aaaa) specified" << std::endl;
        }},
        // -b --bbbb with optional arg
        {'b', "bbbb", (std::function<void(const std::optional<std::string>&)>)[](const auto& optarg) {
            if (optarg.has_value()) {
                std::cout << "option -b(--bbbb) with optional arg: " << optarg.value() << std::endl;
            } else {
                std::cout << "option -b(--bbbb) without optional arg" << std::endl;
            }
        }},
        // -c --cccc with required arg
        {'c', "cccc", [](const std::string& optarg) {
            std::cout << "option -c(--cccc) with required arg: " << optarg << std::endl;
        }},
        // -d without long form
        {'d', std::nullopt, []() {
            std::cout << "option -d specified" << std::endl;
        }},
        // --eeee without short form
        {std::nullopt, "eeee", []() {
            std::cout << "option --eeee specified" << std::endl;
        }}
    });

    for (const auto& arg:args) {
        std::cout << "arg: " << arg << std::endl;
    }
    return 0;
}
#endif

なお今日はこれを作って疲れたので本題には手を付けていない。


  1. 標準の getopt(_long)がなんでも出来るかというと全然そんなことはなく、最低限の機能しかないんだけど、そこは標準だからという理由で許せる。 

2021年8月22日 嶋田大貴

記事一覧へ戻る