C# で 属性を定義することで楽にコマンドライン解析を行う

以前こんな記事を書きましたが、

* [NDesk.Options を使ってコマンドライン解析を楽にする #c# #Ndesk.Options #Unite - Qiita](http://qiita.com/items/0b536d32c9e214261299)

coma2n さんのこちらの記事に、属性とリフレクションを用いるやり方が書かれていました。
こちらの方が、オプションを定義する際に楽ですね。

* [NDesk.Options(Mono) - コマンドラインパーサー - Architect Life](http://d.hatena.ne.jp/coma2n/20091210/1260433596)

# 使い方

下記ソースコード冒頭のコメントを参照のこと。

# 実行結果

ヘルプメッセージはこんな感じで表示されます。

![](https://lh3.googleusercontent.com/-PY0fSXKS0g8/UGHMuB4tg0I/AAAAAAAAEqo/p_WGtU4GlEo/s800/2012-09-25_23h52_40.png)


# ソースコード

自分なりに咀嚼してみたのが以下のコード。
実行するには、別途 NDesk.Options をインストールしてください。

```csharp:CommandlineParser.cs
// # 参考
// * [NDesk.Options(Mono) - コマンドラインパーサー - Architect Life](http://d.hatena.ne.jp/coma2n/20091210/1260433596)

/* 
# CommandlineParser クラスの使い方

    // 自分のアプリケーション用のオプションクラスを定義
    public class MyOptions
    {
        [CommandlineOption("h", "help", Description = "このヘルプを表示します。", OptionType = CommandlineOptionType.BoolSwitch)]
        public bool NeedToShowHelp { get; set; }

        [CommandlineOption("n", "name", Description = "名前を指定します。")]
        public string Name { get; set; }

        [CommandlineOption("s", "switch", Description = "スイッチ", OptionType = CommandlineOptionType.BoolSwitch)]
        public bool Switch { get; set; }
    }

    // コマンドライン引数解析
    var parser = new CommandlineParser();
    var commandline = parser.Parse<MyOptions>( Environment.GetCommandLineArgs() );

    // 解析されたオプション
    commandline.Options.NeedToShowHelp
    commandline.Options.Name
    commandline.Options.Switch
    
    // (オプションと認識されなかった)その他のコマンドライン引数
    commandline.Values

    // ヘルプメッセージの表示
    Console.WriteLine( parser.GetHelpMessage<MyOptions>() );
    
 */

namespace Rohinomiya
{
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System;

    using NDesk.Options;

    
    /// <summary>
    /// コマンドラインオプションのタイプ
    /// </summary>
    public enum CommandlineOptionType
    {
        /// <summary>
        /// ノーマルスイッチ（ファイル名等を１つ引数に取る）
        /// </summary>
        Normal,
        /// <summary>
        /// 単なるBooleanスイッチ
        /// </summary>
        BoolSwitch
    }
    
    /// <summary>
    /// CommandlineOption カスタム属性の定義
    /// NDesk.Options.Add() への引数を生成するための情報を保持する
    /// </summary>
    public class CommandlineOptionAttribute : Attribute
    {
        /// <summary>
        /// オプションの名称(--help とかの help の部分)
        /// </summary>
        public List<string> Names;
        
        /// <summary>
        /// オプションの説明
        /// </summary>
        public string Description;
        
        /// <summary>
        /// オプションのタイプ
        /// </summary>
        public CommandlineOptionType OptionType;

        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public CommandlineOptionAttribute()
        {
            Names = new List<string>();
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="names">オプション名(例：オプションが -h -help であれば "h", "help" )</param>
        public CommandlineOptionAttribute(params string [] names)
        {
            this.Names = names.ToList();
        }
    }

    /// <summary>
    /// NDesk.Options を使ってコマンドラインを解析するクラス
    /// </summary>
    public sealed class CommandlineParser {
        /// <summary>
        /// バインディング、およびリフレクションによるメンバーと型の検索方法を制御するフラグ
        /// </summary>
        private static readonly BindingFlags bindingFlags =
            BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance;

        /// <summary>
        /// 指定したコマンドライン引数を解析します。
        /// </summary>
        /// <typeparam name="TOptions">オプション定義クラスの型</typeparam>
        /// <param name="args">コマンドライン引数</param>
        /// <returns>解析結果</returns>
        public CommandlineParseResult<TOptions> Parse<TOptions>(IEnumerable<string> args) {
            var opts = Activator.CreateInstance<TOptions>();
            var extra = CreateOptSet<TOptions>(opts).Parse(args);   // optsのプロパティに値がセットされる

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

        /// <summary>
        /// コマンドラインオプションの説明文を取得する
        /// </summary>
        /// <returns>コマンドラインオプションの説明文</returns>
        public string GetCommandlineUsage<TOptions>() {
            using(var sw = new StringWriter()) {
                CreateOptSet<TOptions>().WriteOptionDescriptions(sw);
                return sw.ToString();
            }
        }

        OptionSet CreateOptSet<TOptions>() {
            return CreateOptSet<TOptions>(default(TOptions));
        }
        
        /// <summary>
        /// コマンドライン引数を解析するために必要な OptionSet オブジェクトを生成する
        /// </summary>
        /// <param name="opts">CommandlineOption属性を指定されたオプションクラスのインスタンス</param>
        /// <returns>値がセットされたNDesk.Option.OptionSet オブジェクト</returns>
        OptionSet CreateOptSet<TOptions>(TOptions opts) {

            var optSet = new OptionSet();
            
            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);


            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(string prototype, string description, Action<T> action)
                optSet.Add(prototype,
                           pa.Attr.Description,
                           // インスタンスoptsのプロパティに値をセットする
                           v => propInfo.SetValue(
                               opts,
                               (optType == CommandlineOptionType.BoolSwitch) ?
                               (object) v != null :
                               (object) Convert.ChangeType(v, propInfo.PropertyType),
                               null
                              )
                          );
            }
            return optSet;
        }
        

        /// <summary>
        /// ヘルプメッセージの取得
        /// </summary>
        /// <returns>ヘルプメッセージ</returns>
        public string GetHelpMessage<TOptions>()
        {
            var descriptionOfOptions = GetCommandlineUsage<TOptions>();
            
            var vi = FileVersionInfo.GetVersionInfo(PathEx.MyExecutablePath());
            
            var sb = new StringBuilder();
            sb.AppendLine(string.Format("{0} {1}",vi.ProductName, vi.ProductVersion));
            sb.AppendLine(vi.Comments); // sb.AppendLine(vi.FileDescription);
            sb.AppendLine();
            sb.AppendLine("[Usage]:");
            sb.AppendLine(string.Format("{0} [Options] [files..]",PathEx.MyExecutableFileName()));
            sb.AppendLine();
            sb.AppendLine("[Options]:");
            sb.AppendLine(descriptionOfOptions);
            
            return sb.ToString();
        }
    }

    /// <summary>
    /// コマンドライン引数の解析結果を保持するクラス
    /// </summary>
    /// <typeparam name="TOptions">オプションの型</typeparam>
    public sealed class CommandlineParseResult<TOptions> {
        /// <summary>
        /// オプションを覗いた残りの引数
        /// </summary>
        private List<string> values = new List<string>();
        
        /// <summary>
        /// オプション
        /// </summary>
        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(values) = コンストラクター初期化子：引数１つのコンストラクタを呼ぶ
        {
            this.options = options;
        }
    }
    
}

```