Posted at

多機能コマンドラインオプションパーサーMcMaster.Extensions.CommandlineUtilsの紹介


はじめに

C#におけるコマンドラインオプションパーサーの実装にはいくつか種類がある。

過去にCommandlineParserというものの紹介記事も書いたことがある。

今回はMcMaster.Extensions.CommandlineUtilsについて書こうと思う。


出自

以前はASP.NETのコンポーネントの一つとして、Microsoft.Extensions.CommandlineUtilsというものがあった。

ただ、コマンドラインオプションというのはどうしても要望が多くなってしまう類のもので、本質ではない部分のメンテコストが

増大することを危惧したASP.NET Coreチームは、このパッケージをメンテしないことに決定した

そして、ASP.NET本体とは切り離し、完全に別個のパッケージとして分岐したのが今回紹介するMcMaster.Extensions.CommandlineUtilsである。

そのため、多くの部分がMicrosoft.Extensions.CommandlineUtilsに由来している。また、ASP.NETが用意しているDIを非常に意識して作られているので、相性が良い。


利点


  • 元がASP.NETプロジェクトの一部だったこともあり、ASP.NETの各種機能との相性が良い

  • Attributeベースで書くと、コマンドごとにクラスが分かれることになり、見通しが良くなる?


欠点

記述が他のライブラリに比べて多少冗長になる傾向がある。


使い方


基本

仕様的には 公式READMEが大変親切なので、ここを参考にすればいいと思う。

Builderパターンはオプション変数の取り回しが面倒なので、Attributeパターンがお勧め。

Attributeパターンについてのサンプルコードを見ると、基本的に以下の順番で作っていけばいいことがわかる。


  1. コマンドクラスを作る


  2. Option属性を付けたプロパティを定義


  3. OnExecute()メソッドを用意


  4. CommandlineApplication<T>.Execute(argc, argv)を実行


Dependency Injection

コマンドの実行時には、単純な引数とのマッピングだけではなく、例えばロギング等様々なパラメーターと共に使用したい場合がある。

そんな時はインジェクションを使用する。

サンプルコードを見ると、


  1. Microsoft.Extensions.DependencyInjectionをPackageReferenceに追加

  2. Microsoft.Extensions.DependencyInjection.ServiceCollectionをnew


  3. var services = new ServiceCollection();としてインスタンスを設定し、DI設定を追加




  4. CommandLineApplication<T>をnew


  5. CommandLineApplication<T>.Conventions.UseConstructorInjection(services.BuildProvider()).UseDefaultConventions()を実行


  6. CommandLineApplication<T>インスタンスのExecuteを実行

というのが基本的なやり方となる。

これにより、ASP.NETでも使っているようなロギングや、各種便利機能を使えるようになるので、それだけでも結構いいと思う。

が、一番の利点はサブコマンドを使用した時、親コマンドのインスタンスをDIで受け取れるようになることだと思う。

これにより、サブコマンドの扱いがかなり楽になる。

using McMaster.Extensions.CommandLineUtils;

using System;
using Microsoft.Extensions.DependencyInjection;

[Command]
class MySubCommand
{
[Option]
public string Y { get; set; }
public MyCommand _Parent;
public MySubCommand(MyCommand parent)
{
// オプションセット済みの親コマンドのインスタンスが渡される
_Parent = parent;
}
public void OnExecute()
{
// 親コマンドの実行も可能
_Parent.OnExecute();
// 親クラスのオプションの取得も可能
Console.WriteLine($"parent X = {_Parent.X}, Y = {Y}");
}
}

[Command]
[HelpOption]
[Subcommand("sub", typeof(MySubCommand))]
// 親コマンド
class MyCommand
{
[Option]
public string X { get; set; }
public void OnExecute()
{
Console.WriteLine($"in {nameof(MyCommand)}");
}
}

static class Executor
{
public static void EntryPoint()
{
var services = new ServiceCollection();
var app = new CommandLineApplication<MyCommand>();
// UseConstructorInjectionに空引数を指定すれば、デフォルトのServiceProviderが使用される
// 属性ベースのAPIを使用する場合はUseDefaultConventions()は必須
app.Conventions.UseConstructorInjection(services.BuildServiceProvider()).UseDefaultConventions();
app.Execute(new []{ "-x", "abc", "sub", "-y", "def" });
}
}

以上のコードで、Executor.EntryPoint()を実行すると、以下の出力が得られる。

in MyCommand

parent X = abc, Y = def


終りに

サンプルコードを眺めてみても、他のライブラリに比べて煩雑な記述が多いと思うかもしれない。実際書き捨てコマンドには向いていない。

実際自分もそう思うことはあるが、しかしサブコマンドの扱いやロギングまで含めると、

DI経由でASP.NETの機能を使えるようになるのは色々と嬉しいことも多いので、ある程度以上の規模になれば、

その利点も実感できるようになるかと思う。

執筆時点の安定板である2.2.5では、ASP.NETの汎用ホストとの連携はまだ取れないが、masterには連携コードが入っているので、恐らく2.3.x辺りには入ることになると予想される。そうすれば、サービスアプリ等を書くときにこのライブラリが有用になっていくと思う。