September 2016

Volume 31 Number 9

Essential .NET - .NET Core 1.0 によるコマンドライン処理

Mark Michaelis

Mark Michaelis今月の Essential .NET コラムでも引き続き .NET Core のさまざまな機能について調査します。今回は、完全なリリース版を使用します (もはや、ベータ版やリリース候補版ではありません)。具体的に注目するのは、github.com/aspnet/Common (英語) の .NET Core Common ライブラリにあるコマンドライン ユーティリティで、これをコマンドラインの解析に利用する方法を調べます。コマンドライン解析のサポートが組み込まれた .NET Core の登場に大きな期待を寄せています。コマンドラインの解析は、.NET Framework 1.0 から望まれていた機能です。.NET Core の組み込みライブラリが、プログラム間でのコマンドラインの形式と構造の標準化に少しでも役立つことを願っています。標準の内容はそれほど重要ではありません。表記法を自身で生み出すよりは、既定で使える表記法がある方が便利だという程度のことです。

コマンドライン表記

NuGet パッケージの Microsoft.Extensions.CommandLineUtils には、コマンドライン機能がたくさんあります。アセンブリに含めるのは、CommandLineApplication クラスです。このクラスは、オプションの長い名前と短い名前のサポート、コロンや等号を使って代入する (1 つ以上の) 値、ヘルプ用の -? のような記号のサポートを使用して、コマンドライン解析を可能にします。ヘルプに関しては、ヘルプ テキストを自動表示するサポートがこのクラスに含まれています。図 1 に、サポートすることになると思われるサンプルのコマンドラインをいくつか示します。

図 1 サンプル コマンドライン

オプション Program.exe -f=Inigo、-l Montoya –hello –names Princess –names Buttercup

値 “Inigo” を指定したオプション -f

値 “Montoya” を指定したオプション -l

値 “on” を指定したオプション –hello

値 “Princess” と “Buttercup” を指定したオプション –names

引数を持つコマンド Program.exe "hello", "Inigo", "Montoya", "It", "is", "a", "pleasure", "to", "meet", "you."

コマンド “hello”

引数 “Inigo”

引数 “Montoya”

値 “It”、“is”、“a”、“pleasure”、“to"、“meet”、“you” を指定した引数 Greetings

記号 Program.exe -? ヘルプを表示する

次に説明するように、引数には複数の種類があります。その中の 1 つを「引数」と鍵カッコ付きにしています。 引数という言葉はあいまいで、コマンドラインで指定する値にも、コマンドラインを構成するデータにも使われます。そのため、ここからは汎用の引数と鍵カッコ付きの「引数」を区別します。汎用の引数は実行可能ファイル名の後に指定するすべての種類の引数を指します。同様に、汎用の引数と区別して、「オプション」と「コマンド」と鍵カッコ付きにしている引数もあります。ここからはこの区別が重要になるため、ご注意ください。

区別する引数の種類はそれぞれ、次のように説明されます。

  • 「オプション」: 「オプション」は名前で識別します。名前の前に 1 つのダッシュ (-) または 2 つのダッシュ (--) を付けます。「オプション」の名前は、テンプレートを使ってプログラムで定義します。テンプレートでは、短い名前、長い名前、記号の 3 つの指定子を 1 つ以上複数含めることができます。また、「オプション」には、値を関連付けることができます。たとえば、テンプレートで -n | --name | -# <Full Name> と指定すると、3 つの指定子のどれを使用しても、フル ネーム オプションを識別できます (ただし、テンプレートで 3 つの指定子をすべて含める必要はありません)。 短い名前と長い名前のどちらを指定するかは、ダッシュを 1 つ使うか、2 つ使うかによって決まります。実際の名前の長さには関係ありません。
    値を「オプション」に関連付けるには、スペースまたは代入演算子 (=) を使用します。したがって、-f=Inigo と -l Montoya はどちらもオプション値を指定する例です。
    テンプレートで数字を使用している場合、それらは短い名前または長い名前の 1 部で、記号ではありません。
  • 「引数」: 「引数」は、名前ではなく、指定する順序によって識別されます。したがって、オプション名がプレフィックスとして指定されていないコマンドラインの値は 1 つの引数です。値がどの引数に対応するかは、指定する順序で決まります (「オプション」と「コマンド」は順序のカウントから除外されます)。
  • 「コマンド」: 「コマンド」は、引数とオプションのグループを提供します。たとえば、“hello” というコマンドの後に、「引数」と「オプション」 (またはサブ「コマンド」) の組み合わせを続けます。「コマンド」は、構成済みのキーワード (コマンド名) によって識別されます。「コマンド」は、コマンド名に続く値をすべてグループにして、「コマンド」の定義の一部にします。

