はじめに
今回はCocona
というOSSを紹介したいと思います。
概要
よくある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
![](https://cdn-ak.f.st-hatena.com/images/fotolife/h/hanaaaaaachiru/20240531/20240531184847.png)
コマンドでは以下の通り。
$ 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
を定義します。(ICommandParameterSet
をclass
に継承されてもいけます)
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
Options
やArguments
の値が正しい値か検証することができます。
// --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