0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コマンドラインのパースに CLI11 を使ってみた

0
Posted at

TL;DR

CLI11っていうコマンドラインパーサはべんりであります。

#include <CLI/CLI.hpp>
#include <iostream>
#include <string>

int main(int argc, char** argv) {
  CLI::App app{"Example CLI11 Application"};

  int count = 0;
  app.add_option("-c,--count", count, "An integer count")->required();

  std::string name;
  app.add_option("-n,--name", name, "A name string")
      ->default_val("default_name");

  bool flag = false;
  app.add_flag("-f,--flag", flag, "A boolean flag");

  try {
    app.parse(argc, argv);
  } catch (const CLI::ParseError& e) {
    return app.exit(e);
  }

  std::cout << "Count: " << count << "\n";
  std::cout << "Name: " << name << "\n";
  std::cout << "Flag: " << (flag ? "true" : "false") << "\n";

  return 0;
}

車輪は再発明したくないのです

ある日、私は開発用のマシンをもう1台用意しようと思い立ちました。で、15年ぶりくらいにWindowsマシンを購入したのです。そのマシンをセットアップしたり開発環境を整えたりしていて、普段使っているMacを数日間起動しなかったのです。

すると!なんと、このMac、ご機嫌を損ねたようで、2度と起動してくれません。それなりにデータもあったのになあ。ラ○オの音声データとかさっ。

失ったものの中に、オーディオデータをコマンドラインで処理する小さなツールが有りました。ほとんどのコードはGithubにあげてあったのですが、このツールの完全なコードがありません。しょうがない、作るか!

持っていたコードの断片が、なんと、MSVCではそのままコンパイルできません。getoptがないそうで。いやいや、getoptを再発明しちゃだめでしょ。ならば、もっとモダンで高機能なパーサを探すたびに出ましょうか!

CLI11に出会う

で、見つけたのがCLI11というC++用のコマンドラインパーサです。GitHubのリポジトリは
こちら

このパーサ、C++11以降に対応していて、ヘッダオンリーで使えるのでとても便利です。オプションの定義も簡単で、デフォルト値の設定や必須オプションの指定も楽々です。Googleとかで検索してみても、あまり日本語の情報が見つからないように思います。ということで、私が使った範囲で紹介していこうと思います。

基本の使い方

上のコードは、CLI11を使った簡単なコマンドラインアプリケーションの例です。以下に主要な部分を説明します。

  1. CLI::App app{"Example CLI11 Application"};
    ここでアプリケーションのインスタンスを作成します。引数にはアプリケーションの説明を渡せます。
  2. app.add_option("-c,--count", count, "An integer count")->required;
    ここで整数型のオプション--countを定義しています。 このオプションは必須で、指定されなかった場合はエラーになります。
  3. app.add_option("-n,--name", name, "A name string")->default_val("default_name");
    ここでは文字列型のオプション--nameを定義し、デフォルト値をdefault_nameに設定しています。指定されなかった場合、この値が使われます。
  4. app.add_flag("-f,--flag", flag, "A boolean flag");
    ここでブール型のフラグ--flagを定義しています。指定された場合、flag変数がtrueになります。
  5. app.parse(argc, argv);
    ここでコマンドライン引数を解析します。解析中にエラーが発生した場合は、CLI::ParseError例外がスローされます。

実行例:

$ ./my_app --count 5 --name "test_name" --flag

サブコマンドの追加

CLI11はサブコマンドの定義もサポートしています。例えば、以下のようにサブコマンドを追加できます。

CLI::App* subcmd = app.add_subcommand("sub", "A subcommand example");
int sub_value = 0;
subcmd->add_option("-v,--value", sub_value, "An integer value for subcommand");

この例では、subというサブコマンドを追加し、その中に整数型のオプション--valueを定義しています。

実行例:

$ ./my_app sub --value 10

さらにサブコマンドも追加できます。

CLI::App* nested_subcmd = subcmd->add_subcommand("nested", "A nested subcommand example");
std::string nested_name;
nested_subcmd->add_option("-n,--name", nested_name, "A name for nested subcommand");