コマンドラインを構成する

.NET Core Microsoft.Extensions.CommandLineUtils を参照した後、コマンドラインのプログラミングは、CommandLineApplication クラスから始めます。このクラスを使用して、「コマンド」、「オプション」、「引数」をそれぞれ構成します。CommandLineApplication のコンストラクターにはオプションのブール値があります。このオプションをインスタンスの作成時に構成して、引数が明確に構成されていない場合に例外をスローします (既定の設定)。

CommandLineApplication のインスタンスを用意したら、Option、Argument、Command の各メソッドを使用して引数を構成します。たとえば、以下のコマンドライン構文をサポートするとします。角かっこの中の項目は省略可能で、山かっこの中の項目はユーザーが指定する値または引数です。

Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>] 
     [-?|-h|--help] [-u|--uppercase]

図 2 のコードは、基本的な解析機能を構成しています。

図 2 コマンドラインの構成

public static void Main(params string[] args)
{
    // Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>]
    // [-?|-h|--help] [-u|--uppercase]
  CommandLineApplication commandLineApplication =
    new CommandLineApplication(throwOnUnexpectedArg: false);
  CommandArgument names = null;
  commandLineApplication.Command("name",
    (target) =>
      names = target.Argument(
        "fullname",
        "Enter the full name of the person to be greeted.",
        multipleValues: true));
  CommandOption greeting = commandLineApplication.Option(
    "-$|-g |--greeting <greeting>",
    "The greeting to display. The greeting supports"
    + " a format string where {fullname} will be "
    + "substituted with the full name.",
    CommandOptionType.SingleValue);
  CommandOption uppercase = commandLineApplication.Option(
    "-u | --uppercase", "Display the greeting in uppercase.",
    CommandOptionType.NoValue);
  commandLineApplication.HelpOption("-? | -h | --help");
  commandLineApplication.OnExecute(() =>
  {
    if (greeting.HasValue())
    {
      Greet(greeting.Value(), names.Values, uppercase.HasValue());
    }
    return 0;
  });
  commandLineApplication.Execute(args);
}
private static void Greet(
  string greeting, IEnumerable<string> values, bool useUppercase)
{
  Console.WriteLine(greeting);
}

CommandLineApplication から始める

まず、コマンドライン解析を厳密に行う (throwOnUnexpectedArg が ture) か、柔軟に行うかを指定して、CommandLineApplication のインスタンスを作成します。引数が想定通りではないときに例外をスローするように指定する場合は、すべての引数を明示的に構成しなければなりません。throwOnUnexpectedArg に false を指定すると、構成によって認識されない引数は CommandLineApplication.Remaining­Arguments フィールドに格納されます。

「コマンド」とその「引数」を構成する

図 2 では、次の手順で “name” 「コマンド」を構成しています。引数のリスト内のコマンドを識別するキーワードは、name という関数 Command の最初のパラメーターです。2 つ目のパラメーターは、構成と呼ばれるAction<CommandLineApplication> デリゲートで、name 「コマンド」のすべての従属引数がこのデリゲートに構成されます。今回の場合、“greeting” という変数名を持つ CommandArgument 型の「引数」が 1 つだけあります。 ただし、構成デリゲート内で「引数」、「オプション」またはサブ「コマンド」を追加することもできます。さらに、デリゲートの target パラメーター (CommandLineApplication) には Parent プロパティがあります。このプロパティは、commandLineArgument を逆参照します。ターゲットの親に当たる CommandLineArgument で name 「コマンド」を構成します。

