28
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

C#とF#向けコマンドラインパーサーCommandLineParserの紹介

はじめに

C#でコンソールアプリを作成する場合、少し凝ったことをするとなると、引数の処理というのはどうしても必要になる。
この手のパッケージはnugetを探すと色々見つかるが、その中でもパターンマッチングっぽく使えるCommandLineParserについて書く。

CommandLineParser

特徴

F#で使われることも考慮しているためか、パターンマッチのように書けるAPIになっている。
後のコンセプトとしては、細かいことを考えずに使える事を重視しているとのこと。
公式wiki

基本的な使い方

参照は NuGetパッケージ

オプションクラスの定義

オプションを格納するための器として、クラスを定義する。
以下のようなクラスを定義する。

// using CommandLine;
enum Hoge
{
    X, Y, Z
}
class Options
{
    // 基本的な形式
    [Option('a', "aaa", Required = false, HelpText = "AAAA")]
    public string A { get; set; }
    // プリミティブ型であれば、string以外でも受け取ることが可能
    [Option('b', "bbb", Required = false, HelpText = "BBBB")]
    public bool B { get; set; }
    // 複数の値を受け取ることが可能。区切り文字はSeparatorで指定
    [Option('c', "ccc", Separator = ',')]
    public IEnumerable<string> C { get; set; }
    // enumを受け取ることも可能(指定にはenumの名前を指定する)
    [Option('d', "ddd")]
    public Hoge D { get; set; }
    // オプション以外の引数を受け取るための属性
    [Value(1, MetaName = "remaining")]
    public IEnumerable<string> Remaining { get; set; }
}

上記のようにクラスに属性を指定していく。
また、別途F#用パッケージをインストールすると、F#のOption型にも対応できるらしい

Option属性

いわゆる-a [value]--aaa [value]などで受け取るための属性。
以下のようにプロパティに付与する

// 第一引数がショート形式、第二引数がロング形式
// 第一、第二引数は省略可
[Option('a', "aaa", Description = "option A", Required = false)]
public string AAA { get; set; }

これで、-a [value]または--aaa [value]という形で引数を受け取ることができる。
Requiredは引数が必須かどうかを示し、Descriptionはヘルプ表示の時に使用される。
また、受け取る型はstringの他、intdoubleのプリミティブ型、または任意のenum型が使える。
更に、IEnumerable<T>も指定することができ、この場合はSeparatorを設定すれば、任意の区切り記号で複数の値を受け取ることが可能。Separator = ','とすれば、-a a,b,cと指定ができる。

Value属性

オプション形式ではない引数を受け取るための属性。
オプション以外の引数の位置指定が必須(0開始)。

// MetaNameはヘルプ時に表示される
[Value(0, MetaName = "XValue")]
public string X { get; set; }

なお、ここでIEnumerable<string>を型に指定すれば、"オプション以外の全ての引数"の格納先にすることもできる。

Usage属性

ヘルプ出力時、使用例を末尾に出力することができる。

Verb属性

いわゆるdocker psみたいなサブコマンド。クラスに付与する。
以下のような感じ。

// using CommandLine;
[Verb("sub")]
class MySubCommand
{
    [Option(...)]
    public string X { get; set; }
}

第一引数に名前を指定するが、これがそのままコマンド名になる。

ただし、サブコマンドのサブコマンド(docker image lsのようなもの)は作成できないという制限がある。
まあ、この辺りが必要になる段階というのは結構後なので、余り気にすることはないかもしれない。
また、issue見る限り要望はあるっぽいので、PRを送れば採用されるかもしれない。

パース(サブコマンド無し)

さて、クラスを定義したら、実際に引数のパースを行う。
サブコマンドを使わない場合は下記のように行う。

// using CommandLine;
// using CommandLine.Text;

Parser.Default.ParseArguments<Options>(args)
    .WithParsed(opt => {/*パースに成功した場合*/})
    .WithNotParsed(er => {/*パースに失敗した場合*/});

