以前にコマンドライン引数を雑に扱う記事を書きました。
コンソールアプリ、ちょっとした処理を行う際に雑に作ってしまうのが結構いいのですが、何気にコマンドライン引数の解釈コードが面倒です。 それはもう、あらゆる要素をソースコードに書き込んで毎回ソースコード上の値を書き換えてビルドして使うほどに。
というわけで、引数を扱うコードをメモしておきます。まだいろいろ不完全なので今後いじっていきたいコードのちょっとバージョンアップ版コードです。
今回は引数を格納するオブジェクトを 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
雑に作るコンソールアプリ用のコピペ元としてはこんな感じでいいでしょうかね。