rksoftware

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

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

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

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

というわけで、引数を扱うコードをメモしておきます。まだいろいろ不完全なので今後いじっていきたいコードのちょっとバージョンアップ版コードです。
今回は引数を格納するオブジェクトを record にできるようにしました。

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

今回はインスタンスを生成する前にパラメーターに対応するプロパティの名前がわからなければならないので、パラメーター名のリストを static にしてみました。 引数を格納するクラスには次のインタフェースを実装してもらいます。

public interface IArguments
{
    static IEnumerable<string> ParameterNames { get; } = new string[] { };
}

実装例

record struct Arguments(string? Aparam, string? Bparam, string? A, string? B, string? C, bool D, bool E) : IArguments
{
     public static IEnumerable<string> ParameterNames => new[] { nameof(Aparam), nameof(Bparam) };
}

■ 引数を解釈するコード

一つのメソッドの中にひたすら書き続けたので、かなり読みにくいので今後書き換えていきたいです。
ただ読むのは大変ですが、コピペで使うには楽かもしれません。

public static class ArgumentsBuilder
{
    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 parameterNames = ((IEnumerable<string>)(argumentsType.GetProperty(nameof(IArguments.ParameterNames))?.GetValue(null) ?? new string[0])).ToList();
        foreach (var o in options.Where(o => parameterNames.Contains(o.Key.Name)).ToArray()) options.Remove(o.Key);
        foreach (var o in switchs.Where(o => parameterNames.Contains(o.Key.Name)).ToArray()) options.Remove(o.Key);

        var constractor = argumentsType.GetConstructors().OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();
        T arguments = constractor switch
        {
            null => new(),
            _ => ((Func<T>)(() =>
            {
                var constractorParameterLength = constractor.GetParameters().Length;
                var constractorParameters = constractor.GetParameters();
                var constractorParameterValues = constractorParameters.Select(p =>
                {
                    var name = p.Name ?? string.Empty;
                    if (parameterNames.Contains(name))
                    {
                        parameterNames.Remove(name);
                        var value = parameters.FirstOrDefault();
                        if (parameters.Count > 0) parameters.RemoveAt(0);
                        return value;
                    }

                    {
                        var option = options.FirstOrDefault(o => o.Key.Name == name);
                        if (!string.IsNullOrWhiteSpace(option.Key?.Name))
                        {
                            options.Remove(option.Key);
                            return option.Value;
                        }
                    }

                    {
                        var @switch = switchs.FirstOrDefault(o => o.Key.Name == name);
                        if (!string.IsNullOrWhiteSpace(@switch.Key?.Name))
                        {
                            switchs.Remove(@switch.Key);
                            return @switch.Value;
                        }
                    }

                    return (object?)null;
                }).ToArray();

                return (T)constractor.Invoke(constractorParameterValues);
            }))(),
        };

        foreach (var parameter in Enumerable.Zip(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

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