【.NET】Console App Frameworkの基本的な使い方まとめ

Console App Frameworkの基本的な使い方をまとめました。

Console App Frameworkとは?

Console App Frameworkは、.NET環境でCLIのアプリケーション(Terminalとかから実行するアプリケーション)を作るためのフレームワークです。
実際に使ってみるとわかりますが、非常にお手軽にCLIを作ることができます。ふとした瞬間に作りたくなるCLIを簡単に作れるのはありがたいです。

github.com

Cysharpが開発しているOSSで、NuGetからインストールすることができます。
ライセンスはMITです。

www.nuget.org

基本的な使い方

簡単なCLIを作成・実行する

基本的な使い方は、以下の例のようにCLIのコマンドを定義したクラスの型をConsoleApp.Runの型引数に渡すだけです。

namespace Example;

internal sealed class Program
{
    private static void Main(string[] args)
    {
        // コマンドを定義したクラスを型引数に指定する
        ConsoleApp.Run<Command>(args);
    }
}

// コマンドを定義したクラス
internal sealed class Command : ConsoleAppBase // ConsoleAppBase
{
    [RootCommand] // RootCommand 属性を付ける
    public void Run(string arg1)
    {
        Console.WriteLine($"arg1: {arg1}");
    }
}

このアプリケーションをPublishすると以下のように実行できることを確認できます。
Path/To/Tool の部分は適宜書き換えてください)

$ Path/To/Tool --arg1 "Test"
arg1: Test
引数のオプションを設定する

また、以下のようにコマンドの引数に短い名前や説明をつけたり、デフォルト値を指定してオプショナル引数にすることができます。

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public void Run(
        [Option("a1", "first argument")] string arg1, // Optionアトリビュートで引数に短い名前や説明などをつけられる
        [Option("a2", "second argument")] int arg2 = 123 // デフォルト値を指定できる(指定するとオプショナル引数になる)
    )
    {
        Console.WriteLine($"arg1: {arg1}");
        Console.WriteLine($"arg2: {arg2}");
    }
}

helpコマンドも自動生成されるので、helpを見てみます。

$ Path/To/Tool --help
Usage: run [options...]

Options:
  -a1, --arg1 <String>    first argument (Required)
  -a2, --arg2 <Int32>     second argument (Default: 123)

引数の説明などが生成されていることを確認できました。

コマンドを複数作る

コマンドを複数作るには、ルートコマンドとは別にサブコマンドを定義します。
サブコマンドを作るには、RootCommandアトリビュートの他にCommandアトリビュートをつけたメソッドを定義します。

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public void Run(
        [Option("a1", "first argument")] string arg1,
        [Option("a2", "second argument")] int arg2 = 123
    )
    {
        Console.WriteLine($"arg1: {arg1}");
        Console.WriteLine($"arg2: {arg2}");
    }

    // Commandアトリビュートをつけたメソッドを定義すると、サブコマンドとして扱える
    [Command("sub")]
    public void RunSub(string arg1)
    {
        Console.WriteLine($"sub arg1: {arg1}");
    }
}

サブコマンドは以下のようにして実行できます。

$ Path/To/Tool sub --arg1 "SubTest"
sub arg1: SubTest
非同期メソッド

コマンドのメソッドは非同期メソッドにすることもできます。

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public async Task Run(
        [Option("a1", "first argument")] string arg1,
        [Option("a2", "second argument")] int arg2 = 123
    )
    {
        Console.WriteLine($"arg1: {arg1}");
        await Task.Delay(1000);
        Console.WriteLine($"arg2: {arg2}");
    }
}
終了コード

CLIでは正常終了やエラーなどといった状態を終了コードとして返します。
Console App Frameworkではコマンドのメソッドの戻り値としてint型の値を返すと、終了コードとして扱われます。

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public int Run(bool arg1)
    {
        return arg1 ? 1 : -1;
    }
}

非同期の場合はTask<bool>を返すことで終了コードを定義できます。

