16
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-06-12

C# を使っているときに、コマンドラインツールを作るときに便利なコマンドラインパーサーないかな?と思って調査したところ、最もオフィシャルに近い System.CommandLine を試してみることにしました。自分がしたいことを中心に調べたことをまとめてみたいと思います。

基本的な使い方

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になって欲しいですね。

16
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?