.NET Core 時代のコマンドライン引数解析

.NET でコンソールアプリケーションを開発するときの悩みの種が、コマンドライン引数の解析。 .NET Frameworkコマンドラインパーサーを提供してくれないので、 仕方なくオレオレパーサーを書いたり、Mono にあるライブラリを使ったりしてきたけど、 .NET Core 時代になってようやく Microsoft がライブラリを提供してくれた。

www.nuget.org

cURL みたいな、サブコマンドを持たない CLI を書くならこんな感じになる。

using System;
using Microsoft.Extensions.CommandLineUtils;

namespace SampleCli
{
    class Program
    {
        static void Main(string[] args)
        {
            var app = new CommandLineApplication(throwOnUnexpectedArg: false);

            app.Name = nameof(SampleCli);
            app.Description = "cURL みたいな単機能 CLI";
            app.HelpOption("-h|--help");

            var methodOption = app.Option(
                template: "--method",
                description: "HTTP メソッド",
                optionType: CommandOptionType.SingleValue);

            var urlArgument = app.Argument(
                name: "url",
                description: "URL",
                multipleValues: false);

            app.OnExecute(() =>
            {
                if (urlArgument.Value == null)
                {
                    app.ShowHelp();
                    return 1;
                }

                var method = "GET";
                if (methodOption.HasValue())
                {
                    method = methodOption.Value();
                }

                Console.WriteLine($"{method} {urlArgument.Value}");
                return 0;
            });

            app.Execute(args);
        }
    }
}

Command メソッドを使えばサブコマンドも追加できるので、Git みたいな多機能な CLI も夢じゃない。

using System;
using Microsoft.Extensions.CommandLineUtils;

namespace SampleCli
{
    class Program
    {
        static int Main(string[] args)
        {
            var app = new CommandLineApplication(throwOnUnexpectedArg: false);

            app.Name = nameof(SampleCli);
            app.Description = "Git みたいな多機能 CLI";
            app.HelpOption("-h|--help");

            app.Command("clone", command =>
            {
                command.Description = "リポジトリをクローンします。";
                command.HelpOption("-h|--help");

                var urlArgument = command.Argument("url", "リポジトリの URL");

                command.OnExecute(() =>
                {
                    if (urlArgument.Value == null)
                    {
                        command.ShowHelp();
                        return 1;
                    }

                    Console.WriteLine($"clone {urlArgument.Value}");
                    return 0;
                });
            });

            app.Command("remote", remoteCommand =>
            {
                remoteCommand.Description = "リモート一覧を表示します。";
                remoteCommand.HelpOption("-h|--help");

                remoteCommand.Command("add", addCommand =>
                {
                    addCommand.Description = "リモートを追加します。";
                    addCommand.HelpOption("-h|--help");

                    var nameArgument = addCommand.Argument("name", "リモートの名前");
                    var urlArgument = addCommand.Argument("url", "リポジトリの URL");

                    addCommand.OnExecute(() =>
                    {
                        if (nameArgument.Value == null || urlArgument.Value == null)
                        {
                            addCommand.ShowHelp();
                            return 1;
                        }

                        Console.WriteLine($"remote add {nameArgument.Value} {urlArgument.Value}");
                        return 0;
                    });
                });

                remoteCommand.OnExecute(() =>
                {
                    Console.WriteLine("origin");
                    Console.WriteLine("staging");
                    Console.WriteLine("production");
                    return 0;
                });
            });

            return app.Execute(args);
        }
    }
}

サブコマンドにも対応できるくらいに高機能。自分のユースケースは全て満たしているので、もうこればかり使っている。オレオレパーサーは窓から投げ捨てた。