LoginSignup
8
9

More than 5 years have passed since last update.

C++でコマンドラインパーサを書いた

Posted at

C++14で使えるコマンドラインパーサnonsugarを書きました。せっかく書いたので晒します。

動機

C++で使えるコマンドラインパーサとして有名どころにBoost.Program_optionsがありますが、ライブラリのビルド・リンクが必要なのが面倒です。
ヘッダオンリーのライブラリも探せばありますが(このへんとか)、個人的な好みとしてさらに次の2点を求めたいです。

  • ユーザーが非constな変数を定義しなくてよい
  • オプションが引数を取る場合、引数の型の指定は1回だけでよい(オプションの定義時と、引数の取得時の両方で型を指定する必要がない)

そんなわけで自分で書きました。あとoptparse-declarativeとか見て、サブコマンドを簡単に定義できるのかっこいいと思ったので真似しました。

特徴

  • シングルヘッダ
  • char以外の文字型をサポート
  • サブコマンドをサポート
  • オプションの引数として任意の型を指定可能
  • ヘルプの生成

Boost.Program_optionsのチュートリアルを再実装してみたものです。

// compiler.cpp
#include <iostream>
#include <string>
#include <vector>
#include <nonsugar.hpp>
using namespace nonsugar;

int main(int argc, char *argv[])
try {
    auto const cmd = command<char>("compiler", "nonsugar example")
        .flag<'h'>({}, {"help"}, "produce help message")
        .flag<'v'>({'v'}, {"version"}, "print version string")
        .flag<'o', int>({}, {"optimization"}, "N", "optimization level")
        .flag<'I', std::vector<std::string>>({'I'}, {"include-path"}, "PATH", "include path")
        .argument<'i', std::vector<std::string>>("INPUT-FILE")
        ;
    auto const opts = parse(argc, argv, cmd);
    if (opts.has<'h'>()) {
        std::cout << usage(cmd);
        return 0;
    }
    if (opts.has<'v'>()) {
        std::cout << "compiler, version 1.0\n";
        return 0;
    }
    if (opts.has<'o'>()) {
        std::cout << "optimization level: " << opts.get<'o'>() << "\n";
    }
    if (opts.has<'I'>()) {
        std::cout << "include paths:";
        for (auto const &path : opts.get<'I'>()) std::cout << " " << path;
        std::cout << "\n";
    }
    std::cout << "input files:";
    for (auto const &file : opts.get<'i'>()) std::cout << " " << file;
    std::cout << "\n";
} catch (error const &e) {
    std::cerr << e.message() << "\n";
    return 1;
}
$ g++ -std=c++14 -o compiler compiler.cpp

$ ./compiler --help
Usage: compiler [OPTION...] [INPUT-FILE...]
  nonsugar example

Options:
           --help               produce help message
  -v       --version            print version string
           --optimization=N     optimization level
  -I PATH  --include-path=PATH  include path

$ ./compiler --optimization=4 -I foo a.cpp b.cpp
optimization level: 4
include paths: foo
input files: a.cpp b.cpp

インストール

nonsugar.hppを適当なインクルードディレクトリにコピーして下さい。C++14をサポートしたコンパイラなら動作するはずです。

使い方

1. コマンドを定義します。

