C#のCUIアプリケーション用ライブラリ『CuiLib』

C#でCUIアプリケーションを制作する際に役立つライブラリを作成したので紹介します。
C#で軽いスクリプトなどを書く機会が多々あったので作りました。
勿論大規模なソフトウェアに組み込むことも可能です。

## ライブラリ情報

- 名称: CuiLib
- 使用言語: C#
- Nuget: https://www.nuget.org/packages/CuiLib
- GitHub: https://github.com/Funny-Silkie/CuiLib
- 対象バージョン: .NET7.0
- ライセンス: MIT

## 機能

- コマンドを表す `Command` クラスの実装
  - サブコマンドも実装可能
  - `OnExecution()`（同期）または`OnExecutionAsync()`（非同期）メソッドをオーバーライドすることでコマンドの処理を記述
  - ヘルプメッセージの出力を実装
- 引数解析の実装
  - オプションを表す `Option<T>` クラスとオプション以外の値を表す `Parameter<T>` （パラメータ）クラスの定義
  - オプションの名前は `-n` と `--name` の2種類を定義可能
  - `tar -czvf FILE` のような複数のフラグ+値を表す短いオプションの組み合わせの読み取りも可能
  - `ValueConverter<TIn, TOut>` の実装によって文字列からの値（オプション・パラメータ）の変換をカスタマイズ可能
  - `ValueChecker<T>` の実装によって値のチェック＆例外スローをカスタマイズ可能
- OS依存なし（普通はそう）

とご覧になってお分かりの通り，引数の解析がメインです。
`Main` メソッドに与えられる `string[] args` を解析して実行すべき適切なコマンドを選択し，オプションとパラメータの値を与えた上で処理を実行します。

## サンプルコード

文章でうだうだ述べるよりかはサンプルを見たほうが分かりやすいと思うので幾つか置いておきます。

何れのサンプルも以下の想定です。
- NuGetでライブラリをインストール
- アセンブリ名は `Sample`，出力実行ファイル名は `Sample.exe`
- 実行環境はWindows10

### サブコマンド無し，パラメータ取得

名前をパラメータとして入力すると最初の名前について「[一人目], you're the first!」と出力し，二つ目以降の名前をその他として出力するサンプルプログラム。

```cs
using CuiLib;
using CuiLib.Commands;
using CuiLib.Log;
using CuiLib.Options;

internal class MainCommand : Command
{
    // ロガー
    private readonly Logger logger;

    // オプション
    private readonly FlagOption optionHelp;

    // パラメータ
    private readonly Parameter<string> paramFirst;
    private readonly Parameter<string> paramOthers;

    public MainCommand() : base("Sample")
    {
        logger = new Logger()
        {
            ConsoleStdoutLogEnabled = true, // 標準出力にリダイレクト
        };
        Description = "サンプルコマンドです";

        // -hまたは--helpのフラグオプション
        optionHelp = new FlagOption('h', "help")
        {
            Description = "ヘルプを出力します",
        };

        // オプションの登録
        Options.Add(optionHelp);

        // 最初の一つのパラメータの値
        paramFirst = Parameter.Create<string>("1st", 0);
        paramFirst.Description = "最初の値";
        // 二つ目以降のパラメータの値
        paramOthers = Parameter.CreateAsArray<string>("others", 1);
        paramOthers.Description = "二つ目以降の値";

        // パラメータの登録
        Parameters.Add(paramFirst);
        Parameters.Add(paramOthers);
    }

    protected override void OnExecution()
    {
        // フラグはValueプロパティで指定されたか否かを表す
        if (optionHelp.Value)
        {
            WriteHelp(logger);
            return;
        }

        // 一つの値を取るパラメータはValueプロパティから値を取得
        string first = paramFirst.Value;
        // 複数の値を取るパラメータはValuesプロパティから値を取得
        string[] others = paramOthers.Values ?? Array.Empty<string>();

        Console.WriteLine($"{first}, you're the first!");
        Console.WriteLine("--others--");
        Console.WriteLine($"{string.Join(", ", others)}");
    }

    private static void Main(string[] args)
    {
        var command = new MainCommand();

        try
        {
            // コマンドの実行
            command.Invoke(args);
        }
        // 引数解析エラー時にスロー
        catch (ArgumentAnalysisException e)
        {
            Console.Error.WriteLine("引数解析エラー！");
            Console.Error.WriteLine(e.Message);
        }
    }
}
```

