Edited 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は、機能こそ他のライブラリに一歩譲るものの、簡単さと書き方で個人的には気に入っている。

コマンドラインの引数処理というのは、よくある処理だが面倒というのは確かなので、少しでも役に立ってくれれば幸い。


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