With...Action<T>形式のみ扱うため、例えばint型の戻り値を返したい場合や、Task<T>等の非同期を行いたい場合は、
以下のように処理する。

// 非同期でint値を返す
await Parser.Default.ParseArguments<Options>(args)
    .MapResult(
        // 成功した場合
        async opt =>
        {
            // 何かの非同期処理
            await Task.Yield();
            return 0;
        }
        // 失敗した場合
        async er =>
        {
            // 何かの非同期処理
            await Task.Yield();
            return -1;
        }
    );

注意として、全ての戻り値の型は揃える必要がある。

なお、Parser.ParseArguments<T>(args)の結果をParserResult<T>にキャストして結果を見て、更にそこから
成功時はParsed<T>、失敗時はNotParsed<T>にキャストすることで、処理を行うことが可能

var result = (ParserResult<Options>)Parser.Default.ParseArguments<Options>(args);
if(result.Tag == ParserResultType.Parsed)
{
    // パース成功時
    var parsed = (Parsed<Options>)result;
    // 処理
}
else
{
    // パース失敗時
    var notParsed = (NotParsed<Options>)result;
    // 処理
}

パース(サブコマンドあり)

サブコマンドがある場合でも、ない場合と比べてあまり違いはないが、
複数のサブコマンドがある場合は以下のようになる。

// MySubCommandとMySubCommand2があるとする
Parser.Default.ParseArguments<MySubCommand, MySubCommand2>(args)
    .WithParsed<MySubCommand>(opt1 => { /**/ })
    .WithParsed<MySubCommand2>(opt2 => { /**/ })
    .WithNotParsed(er => { /**/ })
    ;

MapResultを使う場合は以下のようになる。

Parser.Default.ParseArguments<MySubCommand, MySubCommand2>(args)
    .MapResult(
        (MySubCommand opt1) => { /**/ }),
        (MySubCommand2 opt2) => { /**/ }),
        er => { /**/ }
    );

上記を見てもらうとわかるかもしれないが、実際サブコマンドが増えてくるとちょっと厳しい書き方ではある。

ヘルプテキスト

大方のコマンドラインツールというものは、エラーがあった場合は、コンソールにヘルプを出力して終了という動作になる場合が多い。
CommandLineParserでは、With...あるいはMapResult時にエラーがあれば、標準エラー出力に出力という動作を
暗黙的に行っている。

楽さを考えるならばそれでも問題ないが、例えばエラーメッセージを独自形式で出力したり、抑制したい場合もあるだろう。
そういう場合は、CommandLine.Parser.Defaultを使うのではなく、CommandLine.Parserを自分で生成して、
設定変更をする。

// using CommandLine;
// using CommandLine.Text;
using(var parser = new Parser((setting) => setting.HelpWriter = null))
{
    var parsed = parser.ParseArguments<Options>(args);
    parsed.WithNotParsed(er =>
        {
            // パース結果からデフォルトの文を生成したい場合は、HelpText.AutoBuildを使用する
            var helpText = HelpText.AutoBuild(parsed);
            // 生成後にhelpText = helpText.Add...で追加記述も可能
            Console.WriteLine($"parse failed: {helpText}");
        });
    // 処理...
}

注意点

デフォルトで--help--versionオプションが設定されており、これらを指定するとエラー扱い(HelpRequestedError、VersionRequestedError等)になる。
現在の所これを回避する手段はないので、自前でエラー内容を見て判断するしかない。
なお、この件に関する制御オプションが提案されており、現在PR中であるマージされており、最新バージョン(2.4.0以降)で回避方法が利用可能

終りに

APIはもちろん、実装の方も中々F#魂を感じさせるような造りのライブラリだった。結構この辺りは好みが出てくるところだと思う。
今回紹介したCommandLineParserは、機能こそ他のライブラリに一歩譲るものの、簡単さと書き方で個人的には気に入っている。
コマンドラインの引数処理というのは、よくある処理だが面倒というのは確かなので、少しでも役に立ってくれれば幸い。

その他のコマンドラインパーサー

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
28
Help us understand the problem. What are the problem?