System.CommandLineを利用したCLI開発(.NET)

C#

CLIを開発する際、引数オプションヘルプの扱いは意外と面倒です。特に、ユーザーフレンドリーなCLIにするためには、わかりやすいヘルプの整備が欠かせません。
これらを一から実装するのは手間ですが、そこで役立つのが .NET 向けライブラリ「System.CommandLine」です。

今回は、このライブラリを使って簡単な計算ツール(加算・減算)を作りながら、主な使い方を紹介します。

System.CommandLineは現在プレビューです。最新情報は公式サイトをご確認ください。

前提条件

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
.NET8.0.304
IDEVisual Studio 2022 Community
System.CommandLine2.0.0-beta4.22272.1

記事のコードは、解説をわかりやすくするために、エラーチェックなど一部の処理を省略しています。

作成するCLIの仕様

以下のように加算・減算の計算ができるCLIを作成します

Calc.exe add 5 2       # → 7  
Calc.exe sub 5 2       # → 3  
Calc.exe sub 5 2 -r    # → -3 (reverseオプション付き)

また、以下の要件も満たします。

  • 引数の数が正しくない、または数値でない場合にエラーメッセージを表示
  • sub コマンドのみに –reverse / -r オプションを付与

用語の整理

System.CommandLineでは、CLIの入力をスペースで区切った単位(トークン)として解析します。以下の例ではsub以降がそれぞれトークンとなります。

Calc.exe sub 5 2 --reverse false

この場合の各トークンと役割は以下のとおりです

トークン名前
subサブコマンド
5サブコマンドの引数
2サブコマンドの引数
–reverseオプション
falseオプションの引数

実装

ライブラリの追加

System.CommandLine は現在プレビュー版のため、NuGet パッケージマネージャで「Include prerelease」にチェックを入れてインストールしてください。

ルートコマンドの作成

まずはルートコマンドを作成します。この下にサブコマンドやオプションを追加していきます。

// Program.cs
using System.CommandLine;
using ImageChange.Utils;

// ルートコマンドを登録
var rootCommand = new RootCommand("");
// 実際に実行される処理を追加
rootCommand.SetHandler(() =>
{
	Console.WriteLine("Hello, World!");
});

await rootCommand.InvokeAsync(args);

この状態で実行すると “Hello, World!” が表示されます。
以降、SetHandler の部分は不要になるため削除して構いません。

サブコマンドの作成

続いてaddsubの2つのサブコマンドを作成します。

// Program.cs

var addCommand = new Command("add", "add numbers");
var subCommnad = new Command("sub", "subtract numbers");

// それぞれのサブコマンドをルートコマンドに登録する
rootCommand.Add(addCommand);
rootCommand.Add(subCommnad);

この時点ではまだサブコマンドを実行しても何も実行されません。

サブコマンドに対する引数の追加

// Program.cs

// 2つの数値を受け取るためintの配列を指定
var numberArguments = new Argument<int[]>(name: "numbers", description: "set two numbers")
// サブコマンドに対して登録する
addCommand.Add(numberArguments);
subCommnad.Add(numberArguments);

サブコマンドの処理の実装

// Program.cs

addCommand.SetHandler((numbers) =>
{
	Console.WriteLine(numbers[0] + numbers[1]);
}, numberArguments);

subCommnad.SetHandler((numbers) =>
{
	Console.WriteLine(numbers[0] - numbers[1]);
}, numberArguments);

これで 「Calc add 1 2」 や 「Calc sub 1 2」 が動作するようになります。

バリデーションの追加

引数の数や値が不正な場合、明示的にエラーを表示させたい場合は、カスタムバリデーションを実装します。

// Program.cs

var numberArguments = new Argument<int[]>(name: "numbers",
	parse: args =>
	{
		(int[] result, args.ErrorMessage) = Validations.IsTwoNumbers(args.Tokens);
		return result;
	},
	description: "set two numbers"
)
{
	Arity = ArgumentArity.OneOrMore
};

4行目でカスタムバリデーションを追加しています。引数が正常の場合はその値を返します。エラーがあった場合はErrorMessageプロパティにエラーメッセージを設定することで、そのメッセージとヘルプをコンソールに表示してくれます。

12行目のArityの指定は必須ではありませんが、OneOrMoreを指定することで1つ以上の引数を必須にできます。引数が0個の場合は組み込みのエラーメッセージを返してくれるため、カスタムバリデーションで引数0個のチェックする必要がなくなります。

引数の型チェックなど組み込みの検証があります。しかし、引数に文字列を指定した場合にエラーとならずにintの配列に0に変換されて渡されてしまうため、カスタムバリデーションを作成しています。

カスタムバリデーションは以下のとおりです。

public static class Validations
{
	private static int[] defaulNumbers = [0, 0];

	public static (int[], string) IsTwoNumbers(IReadOnlyList<Token> tokens)
	{
		if (tokens.Count < 2)
		{
			return (defaulNumbers, $"one more number is required");
		}
		if (tokens.Count >= 3)
		{
			return (defaulNumbers, "too many arguments");
		}

		int[] numbers = new int[2];
		int index = 0;
		foreach (var token in tokens)
		{
			var isConverted = int.TryParse(token.Value, out numbers[index]);
			if (!isConverted)
			{
				return (defaulNumbers, $"arguments should be number");
			}
			index++;
		}
		return (numbers, string.Empty);
	}
}

ここまででCLIを実行してみましょう。まずは正常パターンです。

続いて引数を指定しない場合です。これは組み込みのエラーメッセージが表示されます。また合わせてヘルプ(使い方)も表示されます。このヘルプも一から作成する必要はありません。

数値以外を指定した場合です。

オプションの登録

sub コマンドにだけ –reverse(または -r)オプションを追加して、引く順番を逆にできるようにします。

// Program.cs

var reverseOption = new Option<bool>(name: "--reverse", description: "reverse the two numbers and then calculate them",
	getDefaultValue: () => false);
// -rでも実行できるようにエイリアスを登録する
reverseOption.AddAlias("-r");
// 引き算のサブコマンドにだけ登録
subCommnad.Add(reverseOption);

// オプションを受け取り、その値に基づいて計算する
subCommnad.SetHandler((numbers, option) =>
{
	var result = numbers[0] - numbers[1];
	var reverse = option ? -1 : 1;
	Console.WriteLine(result * reverse);
}, numberArguments, reverseOption);

なお引数とオプションは順不同です。

ただし、オプションをサブコマンドの前に指定すると、サブコマンドのオプションとは認識されなくなるためエラーとなります。

また、登録したオプション以外のオプションを指定すると、引数と解釈されます。

最後に

System.CommandLine を使えば、CLIツールに必要な機能(引数処理、バリデーション、ヘルプ、オプションなど)を簡潔かつ強力に実装できます。

プレビュー段階とはいえ、使い勝手が非常によく、今後も注目のライブラリです。
CLIを.NETで構築する際には、ぜひ活用してみてください。

コメント