🦉

C#でSystem.CommandLine v2 previewを試す

に公開

.NET 10がアツい

https://andrewlock.net/exploring-dotnet-10-preview-features-2-behind-the-scenes-of-dotnet-run-app.cs/

.NET 10のpreviewに画期的な機能が追加されました。内容はdotnet runコマンドの実行に必要なファイルが*.csファイル単体になるというものです。元々、.csprojファイルが必要だったのですが、不要になります。

単に実行可能になっただけでなく、sdkやNugetのパッケージなども導入可能です。具体的には次のように記述します。これでサーバーが立てられます。

#!/usr/bin/dotnet run

#:sdk Microsoft.NET.Sdk.Web
#:package Newtonsoft.Json@13.0.3
#:property UserSecretsId 2eec9746-c21a-4933-90af-c22431f35459

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => JsonConvert.SerializeObject(new { Greeting = "Hello, World!" }));

app.Run();

サーバーにも期待していますが、バッチ処理などでも活躍しそうという予感があります。静的型付けによる型の保証がされつつ、スクリプト言語のような書き方ができるからです。

そんな.NET 10で1st Party製のCLI作成ツールが追加されると調べているうちに分かりました。この記事では Hello ${Name}を出力するCLIを作りながら紹介します。

System.CommandLine (V2) とは

本記事では次の資料を参考にしました。

https://learn.microsoft.com/ja-jp/dotnet/standard/commandline/get-started-tutorial

なお、プレビュー段階のもので破壊的変更が今後追加される可能性があることにご留意ください。

System.CommandLine は現在プレビュー段階であり、このドキュメントはバージョン 2.0 ベータ 5 用です。 一部の情報は、リリース前に大幅に変更される可能性があるプレリリース製品に関連しています。 Microsoft は、ここで提供される情報に関して明示的または黙示的な保証を行いません。

開発環境

Nixで作成しました。Nixについては先日記事を書いているので、そちらを参照してください。

https://zenn.dev/shundeveloper/articles/36307d821d40f7

ソースコードは次のようになります。go-taskとtreeは好みで。

{
  description = "C# Shell";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in {
        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.dotnet-sdk_9
            pkgs.go-task
            pkgs.tree
          ];
          DOTNET_ROOT = "${pkgs.dotnet-sdk_9}/share/dotnet";
          shellHook = ''
            echo "dotnet version: $(dotnet --version)"
            echo "task version: $(task --version)"
            echo "tree version: $(tree --version)"
            echo "DOTNET_ROOT: $DOTNET_ROOT"
          '';
        };
      });
}

インストールしたバージョンは次の通り。

dotnet version: 9.0.303
task version: 3.43.3
tree version: tree v2.2.1 © 1996 - 2024 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro
DOTNET_ROOT: /nix/store/xh91s3lhka5rx8ncd16i3x64q4qngara-dotnet-sdk-wrapped-9.0.303/share/dotnet

プレビュー版のSystem.CommandLineをインストール

dotnet add package [CommandName]--prerelease フラグを利用してプレビュー版のSystem.CommandLine をインストールできます。

bash-5.2$ dotnet add package System.CommandLine --prerelease
  Determining projects to restore...
  Writing /tmp/nix-shell.XcDsD7/tmpQnzHIh.tmp
...
info :   GET https://api.nuget.org/v3-flatcontainer/system.commandline/2.0.0-beta6.25358.103/system.commandline.2.0.0-beta6.25358.103.nupkg
info :   OK https://api.nuget.org/v3-flatcontainer/system.commandline/2.0.0-beta6.25358.103/system.commandline.2.0.0-beta6.25358.103.nupkg 26ms
info : Installed System.CommandLine 2.0.0-beta6.25358.103 from https://api.nuget.org/v3/index.json to /Users/shunsock/.nuget/packages/system.commandline/2.0.0-beta6.25358.103 with content hash YE8bPzXelsU4Fm4mHIFKwPHYJ1RT0hr1b5+tiyjFk4YYpA+WKzdYxrvdJVJIoC+2vbtffajA928Qe7xUj0WR4g==.
...
frameworks in project '/Users/shunsock/hobby/hisui/src/hisui/hisui.csproj'.
info : PackageReference for package 'System.CommandLine' version '2.0.0-beta6.25358.103' added to file '/Users/shunsock/hobby/hisui/src/hisui/hisui.csproj'.
info : Writing assets file to disk. Path: /Users/shunsock/hobby/hisui/src/hisui/obj/project.assets.json
log  : Restored /Users/shunsock/hobby/hisui/src/hisui/hisui.csproj (in 590 ms).