names 「引数」の構成では、multipleValues をサポートすることを明確に特定しています。こうすることで、複数の値 (このケースでは、複数の名前) を指定できるようにします。引数識別子 “name” の後に値が指定されます。値は、別の引数やオプション識別子が出現するまで複数指定できます。関数 Argument の最初の 2 つのパラメーターは、「引数」の名前と説明です。この名前は「引数」のリストから特定できます。

最後に 1 つ、name 「コマンド」の構成で重要な点として、関数 Argument (および指定している場合は関数 Option) の戻り値は保存しておく必要があります。これは、name 「引数」に関連付けられる引数を後から取得できるようにするためです。戻り値の参照を保存しない場合は、「引数」データを探すために commandLineApplication.Commands[0].Arguments コレクション全体を探す必要が生じます。

すべてのコマンドライン データを保存する的確な方法として、ASP.NET スキャフォールディング リポジトリ (github.com/aspnet/Scaffolding) の属性で装飾したクラスに別途格納する方法があります。具体的には、src/Microsoft.VisualStudio.Web.CodeGeneration.Core/CommandLine フォルダーを利用します。詳細については、「.NET Core によるコマンドライン クラスの実装」(bit.ly/296SluA、英語) を参照してください。

「オプション」を構成する

図 2 で構成している次の引数は、CommandOption 型の greeting 「オプション」です。「オプション」の構成には関数 Option を使用します。この関数の最初のパラメーターは、テンプレートと呼ばれる文字列パラメーターです。図の例では 3 つの異なる名前 (-$、-g、-greeting) をオプションとして指定できること、それぞれの名前を使って引数のリストからオプションを識別できることを指定しています。テンプレートでは、必要に応じてそのオプションに関連付ける値を指定することもできます。そのためには、オプション識別子のあとに続く山かっこで囲んで名前を指定します。説明パラメーターの後に、関数 Option の必須パラメーター CommandOptionType を含めます。このオプションは以下のことを特定します。

  1. オプション識別子の後に値を指定するかどうか。CommandOptionType を NoValue にしている場合、そのオプションが引数リスト内に出現すると、関数 CommandOption.Value に “on” が設定されます。オプション識別子の後に異なる値が指定されても値 “on” が返されます。実際には、値が指定されているかどうかにかかわらず、値 “on” が返されます。例については、図 2 の uppercase オプションを確認してください。
  2. さらに、CommandOptionType を SingleValue にしている場合に、オプション識別子が指定されてもその値が出現しないと、テンプレートと一致しないため、オプションが識別されないことを示す CommandParsingException がスローされます。つまり、SingleValue は、オプション識別子が必ず出現すると想定して、値が指定されていることをチェックする手段を提供します。
  3. 最後に、CommandOptionType に Multiple­Value を指定することができます。ただし、コマンドに複数の値を関連付けるのとは異なり、オプションの場合は、同じオプションを複数回指定できます。たとえば、「program.exe -name Inigo -name Montoya」のように指定します。

この構成オプションがなければ構成は成り立たないので、このオプションは必須です。実は、引数についても同じことが当てはまります。値が指定されないときにエラーにするには、引数が false を返したときに関数 HasValue がエラーを報告するかどうかをチェックする必要があります。CommandArgument の場合、値が指定されていなければ、Value プロパティが null を返します。エラーを報告する場合は、エラー メッセージに続いてヘルプ テキストを表示することを検討します。そうすれば、問題を解決するには何をする必要があるかについて、ユーザーが多くの情報を得られるようになります。

