309 lines
11 KiB
C#
309 lines
11 KiB
C#
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;
|
||
}
|
||
|
||
public static string ToArguments<T>(T options)
|
||
{
|
||
var props = typeof(T).GetProperties();
|
||
var parts = new List<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 (prop.PropertyType == typeof(bool))
|
||
{
|
||
if ((bool)value)
|
||
parts.Add(prefix);
|
||
}
|
||
else if (prop.PropertyType.IsGenericType &&
|
||
prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
|
||
{
|
||
var list = (System.Collections.IList)value;
|
||
if (list.Count > 0)
|
||
parts.Add($"{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($"{prefix} {string.Join(",", items)}");
|
||
}
|
||
else
|
||
{
|
||
parts.Add($"{prefix} {value}");
|
||
}
|
||
}
|
||
|
||
return string.Join(" ", parts);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
}
|
||
|