// commandクラスのインスタンスを作成します。
auto const cmd = command<char>("compiler", "nonsugar example")
  // テンプレート引数は、オプションの識別子に使う型です。charが簡便でしょう。
  // 第1引数はプログラムの名前、第2引数はプログラムの説明です(オプション)。

    // flag()メンバ関数でオプションを追加します(新しいcommandクラスのインスタンスが返ります)。
    // 引数を取らないオプションの場合:
    .flag<'h'>({'h'}, {"help"}, "produce help message", true)
      // テンプレート引数は、オプションの識別子です。
      // 第1引数は短い名前(-hなど)のリスト、第2引数は長い名前(--helpなど)のリストです。
      // 第3引数は、オプションの説明です。
      // 第4引数にtrueを与えると、このオプションが指定されたときにサブコマンドや引数を解析しません
      // (オプション)。
      //   --helpなど、サブコマンドや引数を不要とするオプションの場合に指定します。

    // 引数を取るオプションの場合:
    .flag<'o', int>({}, {"optimization","optimisation"}, "N", "optimization level", 10)
      // テンプレート第2引数に、引数の型を指定します。
      //   boost::optional<T>を指定すると、引数を取っても取らなくてもよくなります。
      //   std::vector<T>などコンテナ型を指定すると、オプションを複数回指定できるようになります。
      // 第3引数は、引数の名前を表す文字列です。
      // 第5引数は、オプションが指定されなかった場合のデフォルト値です(オプション)。

    // argument()メンバ関数でプログラムの引数を追加します。
    .argument<'i', std::vector<std::string>>("INPUT-FILE")
      // テンプレート第1引数は、オプションの識別子です。
      // テンプレート第2引数は、プログラムの引数の型です。
      //   boost::optional<T>やコンテナ型が指定できます。
      // 第1引数は、プログラムの引数の名前を表す文字列です。
    ;

2. コマンドライン引数を解析します。

try {
    // parse()関数でコマンドライン引数を解析します。
    // 戻り値は、解析されたオプションのマップです。
    auto const opts = parse(argc, argv, cmd);

} catch (error const &e) {
  // 解析に失敗すると例外が飛びます。

    // message()メンバ関数で、エラーの内容が取得できます。
    std::cerr << e.message() << "\n";
}

3. オプションのマップを読み取ります。

// has()でオプションが指定されたかどうか知ることができます。
if (opts.has<'h'>()) {
  // テンプレート引数はオプションの識別子です。

    // usage()でコマンドからヘルプ文字列を取得できます。
    std::cout << usage(cmd);
    return 0;
}

if (opts.has<'o'>()) {
    // get()でオプションの引数を取得できます。
    // 指定されなかったオプションから引数を取ろうとすると、未定義動作になります。
    int const n = opts.get<'o'>();

    std::cout << "optimization level: " << n << "\n";
}

サブコマンド

1. 普通のコマンドを作成するように、サブコマンドを作成します。

// helloサブコマンド
auto const helloCmd = command<char>("subcmd hello")
    .argument<'n', std::string>("NAME")
    ;
// addサブコマンド
auto const addCmd = command<char>("subcmd add")
    .argument<'l', int>("LHS")
    .argument<'r', int>("RHS")
    ;

2. メインコマンドに作成したサブコマンドを追加します。

// メインコマンド
auto const cmd = command<char>("subcmd")

    // subcommand()メンバ関数でサブコマンドを追加します。
    .subcommand<'H'>("hello", "hello command", helloCmd)
      // テンプレート引数はオプションの識別子です。
      // 第1引数はサブコマンドの名前、第2引数はサブコマンドの説明です。
      // 第3引数はサブコマンドそのものです。

    .subcommand<'A'>("add", "add command", addCmd)

    .flag<'h'>({'h'}, {"help"}, "produce help message", true)
    ;

3. コマンドライン引数をパースし、結果を読み取ります。

auto const opts = parse(argc, argv, cmd);

if (opts.has<'h'>()) {
    std::cout << usage(cmd);
    return 0;
}

// has()でサブコマンドが指定されたかどうかを得ます。
if (opts.has<'H'>()) {
    // get()でサブコマンドのオプションマップを得ます。
    auto const helloOpts = opts.get<'H'>();

    std::cout << "Hello, " << helloOpts.get<'n'>() << "!\n";
}
else if (opts.has<'A'>()) {
    auto const addOpts = opts.get<'A'>();
    std::cout << addOpts.get<'l'>() + addOpts.get<'r'>() << "\n";
}

4. 実行結果

$ g++ -std=c++14 -o subcmd subcmd.cpp

$ ./subcmd
subcmd: command required

$ ./subcmd --help
Usage: subcmd [OPTION...] COMMAND [ARG...]

Options:
  -h  --help  produce help message

Commands:
  hello  hello command
  add    add command

$ ./subcmd hello world
Hello, world!

$ ./subcmd add 3 4
7

その他の使い方

READMEを参照下さい。

補足

間違った使い方をすると、エラーメッセージが地獄になります。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9