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

# はじめに

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

# CommandLineParser

## 特徴

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

## 基本的な使い方

参照は [NuGetパッケージ](https://www.nuget.org/packages/commandlineparser) で

### オプションクラスの定義

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

```csharp
// 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#用パッケージ](https://www.nuget.org/packages/CommandLineParser.CSharp/)をインストールすると、**F#のOption型にも対応できるらしい**

#### `Option`属性

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

```csharp
// 第一引数がショート形式、第二引数がロング形式
// 第一、第二引数は省略可
[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開始)。

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

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

#### `Usage`属性

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

#### `Verb`属性

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

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

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

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

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

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

```csharp
// using CommandLine;
// using CommandLine.Text;

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

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

```csharp
// 非同期で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>`にキャストすることで、処理を行うことが可能

```csharp
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;
    // 処理
}
```

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

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

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

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

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

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

## ヘルプテキスト

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

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

```csharp
// 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以降)で回避方法が利用可能](https://github.com/commandlineparser/commandline/pull/256)。

# 終りに

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

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

* [Mono.Options](https://github.com/mono/mono/blob/master/mcs/class/Mono.Options/Mono.Options/Options.cs)
* [McMaster.Extensions.CommandLineUtils](https://github.com/natemcmaster/CommandLineUtils)