はじめに
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
の他、int
やdouble
のプリミティブ型、または任意の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は、機能こそ他のライブラリに一歩譲るものの、簡単さと書き方で個人的には気に入っている。
コマンドラインの引数処理というのは、よくある処理だが面倒というのは確かなので、少しでも役に立ってくれれば幸い。