CommandLineApplication 解析メカニズムの動作でもう 1 つ重要なのは、大文字と小文字が区別されることです。実は現時点では、大文字と小文字を区別しないようにできる簡単な構成オプションはありません。したがって、大文字と小文字を区別しないようにするためには、CommandLineApplication に渡される実際の引数の大文字と小文字を変更する必要があります (後ほど説明しますが、メソッド Execute を使用します。また、こうしたオプションを有効にするために、github.com/aspnet/Common でプル要求を送信することもできます)。

ヘルプとバージョンを表示する

CommandLineApplication には、コマンド ライン構成に関連付けたヘルプ テキストを自動的に表示する関数 ShowHelp が組み込まれています。たとえば、図 3図 2 のコマンドラインに対する関数 ShowHelp の出力を示しています。

図 3 ShowHelp の表示出力

Usage:  [options] [command]
Options:
  -$|-g |--greeting <greeting>  The greeting to display. 
                                The greeting supports a format string 
                                where {fullname} will be substituted 
                                with the full name.
  -u | --uppercase              Display the greeting in uppercase.
  -? | -h | --help              Show help information
Commands:
  name 
Use " [command] --help" for more information about a command.

残念ながら、表示されているヘルプでは、オプションやコマンドを実際に省略できるかどうかが分かりません。言い換えると、このヘルプ テキストでは、すべてのオプションとコマンドが省略可能であると想定されるように (角かっこを使って) 表示されています。

たとえば、カスタム コマンド ライン エラーを処理するときに、ShowHelp を明示的に呼び出すこともできます。ただし、ShowHelp は HelpOption テンプレートに一致するテンプレートが指定されたときは必ずに自動的に呼び出されます。HelpOption テンプレートはメソッド CommandLineApplication.HelpOption の引数を通じて指定します。

同様に、アプリケーションのバージョンを表示するメソッド ShowVersion もあります。ShowHelp と同様、以下の 2 つの方法の 1 つを使って構成します。

public CommandOption VersionOption(
  string template, string shortFormVersion, string longFormVersion = null).
public CommandOption VersionOption(
  string template, Func<string> shortFormVersionGetter,
  Func<string> longFormVersionGetter = null)

どちらの方法でも、VerisionOption を呼び出す際に、表示するバージョン情報を指定する必要があります。

コマンドライン データの解析と読み取り

ここまでは、CommandLineApplication を構成する方法を詳しく見てきました。しかし、コマンドラインの解析をトリガーする非常に重要なプロセスや、解析の呼び出し直後に何が起きるかについては、まだ説明していません。

コマンドライン解析をトリガーするには、コマンドラインで指定された引数リストを渡して関数 CommandLineApplication.Execute を呼び出す必要があります。図 1 では、引数が Main の args パラメーターで指定されるため、それを 直接関数 Execute に渡します(大文字と小文字の区別が好ましくなければ、最初にそれに対処する必要があります)。構成されたそれぞれの「引数」と「オプション」をコマンドラインのデータを関連付けるのがメソッド Execute です。

CommandLineAppliction には、関数 OnExecute(Func<int> invoke) が含まれています。この関数には、解析完了後に自動的に実行する Func<int> デリゲートを渡すことができます。図 2 では、メソッド OnExecute が簡単なデリゲートを受け取り、関数は Greet を呼び出す前に greet コマンドが指定されているかどうかをチェックしています。

また、invoke デリゲートから返される int 値は、Main からの戻り値を指定する手段として設計されています。実際には、invoke からどのような値が返ってきても、Execute からの戻り値に対応することになります。また、解析が比較的時間がかかる操作だと考えられているため (何でも比較的と言っているような気がしますが)、Execute は Func<Task<int>> を受け取るオーバーロードをサポートし、コマンドライン解析の非同期呼び出しを可能にしています。

ガイドライン: 「コマンド」、「引数」、および「オプション」

3 つの種類のコマンドを利用できることを考えると、どの種類をいつ使うかをすばやく確認できると便利です。

