using System.Reflection; namespace ArgumentsToolkit; /// /// Основной парсер аргументов командной строки. /// Преобразует массив строковых аргументов в объект указанного типа. /// public static class ArgumentsParser { private static readonly List _converters = new() { new DateTimeOptionConverter(), new TimeSpanOptionConverter(), new BoolOptionConverter(), new IntOptionConverter(), new LongOptionConverter(), new DoubleOptionConverter(), new DecimalOptionConverter(), new EnumOptionConverter(), }; public static void RegisterConverter(IOptionConverter converter) => _converters.Add(converter); public static void RegisterConverter() where TConverter : IOptionConverter, new() => _converters.Add(new TConverter()); /// /// Парсит массив аргументов в объект типа . /// /// Тип модели опций, содержащий свойства с атрибутами . /// Массив аргументов командной строки. /// Результат парсинга, содержащий объект и список ошибок. public static ParseResult Parse(string[] args) where T : class, new() { var result = new ParseResult { Success = true, Value = new T() }; var props = typeof(T).GetProperties(); var optionMap = BuildOptionMap(typeof(T)); var valuesByProperty = new Dictionary(); for (int i = 0; i < args.Length; i++) { var token = args[i]; if (!IsOptionToken(token)) { result.Errors.Add(new ArgumentError(ErrorCode.UnknownOption, $"Не является токеном '{token}'")); result.Success = false; continue; } if (!optionMap.TryGetValue(token, out var prop)) { result.Errors.Add(new ArgumentError(ErrorCode.UnknownOption, $"Неизвестный токен '{token}'", token)); result.Success = false; continue; } var targetType = prop.PropertyType; // Значение аргумента string? raw = null; if (targetType == typeof(bool) || targetType == typeof(bool?)) { // Флаг: --flag или --flag=true if (i + 1 < args.Length && !IsOptionToken(args[i + 1])) raw = args[++i]; else raw = string.Empty; // true } else { if (i + 1 >= args.Length || IsOptionToken(args[i + 1])) { result.Errors.Add(new ArgumentError(ErrorCode.MissingValue, $"Токен '{token}' ожидает значение")); result.Success = false; continue; } raw = args[++i]; } // Выбор конвертера var converterAttr = prop.GetCustomAttribute(); var converter = converterAttr != null ? (IOptionConverter)Activator.CreateInstance(converterAttr.ConverterType)! : null; if (TryParse(targetType, raw, converter, out var converted)) { //TODO: Generic List / Dictionary if (valuesByProperty.ContainsKey(prop)) { result.Errors.Add(new ArgumentError(ErrorCode.DuplicateOption, $"Токен '{token}' указан несколько раз")); result.Success = false; continue; } valuesByProperty[prop] = converted; } else { result.Errors.Add(new ArgumentError(ErrorCode.ConversionFailed, $"Не найден конвертер для {targetType.Name}", token)); result.Success = false; continue; } } // Применение значений по умолчанию и проверка обязательных foreach (var prop in optionMap.Values.Distinct()) { var opt = prop.GetCustomAttribute()!; if (!valuesByProperty.TryGetValue(prop, out var value)) { if (opt.Required) { result.Errors.Add(new ArgumentError(ErrorCode.MissingRequired, $"Отсутствует обязательный токен '--{opt.Name}'", opt.Name)); result.Success = false; } else if (opt.DefaultValue is not null) { valuesByProperty[prop] = opt.DefaultValue; } } } // Заполнение объекта foreach (var kv in valuesByProperty) { kv.Key.SetValue(result.Value, kv.Value); } return result; } /// /// Формирование строки аргументов /// /// /// /// public static string ToArguments(T options, bool useShortName = false) { var props = typeof(T).GetProperties(); var parts = new List>(); foreach (var prop in props) { var opt = prop.GetCustomAttribute(); if (opt == null) continue; var value = prop.GetValue(options); if (value == null) continue; string prefix = "--" + opt.Name; if (useShortName && !string.IsNullOrWhiteSpace(opt.ShortName)) { prefix = "-" + opt.ShortName; } if (prop.PropertyType == typeof(bool)) { if ((bool)value) parts.Add(new(prefix, "")); } else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { var list = (System.Collections.IList)value; if (list.Count > 0) parts.Add(new(prefix, string.Join(",", list.Cast()))); } else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { var dict = (System.Collections.IDictionary)value; var items = new List(); foreach (var k in dict.Keys) items.Add($"{k}={dict[k]}"); if (items.Count > 0) parts.Add(new(prefix, string.Join(",", items))); } else { parts.Add(new(prefix, $"{value}")); } } return string.Join(" ", parts.Select(kvp => kvp.Key + " \"" + kvp.Value + "\"")); } private static bool TryParse(Type targetType, string input, IOptionConverter? converterAttr, out object? value) { if (converterAttr != null && converterAttr.CanConvert(targetType)) { try { value = converterAttr.Convert(targetType, input); return true; } catch { value = null; return false; } } // Nullable if (Nullable.GetUnderlyingType(targetType) is Type underlying) { if (string.IsNullOrEmpty(input)) { value = input; return true; } return TryParse(underlying, input, converterAttr, out value); } var converter = _converters.LastOrDefault(c => c.CanConvert(targetType)); if (converter != null) { try { value = converter.Convert(targetType, input); return true; } catch { value = null; return false; } } // Пользовательские типы с string ctor var ctor = targetType.GetConstructor([typeof(string)]); if (ctor != null) { value = ctor.Invoke([input]); return true; } //TODO: Generic types /* if (targetType.IsGenericType) { var genType = targetType.GetGenericTypeDefinition(); if (genType == typeof(List<>)) { var elementType = targetType.GetGenericArguments()[0]; var list = (System.Collections.IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!; foreach (var item in value.Split(',')) list.Add(Convert.ChangeType(item.Trim(), elementType)); return list; } if (genType == typeof(Dictionary<,>)) { var keyType = targetType.GetGenericArguments()[0]; var valueType = targetType.GetGenericArguments()[1]; var dict = (System.Collections.IDictionary)Activator.CreateInstance(targetType)!; foreach (var pair in value.Split(',')) { var kv = pair.Split('='); if (kv.Length == 2) { var k = Convert.ChangeType(kv[0].Trim(), keyType); var v = Convert.ChangeType(kv[1].Trim(), valueType); dict.Add(k, v); } } return dict; } } */ try { value = Convert.ChangeType(input, targetType); return true; } catch { value = null; return false; } } /// /// Проверяет, является ли токен аргументом (начинается с '-' или '--'). /// private static bool IsOptionToken(string token) => token.StartsWith("-"); /// /// Строит карту имён аргументов к свойствам модели. /// private static Dictionary BuildOptionMap(Type optionsType) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var prop in optionsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var opt = prop.GetCustomAttribute(); if (opt == null) continue; void add(string key) { if (!string.IsNullOrWhiteSpace(key)) map[key] = prop; } add("--" + opt.Name); add(("-" + opt.ShortName) ?? string.Empty); } return map; } }