C# を使っているときに、コマンドラインツールを作るときに便利なコマンドラインパーサーないかな?と思って調査したところ、最もオフィシャルに近い System.CommandLine
を試してみることにしました。自分がしたいことを中心に調べたことをまとめてみたいと思います。
基本的な使い方
RootCommand
というクラスをインスタンス化して、そこに、AddArgument
, AddOption
で引数やオプションを渡します。最後に Invoke
または、InvokeAsync
を実行してあげるとよいです。Main
メソッドの中で呼ばれるようにしてみてください。
static async Task<int> Main(string[] args)
{
var rootCommand = new RootCommand
{
Description = "Update the configuration file of the LinuxPoolConfig"
};
rootCommand.AddArgument(new Argument<int>("stage", "Stage Number for update e.g. 1"));
Option sourceOption = new Option<string>(
new string[] {"--source", "-s"},
"Source Version for update. e.g. 3.0.17892 By default, this tool update update version that matches the major version of target version."
);
rootCommand.AddOption(sourceOption);
rootCommand.AddArgument(new Argument<string>("target", "Target Version for update. e.g. 3.0.17893"));
Option configFileOption = new Option<FileInfo>(
aliases: new string[] {"--config-file", "-c"},
description: "Config file to update");
rootCommand.AddOption(configFileOption);
rootCommand.Handler = CommandHandler.Create<int, string, string, FileInfo, IConsole>(new UpdateAction().Execute);
return await rootCommand.InvokeAsync(args);
}
すると、Helpを自動で作成してれます。MyConfigCmd
はプロジェクト名です。
MyConfigCmd
Update the configuration file of the MyConfig
Usage:
MyConfigCmd [options] <stage> <target>
Arguments:
<stage> Stage Number for update e.g. 1
<target> Target Version for update. e.g. 3.0.17893
Options:
-s, --source <source> Source Version for update. e.g. 3.0.17892 By default, this tool update update
version that matches the major version of target version.
-c, --config-file <config-file> Config file to update
--version Show version information
-?, -h, --help Show help and usage information
実行の例は次のような感じです。
$ MyConfigCmd.exe 0 3.0.15829 --source 3.0.15828 --config-file .\Hello\config.json
必須・オプションパラメータの表現
必須項目
必須項目は、Argument
で表現します、必須項目の型、名前、概要を定義できます。概要は、help
でも使われます。
rootCommand.AddArgument(new Argument<int>("stage", "Stage Number for update e.g. 1"));
オプション
オプションの項目は、次のように書くことで、 追加することができます。alias
を定義することで、オプションの書き方や、省略形の書き方も設定できます。また、getDefaultValue()
に対して関数を渡すことでデフォルト値を設定することができます。
Option configFileOption = new Option<FileInfo>(
aliases: new string[] {"--config-file", "-c"},
description: "Config file to update",
getDefaultValue: () => new FileInfo(Path.Combine(".", "config")));
rootCommand.AddOption(configFileOption);
Sub Command
Sub Command は、今回必要なかったので試していませんが Add SubCommand を見ると、Command
というクラスをインスタンス化して、RootCommand
に足してあげるとよいだけのようです。
ハンドラの実行
さて、このコマンドアプリが実行されたら、何らかのアクションを実行したいと思います。このように設定します。
rootCommand.Handler = CommandHandler.Create<int, string, string, FileInfo, IConsole>(new UpdateAction().Execute);
ここで、ポイントは、ここで渡しているハンドラの名前です。定義したArgument
や、Option
で定義した名前と同じ引数名にする必要があります。Argument
は名前が明確なので、わかりやすいですが、Option
は、alias
しかないし、--config-file
とかの場合どうなるの?と思うと思いますがこの場合は、configFile
というキャメルケースになるようです。私はここが最初わからなくて、パラメータが渡ってこないという問題に遭遇しました。
public int Execute(int stage, string source, string target, FileInfo configFile, IConsole console)
{
:
終了ステータス
コマンドラインアプリを作っていると、終了ステータスを定義したいと思います。実は、先ほどのハンドラは戻り値無しでもかけるのですが、先ほど紹介した通り戻り値をint
として定義しています。そうすると、この部分が終了ステータスになるので、return 1;
とか返すと、エラーの終了ステータスになりますので便利です。
ユニットテストで便利な機能
先ほどのハンドラの部分で、IConsole
という謎のパラメータがあります。これをつけてあげると、Console のオブジェクトを引き取ることができます。コンソールへの出力は次のように標準出力にも、エラー出力にも書くことができます。
if (configFile == null || !File.Exists(configFile?.FullName))
{
console.Error.WriteLine($"config file : {configFile} does not exists.");
return 1;
}
console.Out.WriteLine($"executing.... Stage: {stage}, SourceVersion: {source}, TargetVersion : {target}, ConfigFile : {configFile?.FullName}");
:
これが何が良いか?というと、このハンドラのユニットテストを書くと、次のような感じで書くことができます。つまり、コンソールのMockを渡して、エラーの時のメッセージを簡単にテストすることができます。
TestConsole console = new TestConsole();
var status = new UpdateAction().Execute(0, "3.0.15828", "3.0.15829", new FileInfo(wrongConfigPath), console);
console.StdErr().Flush();
string output = Encoding.UTF8.GetString(console.StdErr().ToArray());
Assert.Contains($"config file : {wrongConfigPath} does not exists.", output);
public class TestConsole :IConsole
{
public IStandardStreamWriter Out { get; } = new TestConsoleStandardOutputWriter();
public bool IsOutputRedirected { get; }
public IStandardStreamWriter Error { get; } = new TestConsoleStandardErrorWriter();
public bool IsErrorRedirected { get; }
public bool IsInputRedirected { get; }
public MemoryStream StdOut()
{
return ((TestConsoleStandardOutputWriter) Out).Message;
}
public MemoryStream StdErr()
{
return ((TestConsoleStandardErrorWriter) Error).Message;
}
}
public class TestConsoleStandardOutputWriter : IStandardStreamWriter
{
public MemoryStream Message { get; } = new MemoryStream();
public void Write(string value)
{
Message.Write(Encoding.UTF8.GetBytes(value));
}
}
public class TestConsoleStandardErrorWriter : IStandardStreamWriter
{
public MemoryStream Message { get; } = new MemoryStream();
public void Write(string value)
{
Message.Write(Encoding.UTF8.GetBytes(value));
}
}
おわりに
私のやりたいことは、大体 System.CommandLine
で出来る感じなので、今後も使っていこうと思います。ちなみに、まだβなので、はよGAになって欲しいですね。