C#でSystem.CommandLine v2 previewを試す
.NET 10がアツい
.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) とは
本記事では次の資料を参考にしました。
なお、プレビュー段階のもので破壊的変更が今後追加される可能性があることにご留意ください。
System.CommandLine は現在プレビュー段階であり、このドキュメントはバージョン 2.0 ベータ 5 用です。 一部の情報は、リリース前に大幅に変更される可能性があるプレリリース製品に関連しています。 Microsoft は、ここで提供される情報に関して明示的または黙示的な保証を行いません。
開発環境
Nixで作成しました。Nixについては先日記事を書いているので、そちらを参照してください。
ソースコードは次のようになります。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に対応しているので、若干違うということなのでしょう。
具体的には次のCommand.WriteLineのソースコードで確認できます。
public bool Required { get; set; }
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#などにもアップデートが来る予定なので気になる方は次のページを確認してみてください。
Discussion