**実行例**
```shell-session
> .\Sample.exe -h
Sample

Description:
サンプルコマンドです

Usage:
Sample [-h] <1st> <others ..>

Options:
  -h, --help  ヘルプを出力します
Parameters:
     1st  最初の値
  others  二つ目以降の値
> .\Sample.exe takahashi suzuki sato
takahashi, you're the first!
--others--
suzuki, sato
```

### サブコマンド無し，オプション取得

0から指定した値までの自然数を列挙するサンプルプログラムです。
`--max` オプションで最大値（ここでは1-30）を，`-o` or `--out` オプションで出力先ファイル（無指定の場合は標準出力）を指定します。

```cs
internal class MainCommand : Command
{
    // ロガー
    private readonly Logger logger;

    // オプション
    private readonly FlagOption optionHelp;
    private readonly SingleValueOption<TextWriter?> optionOut;
    private readonly SingleValueOption<int> optionMax;

    public MainCommand() : base("Sample")
    {
        logger = new Logger()
        {
            ConsoleStdoutLogEnabled = true, // 標準出力にリダイレクト
        };
        Description = "サンプルコマンドです";

        // -hまたは--helpのフラグオプション
        optionHelp = new FlagOption('h', "help")
        {
            Description = "ヘルプを出力します",
        };
        // -oまたは--outのオプション
        optionOut = new SingleValueOption<TextWriter?>('o', "out")
        {
            Description = "出力先のテキストファイル\n無指定で標準出力",
            DefaultValue = null, // DefaultValueで既定値を設定
        };
        // --maxのオプション
        optionMax = new SingleValueOption<int>("max")
        {
            Description = "列挙する値の最大値（30以内の正の実数）",
            Required = true,
            // 値の条件はAND（＆）やOR（|）で結合可能
            Checker = ValueChecker.Larger(0) & ValueChecker.LowerOrEqual(30),
        };

        // オプションの登録
        Options.Add(optionHelp);
        Options.Add(optionOut);
        Options.Add(optionMax);
    }

    protected override void OnExecution()
    {
        // フラグはValueプロパティで指定されたか否かを表す
        if (optionHelp.Value)
        {
            WriteHelp(logger);
            return;
        }

        // TextWriterのオプションは指定したファイルへ出力するインスタンスを返す
        using TextWriter dstWriter = optionOut.Value ?? Console.Out;
        int max = optionMax.Value;

        for (int i = 0; i <= max; i++)
        {
            dstWriter.WriteLine(i);
        }
    }

    private static void Main(string[] args)
    {
        var command = new MainCommand();

        try
        {
            // コマンドの実行
            command.Invoke(args);
        }
        // 引数解析エラー時にスロー
        catch (ArgumentAnalysisException e)
        {
            Console.Error.WriteLine("引数解析エラー！");
            Console.Error.WriteLine(e.Message);
        }
    }
}
```

**実行例**
```shell-session
> .\Sample.exe -h
Sample

Description:
サンプルコマンドです

Usage:
Sample [-h] [-o file] --max int

Options:
  -h, --help  ヘルプを出力します
  -o, --out   出力先のテキストファイル
              無指定で標準出力
      --max   列挙する値の最大値（30以内の正の実数）
> .\Sample.exe
引数解析エラー！
オプション'--max'を入力してください
> .\Sample.exe --max 5
0
1
2
3
4
5
> .\Sample.exe --max 50
引数解析エラー！
値が30より大きいです
```