コンパイル、インポート、バックアップなどのアクションを意味的に識別する場合は「コマンド」を使います

プログラム全体または特定のコマンドに対して構成情報を有効にするには「オプション」を使います

コマンドの名前には動詞を使うようにし、オプションの名前には形容詞か名詞 (-color、-parallel、-projectname など) を使うことを推奨します

どの種類の引数を構成するかに関係なく、次のガイドラインを検討してください。

引数識別子名の大文字と小文字の区別を確認します。コマンドラインで大文字と小文字の区別を求めると、ユーザーは -FullName または -fullname のどちらを指定するか非常に混乱します。

コマンドライン解析のためのテストを作成します。このようなテストは、Execute や OnExecute などのメソッドで比較的簡単に実行できます。

特定の引数を名前で識別するのが難しい場合、あるいは複数値の指定が許可されていても、それぞれの値のプレフィックスとしてオプションの識別子を用意するのが面倒な場合は「引数」を使います

コンソールを挿入およびキャプチャしてテストできるようにするために、コンソール入出力をリダイレクトする場合は、IntelliTect.AssertConsole (itl.tc/Command­LineUtils) の利用を検討します

.NET Core の Command­LineUtils を使用する際にデメリットとなり得る要素が 1 つあります。それは、.NET Core のCommand­LineUtils が英語ベースで、ローカライズされていない点です。ShowHelp などで表示されるテキストはすべて英語です (一般的には、例外メッセージもローカライズされません)。通常は、あまり問題にならないかもしれません。しかし、コマンドラインはユーザーが使用するアプリケーション インターフェイスの一部なので、英語のみだと受け入れられない可能性があります。こうした理由から、以下のガイドラインが生まれます。

ローカライズが重要な場合は、 ShowHelp と ShowHin のカスタム関数を作成することを検討します

CommandLineApplication が例外をスローしないように構成している (throwOnUnexpectedArg = false) 場合は、CommandLineApplication.RemainingArguments をチェックします。

まとめ

ここ 3 年間、.NET Framework では大規模な変革が行われてきました。

  • その結果、iOS、Android、Linux のサポートなど、クロスプラットフォーム サポートが提供されるようになりました。
  • .NET Framework は、機密に属する占有アプローチから、オープン ソースと同様の完全なオープン モジュールの開発に移行しました。
  • NET Standard Library の BCL API は、サービスとしてのソフトウェア、モバイル、オンプレミス、モノのインターネット、デスクトップなど、多種多様なアプリケーションに利用できる、高度なモジュール方式の (クロス) プラットフォームへと大きくリファクタリングされました。
  • .NET は、戦略や計画がほとんどないと軽視されていた Windows 8 の時代から生まれ変わりました。

まだ新しい .NET Core 1.0 を使い始めていない方は、習得に多くの時間を割くためにも、今が始める絶好のチャンスです。以前のバージョンから .NET Core 1.0 へのアップグレードを考えている方は、今すぐアップグレードしましょう。いずれアップグレードするのであれば、早く始めるほど新しい機能を早く習得できます。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。彼は約 20 年間 Microsoft MVP に認定され、2007 年から Microsoft Regional Director を務めています。Michaelis は、C#、Microsoft Azure、SharePoint、Visual Studio ALM など、マイクロソフト ソフトウェアの設計レビュー チームにも所属しています。開発者を対象としたカンファレンスで講演を行い、多数の書籍を執筆しています。最近では、『Essential C# 6.0 (5th Edition)』(Addison-Wesley Professional、2015 年) を執筆しました (itl.tc/EssentialCSharp、英語)。連絡先は、Facebook (facebook.com/Mark.Michaelis、英語)、ブログ (IntelliTect.com/Mark、英語)、Twitter (@markmichaelis、英語)、または電子メール mark@IntelliTect.com (英語のみ) です。

この記事のレビューに協力してくれた IntelliTect 技術スタッフの Phil Spokas と Michael Stokesbary に心より感謝いたします。