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