今回は、System.CommandLine 2.0.0-beta6.25358.103がインストールされたようです。

ソースコード

全体図

先にソースコード全体を提示します。

using System.CommandLine;
using System.CommandLine.Parsing;

namespace hisui;

class Program
{
    static int Main(string[] args)
    {
        Option<string> nameOption = new("--name")
        {
            Description = "The name to read and display on the console.",
            Required = true
        };

        // RootCommand
        RootCommand rootCommand = new("Sample app for System.CommandLine");
        rootCommand.Options.Add(nameOption);

        // Parse
        ParseResult parseResult = rootCommand.Parse(args);
        if (parseResult.Errors.Count > 0)
        {
            rootCommand.Parse("-h").Invoke();
            foreach (ParseError parseError in parseResult.Errors)
            {
                Console.Error.WriteLine(parseError.Message);
            }
            return 1;
        }

        string? name = parseResult.GetValue(nameOption);
        Console.WriteLine($"Hello, {name}");
        return 0;
    }
}

解説

Option<T> classは、Beta5までは IsRequired オプションがあったようですが、Requiredに変更されています。ドキュメントはBeta5に対応しているので、若干違うということなのでしょう。

https://learn.microsoft.com/ja-jp/dotnet/api/system.commandline.option?view=system-commandline

具体的には次のCommand.WriteLineのソースコードで確認できます。

public bool Required { get; set; }

https://github.com/dotnet/command-line-api/blob/0b4618bc860374941e605d8eb1d2bc29c32801db/src/System.CommandLine/Option.cs#L109

Parseの結果はResult型で取得できます。嬉しいですね。エラーハンドリングですが、ParseResult 型の Errors プロパティが IReadOnlyList<ParseError> というリスト形式で得られるのでそれを利用します。

foreach 文でエラーのリストをiterateするのが新鮮で個人的には良い体験でした。他の言語でも実装していきたいですね。

ParseResult parseResult = rootCommand.Parse(args);
if (parseResult.Errors.Count > 0)
{
    foreach (ParseError parseError in parseResult.Errors)
    {
        Console.Error.WriteLine(parseError.Message);
        rootCommand.Parse("-h").Invoke();
    }
    return 1;
}

テスト

正常系

bash-5.2$ /Users/shunsock/hobby/hisui/src/hisui/bin/Debug/net9.0/hisui --name しゅんそく
Hello, しゅんそく

異常系

--name オプションのあとに入力が無い場合

bash-5.2$ /Users/shunsock/hobby/hisui/src/hisui/bin/Debug/net9.0/hisui --name
Description:
  Sample app for System.CommandLine

Usage:
  hisui [options]

Options:
  -?, -h, --help     Show help and usage information
  --version          Show version information
  --name (REQUIRED)  The name to read and display on the console.

Required argument missing for option: '--name'.

Parseの時点で失敗するとParseResultの分岐でエラーハンドリングがされます。

bash-5.2$ /Users/shunsock/hobby/hisui/src/hisui/bin/Debug/net9.0/hisui --file
Description:
  Sample app for System.CommandLine

Usage:
  hisui [options]

Options:
  -?, -h, --help     Show help and usage information
  --version          Show version information
  --name (REQUIRED)  The name to read and display on the console.

Unrecognized command or argument '--file'.
Option '--name' is required.

存在しないフラグを立てると上のようになります。

まとめ

.NET10のGAが楽しみですね。Pythonのargparseのような手離せないツールになりそうです。GAまでは破壊的変更が入る可能性があるのでそれまでは別のパッケージで様子見をしようかなと考えています。

なお、.NET 10はC#だけでなく、F#などにもアップデートが来る予定なので気になる方は次のページを確認してみてください。

https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview

Discussion