【メモ】C#でCLIコマンドを作成するためのライブラリ「EntryPoint」の使い方

C#でバッチ処理作る機会があり、ベースになるフレームワークがないかと探していたら`EntryPoint`というライブラリを見つけました。
CLIコマンドを保守する上でとても便利なライブラリだと感じました。ですが、日本語の記事が見つからなかったので実装例をメモしておきます。

Nick-Lucas/EntryPoint
https://github.com/Nick-Lucas/EntryPoint

# 環境

```bash
$ dotnet --version
3.1.200
```

手元にwindowsがなくmacを使用しました。基本的にwindowsでも同じだと思います。
（前はwindwsでビルドしました。）

# プロジェクト作成

```bash
$ dotnet new console -n QiitaEntryPoint -o QiitaEntryPoint
cd QiitaEntryPoint
dotnet add package EntryPoint
```

QiitaEntryPointというプロジェクトを作成し、EntryPointをnugetでインストールします。

# ディレクトリ構成

```bash
$ tree
.
├── CommandLine
│   ├── CliCommands.cs
│   ├── Primary
│   │   ├── PrimaryCliCommand.cs
│   │   └── PrimaryCliCommandArgs.cs
│   └── Secondary
│       └── SecondaryCliCommand.cs
├── Program.cs
└── QiitaEntryPoint.csproj
```

最終的には上記のようになります。
サンプルコードを参考にしつつ、コマンドの処理が膨らんでもファイルを作りやすいようにコマンドごとにディレクトリを切りました。

# 実装

今回はPrimaryコマンドとSecondaryコマンドを用意しました。

### Primaryコマンド

Primaryコマンドの処理内容となります。
コマンドライン引数と対応したクラス「PrimaryCliCommandArgs」を引数に取ります。

```PrimaryCliCommand.cs
using System;

namespace QiitaEntryPoint.CommandLine.Primary
{
    public class PrimaryCliCommand
    {
        public void Handle(PrimaryCliCommandArgs args)
        {
            if (args.AddDay)
            {
                Console.WriteLine($"Hello. {args.Message} by {DateTime.Today.ToShortDateString()}");
            }
            else
            {
                Console.WriteLine(args.Message);
            }
        }
    }
}
```

こちらはPrimaryコマンドのコマンドライン引数と対応したクラスです。
「BaseCliArguments」を継承します。

```PrimaryCliCommandArgs.cs
using EntryPoint;

namespace QiitaEntryPoint.CommandLine.Primary
{
    public class PrimaryCliCommandArgs : BaseCliArguments
    {
        public PrimaryCliCommandArgs() : base("Primary Command") { }

        [Required]
        [OptionParameter(ShortName: 'm',
                         LongName: "message")]
        [Help("Output message")]
        public string Message { get; set; }

        [Option(ShortName: 'd',
                LongName: "day")]
        [Help("add execute day")]
        public bool AddDay { get; set; }
    }
}
```

下記のコマンドで`Message`に対して"I am Engineer"が`AddDay`に対して"true"が入ってくるイメージです。

```bash
$ dotnet run primary -- -m "I am Engineer" -d
```

`[Required]`を使用することでその引数は必須になります。

```bash
$ dotnet run primary

Arguments Error: 
The option -m/--message was not included, but is a required option
```

また、LongNameで引数名を指定して、ShortNameで引数の省略形を設定できます。
他にもいろいろ機能があるので以下のサンプルコードを参考にすると良いです。直感的に理解できると思います。

https://github.com/Nick-Lucas/EntryPoint/blob/master/test/Example/CommandLine/PrimaryCliArguments.cs

ヘルプを表示するとこんな感じです。

```bash
$ dotnet run primary -- --help
Primary Command v1 Documentation

   Usage:
   QiitaEntryPoint [ -o | --option ] [ -p VALUE | --parameter VALUE ] [ operands ]


Arguments:

   -d --day 
   add execute day

   -h --help 
   Displays Help information about arguments when set

   -m --message [String] 
   REQUIRED
   Output message
```

### Secondaryコマンド

Secondaryコマンドは引数を受け取らないシンプルなコマンドとします。

```SecondaryCliCommand.cs
using System;

namespace QiitaEntryPoint.CommandLine.Secondary
{
    public class SecondaryCliCommand
    {
        public void Handle()
        {
            Console.WriteLine("Called secondaryCliCommand");
        }
    }
}
```

### 処理の振り分け

このクラスは各コマンドのファサードになっており、各コマンドの処理へと振り分けを行います。
「BaseCliCommands」を継承します。

```CliCommands.cs
using EntryPoint;
using QiitaEntryPoint.CommandLine.Primary;
using QiitaEntryPoint.CommandLine.Secondary;

namespace QiitaEntryPoint.CommandLine
{
    public class CliCommands : BaseCliCommands
    {
        [DefaultCommand]
        [Command("primary")]
        [Help("The Main command")]
        public void Primary(string[] args)
        {
            var options = Cli.Parse<PrimaryCliCommandArgs>(args);
            var command = new PrimaryCliCommand();
            command.Handle(options);
        }

        [Command("secondary")]
        [Help("The Secondary command")]
        public void Secondary(string[] args)
        {
            var command = new SecondaryCliCommand();
            command.Handle();
        }
    }
}
```

ここも直感的ですが、`CommandAttribute`を設定することで、コマンドと処理の紐付けが行われます。
`[DefaultCommand]`を設定すると、コマンド名を省略したときのデフォルト操作を指定することができます。

`Cli.Parse<PrimaryCliCommandArgs>(args);`を実行することで、型引数にコマンドライン引数をパースしたインスタンスを取得できます。

ヘルプを表示するとこんな感じです。

```bash
$ dotnet run -- --help
Commands for QiitaEntryPoint

Usage:
QiitaEntryPoint [COMMAND] [COMMAND ARGUMENTS]

For Command Help:
QiitaEntryPoint [COMMAND] --help

   PRIMARY [DEFAULT]
   The Main command

   SECONDARY
   The Secondary command
```

### Main関数

最後にエントリーポイントを実装します。

```Program.cs
using EntryPoint;
using QiitaEntryPoint.CommandLine;

namespace QiitaEntryPoint
{
    class Program
    {
        static void Main(string[] args)
        {
            Cli.Execute<CliCommands>(args);
        }
    }
}
```

# 実行

3つのパターンで実行してみます。

```bash
$ dotnet run -- -m "I am Engineer" -d
Hello. I am Engineer by 2020/11/02

$ dotnet run primary -- -m "I am Engineer"
I am Engineer

$ dotnet run secondary
Called secondaryCliCommand
```

# 最後に

バッチ処理の実装方法はいろいろあると思いますが、こちらのライブラリを使用することで初見にも優しいソースコードになると思います。（CommandAttributeから追えば良いので）
また、CLIコマンド特有のヘルプ処理などをAttributeで切り出すことにより、本質的な実装に集中できるとともにクリーンなコードを書くことができると感じました。

今回使用したソースコードです。
https://github.com/ishiyama0530/qiita-EntryPoint