### サブコマンドあり

サブコマンド `cmdA` と `cmdB` を定義したサンプルプログラムです。

```cs
internal class MainCommand : Command
{
    // ロガー
    private readonly Logger logger;

    // オプション
    private readonly FlagOption optionHelp;

    public MainCommand() : base("Sample")
    {
        logger = new Logger()
        {
            ConsoleStdoutLogEnabled = true, // 標準出力にリダイレクト
        };
        Description = "サンプルコマンドです";

        // サブコマンド登録
        Children.Add(new CommandA());
        Children.Add(new CommandB());

        // -hまたは--helpのフラグオプション
        optionHelp = new FlagOption('h', "help")
        {
            Description = "ヘルプを出力します",
        };

        // オプションの登録
        Options.Add(optionHelp);
    }

    protected override void OnExecution()
    {
        // フラグはValueプロパティで指定されたか否かを表す
        if (optionHelp.Value)
        {
            WriteHelp(logger);
            return;
        }

        Console.Error.WriteLine("サブコマンドを指定してください");
    }

    private static void Main(string[] args)
    {
        var command = new MainCommand();

        try
        {
            // コマンドの実行
            command.Invoke(args);
        }
        // 引数解析エラー時にスロー
        catch (ArgumentAnalysisException e)
        {
            Console.Error.WriteLine("引数解析エラー！");
            Console.Error.WriteLine(e.Message);
        }
    }
}

// サブコマンド
internal class CommandA : Command
{
    // ロガー
    private readonly Logger logger;

    // オプション
    private readonly FlagOption optionHelp;

    public CommandA() : base("cmdA")
    {
        logger = new Logger()
        {
            ConsoleStdoutLogEnabled = true,
        };
        Description = "サブコマンドA";

        // -hまたは--helpのフラグオプション
        optionHelp = new FlagOption('h', "help")
        {
            Description = "ヘルプを出力します",
        };

        // オプションの登録
        Options.Add(optionHelp);
    }

    protected override void OnExecution()
    {
        // フラグはValueプロパティで指定されたか否かを表す
        if (optionHelp.Value)
        {
            WriteHelp(logger);
            return;
        }

        Console.WriteLine("サブコマンドAが実行されました");
    }
}

// サブコマンド
internal class CommandB : Command
{
    // ロガー
    private readonly Logger logger;

    // オプション
    private readonly FlagOption optionHelp;

    public CommandB() : base("cmdB")
    {
        logger = new Logger()
        {
            ConsoleStdoutLogEnabled = true,
        };
        Description = "サブコマンドB";

        // -hまたは--helpのフラグオプション
        optionHelp = new FlagOption('h', "help")
        {
            Description = "ヘルプを出力します",
        };

        // オプションの登録
        Options.Add(optionHelp);
    }

    protected override void OnExecution()
    {
        // フラグはValueプロパティで指定されたか否かを表す
        if (optionHelp.Value)
        {
            WriteHelp(logger);
            return;
        }

        Console.WriteLine("サブコマンドBが実行されました");
    }
}
```

**実行例**
```shell-session
> .\Sample.exe -h
Sample

Description:
サンプルコマンドです

Usage:
Sample [-h] [Subcommand]

Options:
  -h, --help  ヘルプを出力します
Subcommands:
  cmdA  サブコマンドA
  cmdB  サブコマンドB
> .\Sample.exe cmdA
サブコマンドAが実行されました
> .\Sample.exe cmdA -h
cmdA

Description:
サブコマンドA

Usage:
cmdA [-h]

Options:
  -h, --help  ヘルプを出力します
> .\Sample.exe cmdB
サブコマンドBが実行されました
> .\Sample.exe
サブコマンドを指定してください
```

## あとがき

質問や意見，改良などがありましたがGitHubでガンガンIssueなりPull Requestなりを飛ばしてくれると大変喜びます。
このライブラリがC#でCUIアプリを作りたい民のお役に立てば幸いです。