はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】Coconaを利用してSystem.CommandLineなどを利用せずに簡単にConsoleアプリケーションを作成する

概要

よくあるSystem.CommandLineを利用してコンソールアプリケーションを作る例を書いてみました。
learn.microsoft.com

// System.CommandLineを利用した例
public class Program
{
    public static async Task<int> Main(string[] args)
    {
        // dotnet run --name Sato --age 18
        RootCommand rootCommand = new RootCommand();
        var nameOption = new Option<string>("--name");
        var ageOption = new Option<int>("--age");
        rootCommand.Add(nameOption);
        rootCommand.Add(ageOption);

        rootCommand.SetHandler((name, age) =>
        {
            Console.WriteLine($"Name : {name}, Age : {age}");
        }, nameOption, ageOption);
        
        return await rootCommand.InvokeAsync(args);
    }
}
$ dotnet run --name Sato --age 18
Name : Sato, Age : 18


Coconaを利用することで以下のように短く直感的なコードでコンソールアプリケーションを構築できます。

// dotnet run --name Sato --age 18
CoconaApp.Run((string name, int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run --name Sato --age 18
Name : Sato, Age : 18


かなり短くなりました。C# 9で導入された最上位レベルのステートメントを利用すれば本当に数行です。
最上位レベルのステートメント - Main メソッドを使用しないプログラム - C# | Microsoft Learn

Micro-framework for .NET Core console application. Cocona makes it easy and fast to build console applications on .NET.🚀

// DeepL翻訳
.NET Coreコンソール・アプリケーションのためのマイクロ・フレームワーク。Coconaは、.NET上でコンソール・アプリケーションを簡単かつ高速に構築できる。

ちなみにCySharpさんでもConsoleAppFrameworkという似たOSSを出しているようですね。最近v5がでてました。v4までとの互換性はないみたいです。
github.com

インストール

NuGetからインストールできます。
www.nuget.org

RiderでNuGetからインストールしている様子

コマンドでは以下の通り。

$ dotnet add package Cocona

# Microsoft.Extensions.LoggingやMicrosoft.Extensions.DependencyInjectionなどのMicrosoft.Extensions.*への依存がない軽量版
$ dotnet add package Cocona.Lite

使い方

基本

コマンドが一つの場合と複数の場合の書き方は以下の通りです。

// コマンドが一つだけの場合
// dotnet run --name Sato --age 18
CoconaApp.Run((string name, int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run --name Sato --age 18
Name : Sato, Age : 18


// コマンドが複数の場合
var app = CoconaApp.Create();

// dotnet run add --name Sato
app.AddCommand("add", (string name) =>
{
    Console.WriteLine($"Add : {name}");
});
        
// dotnet run delete --name Sato
app.AddCommand("delete", (string name) =>
{
    Console.WriteLine($"Delete : {name}");
});

app.Run();
$ dotnet run add --name Sato
Add : Sato

$ dotnet run delete --name Sato
Delete : Sato

Optionsについて

上記の書き方だとnameというコマンドラインオプションは必須になっていますが、任意にしたい場合はstring?のようにnullableにします。

// コマンドが一つだけの場合
// dotnet run --age 18
CoconaApp.Run((string? name, int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run --age 20
Name : , Age : 20

また--nameだけでなく-nのようなショートネームも利用できるようにするには[Option]をつけます。

CoconaApp.Run(([Option("n")]string name, [Option("a")]int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run --n Sato --a 18
Name : Sato, Age : 18

Argumentsについて

--hogeのようなものを使わずコマンドライン引数から値を受け取るには[Argument]をつけます。

// dotnet run Sato 19
CoconaApp.Run(([Argument]string name, [Argument]int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run Sato 19
Name : Sato, Age : 19

Sub-commands

複数コマンドがあるものはSub-commandsといいます。実はネストできたりもします。

var app = CoconaApp.Create();

// dotnet run add dog --name Shiba
app.AddSubCommand("add", x =>
{
    x.AddCommand("dog", (string name) => Console.WriteLine($"Dog : {name}"));
    x.AddCommand("cat", (string name) => Console.WriteLine($"Cat : {name}"));
});

app.Run();
$  dotnet run add dog --name Shiba
Dog : Shiba

パラメーターの共通化

複数のコマンドで共通したパラメーターを持たせたい場合はICommandParameterSetを実装したrecordを定義します。(ICommandParameterSetclassに継承されてもいけます)

var app = CoconaApp.Create();

// dotnet run add --name Sato --age 18
app.AddCommand("add", (CommonParameters common) =>
{
    Console.WriteLine($"Add : {common.Name}({common.Age})");
});

app.AddCommand("delete", (CommonParameters common) =>
{
    Console.WriteLine($"Delete : {common.Name}({common.Age})");
});

app.Run();

// パラメーターの共通化
public record CommonParameters(
    [Option]
    string Name,
    [Option]
    int Age
) : ICommandParameterSet;

Validation

OptionsArgumentsの値が正しい値か検証することができます。

// --name が 10文字以下でないとエラーになる
// --age が 20~100 でないとエラーになる
CoconaApp.Run(([StringLength(10)]string name, [Range(20, 100)] int age) =>
{
    Console.WriteLine($"Name : {name}, Age : {age}");
});
$ dotnet run --name Sato --age 18
エラー: The field age must be between 20 and 100.
$ dotnet run --name Aaaaaaaaaaaaaaaaaaaaa --age 22
エラー: The field name must be a string with a maximum length of 10.

どうやらSystem.ComponentModel.DataAnnotationsにある属性が使えるみたいです。
learn.microsoft.com

Microsoft.Extensions.*の利用

Logging

なんとMicrosoft.Extensions.Loggingに対応してます。
learn.microsoft.com

var builder = CoconaApp.CreateBuilder();

// DebugLogger
// builder.Logging.AddDebug();

// ConsoleLogger
// builder.Logging.AddConsole();

// 構造化ログ
builder.Logging.AddJsonConsole();

var app = builder.Build();

// {"EventId":0,"LogLevel":"Information","Category":"Program","Message":"Hello Konnichiwa!","State":{"Message":"Hello Konnichiwa!","{OriginalFormat}":"Hello Konnichiwa!"}}
app.AddCommand((ILogger<Program> logger) => logger.LogInformation("Hello Konnichiwa!"));

app.Run();
DI

加えてMicrosoft.Extensions.DependencyInjectionも対応してます。
learn.microsoft.com

// Builderを用意
CoconaAppBuilder builder = CoconaApp.CreateBuilder();

// HogeServiceをDIコンテナに登録
builder.Services.AddTransient<HogeService>();

// CoconaAppの生成
CoconaApp app = builder.Build();

// 依存が注入される
app.AddCommand((HogeService service) =>
{
    service.Execute("Hello, World!");
});

app.Run();

class HogeService
{
    public void Execute(string message)
    {
        Console.WriteLine(message);
    }
}

さいごに

まだ紹介しきれてない機能がたくさんあるので、気になる方はReameを参照してみてください。
https://github.com/mayuki/Cocona