この例では、subサブコマンドの中にさらにnestedというサブコマンドを追加しています。

実行例:

$ ./my_app sub nested --name "nested_name"

オプションはクラス内にあるんですよ

こんなコードがあります。

class Options {
 private:
  std::string name;
  int count;
  bool flag;

 public:
  // ゲッター
  std::string getName() const { return name; }
  int getCount() const { return count; }
  bool getFlag() const { return flag; }

  // セッター
  void setName(const std::string& n) { name = n; }
  void setCount(int c) { count = c; }
  void setFlag(bool f) { flag = f; }
};

このクラスのメンバに値をセットするとき、これまで行ってきたような方法ではうまくいきません。CLI11は直接変数にアクセスするので、クラスのメンバ関数を使うことができないのです。

この場合、ラムダ関数を使ってセッターを呼び出す方法があります。以下に例を示します。

int main(int argc, char** argv) {
  CLI::App app{"Example CLI Application"};
  Options options;
  // オプションの追加
  app.add_option(
      "-n,--name",
      [&options](const CLI::results_t& res) {
        if (!res.empty()) options.setName(res[0]);
        return true;
      },
      "Set the name");
  app.parse(argc, argv);
}

しかし、問題があります。ラムダ関数の引数にはコマンドライン文字列が渡されるだけなので、この例のようにstd::stringならばそのまま代入できますが、intboolの場合は変換が必要です。直接変数に代入していたときにはCLI11が自動的に行ってくれていた変換を、自分で実装しなければならないのです。

これはなんとかならないものかと調べてみたら、下のように書くことができることがわかりました。

app.add_option_function<std::string>(
       "-n,--name",
       [&options](const std::string& res) {
          options.setName(res);
         return true;
       },
       "Set the name");

intならば、

app.add_option_function<int>("-c,--count", [&options](int res) {
  options.setCount(res);
  return true;
},
"Set the count");

みたいになります。これならば、型変換もCLI11がやってくれますので、楽ちんです。

そうなんだけどさっ

ふと思いました。

OptionsクラスがCLI11をプライベートメンバとして持って、Parse(int argc, char* argv[])みたいなメソッドを公開してやれば、最初にやったような簡単な書き方で目的を達成できるんじゃないか?そうすると

int main() {
    Options options;
    options.Parse(argc, argv);
}

みたいにできるんじゃないかと。こうすれば、ラムダ式を頑張って書かなくても良いし、型変換もちゃんとやってくれるし、可読性も良くなるし。

では、サンプルを場。

#include <CLI/CLI.hpp>
#include <iostream>
#include <string>

class Options {
 public:
 int Parse(int argc, char** argv) {
    app.add_option("-n,--name", name, "Name option");
    app.add_option("-c,--count", count, "Count option");
    app.add_flag("-f,--flag", flag, "Flag option");
    app.parse(argc, argv);
    return 0;
  }
  // ゲッター
  std::string getName() const { return name; }
  int getCount() const { return count; }
  bool getFlag() const { return flag; }

  // セッター
  void setName(const std::string& n) { name = n; }
  void setCount(int c) { count = c; }
  void setFlag(bool f) { flag = f; }
 private:
  std::string name;
  int count;
  bool flag;
  CLI::App app{"Options Manager"};
};

int main(int argc, char** argv) {
    Options options;
    options.Parse(argc, argv);
    
    std::cout << "Name: " << options.getName() << "\n";
    std::cout << "Count: " << options.getCount() << "\n";
    std::cout << "Flag: " << (options.getFlag() ? "true" : "false") << "\n";
    
    return 0;
}

はい、ちゃんとできましたね。

まとめ

CLI11はC++でコマンドライン引数を扱うのに非常に便利なライブラリじゃないかと思います。導入については触れませんでしたが、私はCMake+vcpkgで導入しました。

ということで、これからオーディオデータを処理するツールを再構築していこうと思います。

0
2
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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?