Files
FrigaT 363c078321
All checks were successful
CI / build-test (push) Successful in 29s
Release / pack-and-publish (release) Successful in 35s
Доработано формирование аргументов
2025-11-27 10:41:57 +03:00

320 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Reflection;
namespace ArgumentsToolkit;
/// <summary>
/// Основной парсер аргументов командной строки.
/// Преобразует массив строковых аргументов в объект указанного типа.
/// </summary>
public static class ArgumentsParser
{
private static readonly List<IOptionConverter> _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<TConverter>()
where TConverter : IOptionConverter, new()
=> _converters.Add(new TConverter());
/// <summary>
/// Парсит массив аргументов <paramref name="args"/> в объект типа <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Тип модели опций, содержащий свойства с атрибутами <see cref="OptionAttribute"/>.</typeparam>
/// <param name="args">Массив аргументов командной строки.</param>
/// <returns>Результат парсинга, содержащий объект и список ошибок.</returns>
public static ParseResult<T> Parse<T>(string[] args)
where T : class, new()
{
var result = new ParseResult<T> { Success = true, Value = new T() };
var props = typeof(T).GetProperties();
var optionMap = BuildOptionMap(typeof(T));
var valuesByProperty = new Dictionary<PropertyInfo, object?>();
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<OptionConverterAttribute>();
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<OptionAttribute>()!;
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;
}
/// <summary>
/// Формирование строки аргументов
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
public static string ToArguments<T>(T options, bool useShortName = false)
{
var props = typeof(T).GetProperties();
var parts = new List<KeyValuePair<string, string>>();
foreach (var prop in props)
{
var opt = prop.GetCustomAttribute<OptionAttribute>();
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<object>())));
}
else if (prop.PropertyType.IsGenericType &&
prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var dict = (System.Collections.IDictionary)value;
var items = new List<string>();
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<T>
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;
}
}
/// <summary>
/// Проверяет, является ли токен аргументом (начинается с '-' или '--').
/// </summary>
private static bool IsOptionToken(string token) => token.StartsWith("-");
/// <summary>
/// Строит карту имён аргументов к свойствам модели.
/// </summary>
private static Dictionary<string, PropertyInfo> BuildOptionMap(Type optionsType)
{
var map = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var prop in optionsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var opt = prop.GetCustomAttribute<OptionAttribute>();
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;
}
}