NDesk.Options(Mono) - コマンドラインパーサー

Monoでコマンドラインパーサーがあったので使ってみた。

使い方はこんな感じ

string data = null;
bool help   = false;
int verbose = 0;

var p = new OptionSet () {
  { "file=",      v => data = v },
  { "v|verbose",  v => { ++verbose } },
  { "h|?|help",   v => help = v != null }
};
List<string> extra = p.Parse(args);

OptionSetAddメソッドが定義されていて、以下のようなシグネチャのメソッドが定義されている。

  • Add(string prototype, Action action);
  • Add(string prototype, string description, Action action)

prototype引数には、コマンドラインのオプション引数の引数名を指定する。|(パイプ)はorを意味する。

v|verbose

と指定した場合は、コマンドライン引数として、

PS > コマンド名 -v
PS > コマンド名 -verbose

のように渡すことができる。

末尾に=(イコール)を付けた場合は、

file=

以下のように引数名と値という形で指定できるようになる。

PS > コマンド名 -file=100.txt

description引数はその名の通り、オプション引数の説明になる。これはOptionSetクラスに定義されているWriteOptionDescriptionsメソッドというコマンドラインの説明文を出力するメソッドを呼び出した時にその出力に含まれるようになる。

action引数には、コマンドラインをパースする中で実際にオプション引数が見つかった時にその値をどのように処理するかを委譲するデリゲートを指定する。

これらの設定を行ってから、Parseメソッドを呼び出すと戻り値として、オプション以外のコマンドライン引数がコレクションとして返ってくる。

使い易いといえば使い易いけど、値の設定にデリゲートを渡すという発想が、完全に一般のデベロッパを無視しているので、もう少し使いやすくするために属性ベースで値を取得できるラッパを作ってみた。

CommandlineParser

使い方は以下のようになる。
まず、オプション引数を格納するクラスを定義し、値を格納するプロパティをCommandlineOption属性でマークする。

MyOptions.cs

class MyOptions {
    [CommandlineOption("n", "name", Description = "名前を指定します。")]
    public string Name { get; set; }
    [CommandlineOption("s", "switch", Description = "スイッチ", OptionType = CommandlineOptionType.Switch)]
    public bool Switch { get; set; }
    [CommandlineOption("value")]
    public int Value { get; set; }
}

後はCommandlineParserクラスをインスタンス化し、Parseメソッドを呼び出すだけ。

[Test]
public void コマンドラインをパースできる() {
    // コマンドライン引数
    var args = new[] { "-n=hoge", "-switch", "value1", "value2", "-value=20" };

    var result = cmdlineParser.Parse<MyOptions>(args);

    Assert.That(result, Is.Not.Null);
    Assert.That(result.Values, Has.Count.EqualTo(2));
    // Valuesプロパティにオプション引数以外の値が入っている。
    Assert.That(result.Values[0], Is.EqualTo("value1"));
    Assert.That(result.Values[1], Is.EqualTo("value2"));

    // オプション引数がカスタム属性でマークしたプロパティに設定されている。
    Assert.That(result.Options.Name, Is.EqualTo("hoge"));
    Assert.That(result.Options.Switch, Is.True);
    Assert.That(result.Options.Value, Is.EqualTo(20));

    // コマンドラインの説明文を取得できる。
    Console.WriteLine(cmdlineParser.GetCommandlineUsage<MyOptions>());
}

戻り値としてCommandlineParseResultが返ってくるので、そこからオプション引数の情報を取得できるようになっている。

探せば色々とあるものですね。

以下ソース

CommandlineParser.cs

using System;
using System.IO;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;

using NDesk.Options;

/// <summary>
/// コマンドラインを解析するクラス
/// </summary>
public sealed class CommandlineParser {
    /// <summary>
    /// 取得するプロパティの条件
    /// </summary>
    private static readonly BindingFlags bindingFlags =
        BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance;

    /// <summary>
    /// 指定したコマンドライン引数を解析します。
    /// </summary>
    /// <typeparam name="T">オプションの型</typeparam>
    /// <param name="args">コマンドライン引数</param>
    /// <returns>解析結果</returns>
    public CommandlineParseResult<T> Parse<T>(IEnumerable<string> args) {
        var opts = Activator.CreateInstance<T>();
        var extra = CreateOptSet<T>(opts).Parse(args);

        return new CommandlineParseResult<T>(extra, opts);
    }

    public string GetCommandlineUsage<TOptions>() {
        using(var sw = new StringWriter()) {
            CreateOptSet<TOptions>().WriteOptionDescriptions(sw);

            return sw.ToString();
        }
    }

    OptionSet CreateOptSet<TOptions>() {
        return CreateOptSet<TOptions>(default(TOptions));
    }
    OptionSet CreateOptSet<TOptions>(TOptions opts) {
        var tp = typeof(TOptions);

        var propAttrs = tp.GetProperties(bindingFlags)
            .Select(p => {
                var attr = p.GetCustomAttributes(typeof(CommandlineOptionAttribute), true).FirstOrDefault();

                return attr != null ? new { Prop = p, Attr = (CommandlineOptionAttribute)attr } : null;
            })
            .Where(o => o != null);

        var optSet = new OptionSet();

        foreach(var pa in propAttrs) {
            var optType = pa.Attr.OptionType;
            var propInfo = pa.Prop;
            var prototype = string.Join("|", pa.Attr.Names.ToArray());

            // Normalなら=を付加する。
            if(optType == CommandlineOptionType.Normal) prototype += "=";

            optSet.Add(prototype, pa.Attr.Description,
                v => propInfo.SetValue(opts,
                    (optType == CommandlineOptionType.Switch) ?
                        (object)v != null :
                        (object)Convert.ChangeType(v, propInfo.PropertyType), null
                )
            );
        }
        return optSet;
    }
}

/// <summary>
/// コマンドライン引数の解析結果を格納するクラス
/// </summary>
/// <typeparam name="TOptions">オプションの型</typeparam>
public sealed class CommandlineParseResult<TOptions> {
    private List<string> values = new List<string>();
    private TOptions options;
    /// <summary>
    /// コマンドラインの値の一覧を取得します(読み取り専用)。
    /// </summary>
    public ReadOnlyCollection<string> Values { get { return values.AsReadOnly(); } }
    /// <summary>
    /// コマンドラインのオプションを取得します。
    /// </summary>
    public TOptions Options { get { return options; } }

    /// <summary>
    /// 指定した値の一覧を設定するコンストラクタ
    /// </summary>
    /// <param name="values">オプション</param>
    public CommandlineParseResult(IEnumerable<string> values) {
        this.values.AddRange(values);
    }
    /// <summary>
    /// 指定した値の一覧とオプションを設定するコンストラクタ
    /// </summary>
    /// <param name="values">値の一覧</param>
    /// <param name="options">オプション</param>
    public CommandlineParseResult(IEnumerable<string> values, TOptions options)
        : this(values) {
        this.options = options;
    }
}