$ Path/To/Tool --arg1 123
$ echo $?
123
Dispose処理

コマンドを定義しているクラスにIDisposableIAsyncDisposableを実装すると、実行終了時にそれらが呼ばれます。

両方定義している場合にはIAsyncDisposableが優先され、IDisposableは呼ばれません。

internal sealed class Command : ConsoleAppBase, IDisposable, IAsyncDisposable
{
    // IDisposableとIAsyncDisposableを実装している場合にはIAsyncDisposableが優先される
    public async ValueTask DisposeAsync()
    {
        Console.WriteLine("called.");
    }
    
    public void Dispose()
    {
        Console.WriteLine("Not called.");
    }

    [RootCommand]
    public int Run(int arg1)
    {
        return arg1;
    }
}

その他覚えておくべきこと

基本的な使い方の他に覚えておくとよさそうなことをまとめておきます。

クラスや構造体を引数にする

以下のようにクラスや構造体を引数に設定することもできます。

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public void Run(Args args)// クラスを引数に
    {
        Console.WriteLine($"Arg1: {args.Arg1}");
        Console.WriteLine($"Arg2: {args.Arg2}");
    }

    internal sealed class Args
    {
        public string Arg1 { get; set; }
        public int Arg2 { get; set; }
    }
}

クラスや構造体を引数にする場合には、コマンドラインからはJSONの文字列を渡します。

$ Path/To/Tool --args "{\"Arg1\":\"Argument 01\",\"Arg2\":123}"
Arg1: Argument 01
Arg2: 123

配列の指定方法など、詳細は公式のドキュメントを参照してください。

github.com

実行オプション

ConsoleApp.Runの代わりに以下のようにConsoleApp.Createを使うとConsoleAppOptionを設定して実行時のオプションを設定できます。

namespace Example;

internal sealed class Program
{
    private static void Main(string[] args)
    {
        var app = ConsoleApp.Create(args, options =>
        {
            // ここにオプションを設定できる
        });
        app.AddCommands<Command>();
        app.Run();
    }
}

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public void Run(int arg1)
    {
        Console.WriteLine($"Arg1: {arg1}");
    }
}

使いそうなオプションとその説明は以下のとおりです。

var app = ConsoleApp.Create(args, options =>
{
    // falseに変えると引数名を指定する際に-と--を区別しなくなる
    options.StrictOption = false;

    // falseに変えるとConsoleAppFramework側で定義されているコマンドがhelpに表示されなくなる
    options.ShowDefaultCommand = false;

    // 型名やメソッド名、パラメータ名をコマンドに変換する時の方法を指定できる
    // デフォルトはケバブケース
    options.NameConverter = x => x.ToLower();

    // ヘルプに表示するアプリケーション名
    options.ApplicationName = "Example";
});
app.AddCommands<Command>();
app.Run();
Delegateを使ってコマンドを追加する

ConsoleApp.Runには非ジェネリックなメソッドも用意されています。
これを使うと以下のようにDelegateを使ってコマンドを追加することもできます。

namespace Example;

internal sealed class Program
{
    private static void Main(string[] args)
    {
        static void Run([Option("arg1")] int arg1)
        {
            Console.WriteLine($"Arg1: {arg1}");
        }

        ConsoleApp.Run(args, Run);
    }
}
コマンド引数をバリデーションする

引数にSystem.ComponentModel.DataAnnotationsValidationAttributeを継承したアトリビュート設定すると、引数をバリデーションできます。

using System.ComponentModel.DataAnnotations;

namespace Example;

internal sealed class Program
{
    private static void Main(string[] args)
    {
        ConsoleApp.Run<Command>(args);
    }
}

internal sealed class Command : ConsoleAppBase
{
    [RootCommand]
    public void Run([Range(0, 3)] int arg1) // arg1が0~3の範囲内であることを検証する
    {
        Console.WriteLine($"Arg1: {arg1}");
    }
}

参考

github.com