Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9723

System.CommandLine コマンドラインパーサーを利用する

$
0
0
C# を使っているときに、コマンドラインツールを作るときに便利なコマンドラインパーサーないかな?と思って調査したところ、最もオフィシャルに近い System.CommandLine を試してみることにしました。自分がしたいことを中心に調べたことをまとめてみたいと思います。 System.CommandLine を使用してコマンド ラインを解析する GitHub Project 基本的な使い方 RootCommand というクラスをインスタンス化して、そこに、AddArgument, AddOption で引数やオプションを渡します。最後に Invoke または、InvokeAsync を実行してあげるとよいです。Main メソッドの中で呼ばれるようにしてみてください。 Program.cs 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 はプロジェクト名です。 Help 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 でも使われます。 Argument rootCommand.AddArgument(new Argument<int>("stage", "Stage Number for update e.g. 1")); オプション オプションの項目は、次のように書くことで、 追加することができます。alias を定義することで、オプションの書き方や、省略形の書き方も設定できます。また、getDefaultValue() に対して関数を渡すことでデフォルト値を設定することができます。 Option 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 に足してあげるとよいだけのようです。 ハンドラの実行 さて、このコマンドアプリが実行されたら、何らかのアクションを実行したいと思います。このように設定します。 Handler rootCommand.Handler = CommandHandler.Create<int, string, string, FileInfo, IConsole>(new UpdateAction().Execute); ここで、ポイントは、ここで渡しているハンドラの名前です。定義したArgument や、Option で定義した名前と同じ引数名にする必要があります。Argument は名前が明確なので、わかりやすいですが、Option は、aliasしかないし、--config-file とかの場合どうなるの?と思うと思いますがこの場合は、configFile というキャメルケースになるようです。私はここが最初わからなくて、パラメータが渡ってこないという問題に遭遇しました。 UpdateAction.Execute public int Execute(int stage, string source, string target, FileInfo configFile, IConsole console) { : 終了ステータス コマンドラインアプリを作っていると、終了ステータスを定義したいと思います。実は、先ほどのハンドラは戻り値無しでもかけるのですが、先ほど紹介した通り戻り値をint として定義しています。そうすると、この部分が終了ステータスになるので、return 1; とか返すと、エラーの終了ステータスになりますので便利です。 ユニットテストで便利な機能 先ほどのハンドラの部分で、IConsole という謎のパラメータがあります。これをつけてあげると、Console のオブジェクトを引き取ることができます。コンソールへの出力は次のように標準出力にも、エラー出力にも書くことができます。 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を渡して、エラーの時のメッセージを簡単にテストすることができます。 TestSomething.cs 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); TestConsole.cs 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になって欲しいですね。

Viewing all articles
Browse latest Browse all 9723

Trending Articles