rksoftware

Visual Studio とか C# とかが好きです

コマンドライン引数を雑に扱う (2)

以前にコマンドライン引数を雑に扱う記事を書きました。

コンソールアプリ、ちょっとした処理を行う際に雑に作ってしまうのが結構いいのですが、何気にコマンドライン引数の解釈コードが面倒です。 それはもう、あらゆる要素をソースコードに書き込んで毎回ソースコード上の値を書き換えてビルドして使うほどに。

というわけで、引数を扱うコードをメモしておきます。まだいろいろ不完全なので今後いじっていきたいコードのちょっとバージョンアップ版コードです。

■ 今回使う言葉

今回、-/ で始まる引数をオプションまたはスイッチと呼びます。

オプション

-/ で始まる引数の次の引数が対応する値となる場合、オプション。

スイッチ

-/ の後に値を取らない (実体としては bool 型プロパティに true をセットする) 場合、スイッチと呼びます。

パラメーター

-/ で始まる引数でもその値でもないものをパラメーターと呼びます。

よくわからないと思うのでこの記事の最後の実例を見てください。

■ 引数オブジェクト用インタフェース

今回は引数を格納するクラスをユーザーが作って、そのオブジェクトに引数の値を詰めるようにしてみました。
引数を格納するクラスには次のインタフェースを実装してもらいます。

public interface IArguments
{
    IEnumerable<string> ParameterNames { get; }
}

実装例

class Arguments :IArguments
{
    // パラメータを頭から格納するプロパティの名前リスト(リストの先頭から順にその名前を持つプロパティに値をセットしていく)
    public IEnumerable<string> ParameterNames => new[] { nameof(Aparam), nameof(Bparam) };

    public string? Aparam { get; set; }
    public string? Bparam { get; set; }
    public string? A { get; set; }
    public string? B { get; set; }
    public string? C { get; set; }
    public bool D { get; set; }
    public bool E { get; set; }
}

■ 引数を解釈するコード

public static T Parse<T>(string[] args) where T : IArguments, new()
{
    var argumentsType = typeof(T);
    List<string> parameters = new();
    Dictionary<PropertyInfo, string> options = new();
    Dictionary<PropertyInfo, bool> switchs = new();
    PropertyInfo? optionProperty = default;

    foreach (var arg in args.Select(arg => arg ?? string.Empty))
    {
        {
            var pre = arg.First();
            if (new[] { '-', '/' }.Contains(pre))
            {
                var optionName = new string(arg.SkipWhile(c => c == pre).ToArray());
                optionProperty = argumentsType.GetProperty(optionName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (optionProperty == null) continue;
                if (optionProperty.PropertyType == typeof(bool))
                {
                    switchs.Add(optionProperty, true);
                    optionProperty = null;
                    continue;
                }
                options.Add(optionProperty, "");
                continue;
            }
        }
 
        if (optionProperty == null)
        {
            parameters.Add(arg);
            continue;
        }
 
        options[optionProperty] = arg;
        optionProperty = null;
    }
 
    var arguments = new T();
    foreach (var parameter in Enumerable.Zip(arguments.ParameterNames, parameters)) argumentsType.GetProperty(parameter.First)?.SetValue(arguments, parameter.Second);
    foreach (var option in options) option.Key.SetValue(arguments, option.Value);
    foreach (var @switch in switchs) @switch.Key.SetValue(arguments, @switch.Value);
 
    return arguments;
}

■ 使う方

<実行ファイル名> aparam -a aopt -d -b bopt bparam /c copt
var parsed = ArgumentsBuilder.Parse<Arguments>(args);

// パラメーター
parsed.Aparam; // aparam
parsed.Bparam; // bparam;

// オプション
parsed.A; // aopt
parsed.B; // bopt
parsed.C; // copt

// スイッチ
parsed.D; // true
parsed.E; // false

雑に作るコンソールアプリ用のコピペ元としてはこんな感じでいいでしょうかね。

ただ、このコード少し言語仕様的に古いと思うのでまたアップデートしたいです。