
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
なお今日はこれを作って疲れたので本題には手を付けていない。
-
標準の getopt(_long)がなんでも出来るかというと全然そんなことはなく、最低限の機能しかないんだけど、そこは標準だからという理由で許せる。 ↩
2021年8月22日 嶋田大貴