diff --git a/ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj b/ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj
new file mode 100644
index 0000000..4722a42
--- /dev/null
+++ b/ArgumentsToolkit.Core/ArgumentsToolkit.Core.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+ net8.0
+
+
+ ArgumentsToolkit.Core
+ 0.1.0
+ FrigaT
+ FrigaT
+ Ядро библиотеки ArgumentsToolkit: парсинг аргументов, генерация строки, обязательные параметры, базовые конвертеры.
+ cli arguments parser toolkit core
+ https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit
+ https://git.frigat.duckdns.org/FrigaT/ArgumentsToolkit
+ MIT
+
+
+ true
+ true
+ snupkg
+
+
+ true
+ enable
+ enable
+ latest
+
+
+
diff --git a/ArgumentsToolkit.Core/Attributes/OptionAttribute.cs b/ArgumentsToolkit.Core/Attributes/OptionAttribute.cs
new file mode 100644
index 0000000..88a0931
--- /dev/null
+++ b/ArgumentsToolkit.Core/Attributes/OptionAttribute.cs
@@ -0,0 +1,49 @@
+namespace ArgumentsToolkit;
+
+///
+/// Атрибут для описания параметра командной строки.
+/// Позволяет задать имя, короткое имя, описание, обязательность и значение по умолчанию.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public class OptionAttribute : Attribute
+{
+ ///
+ /// Поолное название для --Name
+ ///
+ public string Name { get; }
+
+ ///
+ /// Краткое название для -ShortName
+ ///
+ public string? ShortName { get; }
+
+ ///
+ /// Описание параметра
+ ///
+ public string? Description { get; }
+
+ ///
+ /// Обязательный параметр
+ ///
+ public bool Required { get; }
+
+ ///
+ /// Значение по умолчанию
+ ///
+ public object? DefaultValue { get; }
+
+ public OptionAttribute(
+ string name,
+ string? shortName = null,
+ string? description = null,
+ bool required = false,
+ object? defaultValue = null
+ )
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ ShortName = shortName;
+ Description = description;
+ Required = required;
+ DefaultValue = defaultValue;
+ }
+}
diff --git a/ArgumentsToolkit.Core/Attributes/OptionConverterAttribute.cs b/ArgumentsToolkit.Core/Attributes/OptionConverterAttribute.cs
new file mode 100644
index 0000000..a6e9e24
--- /dev/null
+++ b/ArgumentsToolkit.Core/Attributes/OptionConverterAttribute.cs
@@ -0,0 +1,15 @@
+namespace ArgumentsToolkit;
+
+///
+/// Атрибут для указания кастомного конвертера для свойства.
+///
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+public sealed class OptionConverterAttribute : Attribute
+{
+ public Type ConverterType { get; }
+
+ public OptionConverterAttribute(Type converterType)
+ {
+ ConverterType = converterType;
+ }
+}
diff --git a/ArgumentsToolkit.Core/Converters/Converters.cs b/ArgumentsToolkit.Core/Converters/Converters.cs
new file mode 100644
index 0000000..0b63eab
--- /dev/null
+++ b/ArgumentsToolkit.Core/Converters/Converters.cs
@@ -0,0 +1,93 @@
+using System.Globalization;
+
+namespace ArgumentsToolkit;
+
+public class DateTimeOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(DateTime);
+ public object Convert(Type targetType, string value) => DateTime.Parse(value);
+}
+
+public class TimeSpanOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(TimeSpan);
+ public object Convert(Type targetType, string value) => TimeSpan.Parse(value);
+}
+
+public class JsonOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => true;
+
+ public object Convert(Type targetType, string value)
+ {
+ return System.Text.Json.JsonSerializer.Deserialize(value, targetType)!;
+ }
+}
+
+public class BoolOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(bool);
+
+ public object Convert(Type targetType, string value)
+ {
+ // когда указали просто флаг без значения
+ if (string.IsNullOrEmpty(value)) { return true; }
+
+ if (bool.TryParse(value, out var b)) { return b; }
+
+ if (value == "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { return true; }
+ if (value == "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase)) { return true; }
+
+ return false;
+ }
+}
+
+public class EnumOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType.IsEnum;
+
+ public object Convert(Type targetType, string value)
+ {
+ return Enum.Parse(targetType, value, ignoreCase: true);
+ }
+}
+
+public class IntOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(int);
+
+ public object Convert(Type targetType, string value)
+ {
+ return int.Parse(value, CultureInfo.InvariantCulture);
+ }
+}
+
+public class LongOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(long);
+
+ public object Convert(Type targetType, string value)
+ {
+ return long.Parse(value, CultureInfo.InvariantCulture);
+ }
+}
+
+public class DoubleOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(double);
+
+ public object Convert(Type targetType, string value)
+ {
+ return double.Parse(value, CultureInfo.InvariantCulture);
+ }
+}
+
+public class DecimalOptionConverter : IOptionConverter
+{
+ public bool CanConvert(Type targetType) => targetType == typeof(decimal);
+
+ public object Convert(Type targetType, string value)
+ {
+ return decimal.Parse(value, CultureInfo.InvariantCulture);
+ }
+}
\ No newline at end of file
diff --git a/ArgumentsToolkit.Core/Converters/IOptionConverter.cs b/ArgumentsToolkit.Core/Converters/IOptionConverter.cs
new file mode 100644
index 0000000..9410e10
--- /dev/null
+++ b/ArgumentsToolkit.Core/Converters/IOptionConverter.cs
@@ -0,0 +1,17 @@
+namespace ArgumentsToolkit;
+
+///
+/// Интерфейс для конвертера значений аргументов.
+///
+public interface IOptionConverter
+{
+ ///
+ /// Проверяет, может ли данный конвертер обработать указанный тип.
+ ///
+ bool CanConvert(Type targetType);
+
+ ///
+ /// Преобразует строковое значение в указанный тип.
+ ///
+ object Convert(Type targetType, string value);
+}
diff --git a/ArgumentsToolkit.Core/Converters/TypeConverters.cs b/ArgumentsToolkit.Core/Converters/TypeConverters.cs
new file mode 100644
index 0000000..1af1f9d
--- /dev/null
+++ b/ArgumentsToolkit.Core/Converters/TypeConverters.cs
@@ -0,0 +1,43 @@
+using System.Globalization;
+
+namespace ArgumentsToolkit;
+
+internal static class TypeConverters
+{
+ public static bool TryConvert(string input, Type targetType, out object? value)
+ {
+ value = null;
+ try
+ {
+ if (targetType == typeof(string)) { value = input; return true; }
+ if (targetType == typeof(bool))
+ {
+ if (string.IsNullOrEmpty(input)) { value = true; return true; }
+ if (bool.TryParse(input, out var b)) { value = b; return true; }
+ if (input == "0" || input.Equals("false", StringComparison.OrdinalIgnoreCase)) { value = false; return true; }
+ if (input == "1" || input.Equals("true", StringComparison.OrdinalIgnoreCase)) { value = true; return true; }
+ return false;
+ }
+ if (targetType.IsEnum) { value = Enum.Parse(targetType, input, ignoreCase: true); return true; }
+ if (targetType == typeof(int)) { value = int.Parse(input, CultureInfo.InvariantCulture); return true; }
+ if (targetType == typeof(long)) { value = long.Parse(input, CultureInfo.InvariantCulture); return true; }
+ if (targetType == typeof(double)) { value = double.Parse(input, CultureInfo.InvariantCulture); return true; }
+ if (targetType == typeof(decimal)) { value = decimal.Parse(input, CultureInfo.InvariantCulture); return true; }
+ if (targetType == typeof(Uri)) { value = new Uri(input, UriKind.RelativeOrAbsolute); return true; }
+
+ // Nullable
+ if (Nullable.GetUnderlyingType(targetType) is Type underlying)
+ {
+ if (string.IsNullOrEmpty(input)) { value = null; return true; }
+ return TryConvert(input, underlying, out value);
+ }
+
+ // Custom types with string ctor
+ var ctor = targetType.GetConstructor(new[] { typeof(string) });
+ if (ctor != null) { value = ctor.Invoke(new object[] { input }); return true; }
+
+ return false;
+ }
+ catch { return false; }
+ }
+}
diff --git a/ArgumentsToolkit.Core/Errors/ArgumentError.cs b/ArgumentsToolkit.Core/Errors/ArgumentError.cs
new file mode 100644
index 0000000..9860275
--- /dev/null
+++ b/ArgumentsToolkit.Core/Errors/ArgumentError.cs
@@ -0,0 +1,3 @@
+namespace ArgumentsToolkit;
+
+public record ArgumentError(ErrorCode Code, string Message, string? Option = null);
\ No newline at end of file
diff --git a/ArgumentsToolkit.Core/Errors/ErrorCode.cs b/ArgumentsToolkit.Core/Errors/ErrorCode.cs
new file mode 100644
index 0000000..466cde0
--- /dev/null
+++ b/ArgumentsToolkit.Core/Errors/ErrorCode.cs
@@ -0,0 +1,14 @@
+namespace ArgumentsToolkit;
+
+///
+/// Ошибки парсера
+///
+public enum ErrorCode
+{
+ UnknownOption,
+ MissingValue,
+ InvalidValue,
+ MissingRequired,
+ DuplicateOption,
+ ConversionFailed,
+}
diff --git a/ArgumentsToolkit.Core/Exceptions.cs b/ArgumentsToolkit.Core/Exceptions.cs
new file mode 100644
index 0000000..9f052b8
--- /dev/null
+++ b/ArgumentsToolkit.Core/Exceptions.cs
@@ -0,0 +1,10 @@
+namespace ArgumentsToolkit;
+
+///
+/// Исключение для отсутствующих обязательных параметров.
+///
+public class MissingRequiredOptionException : Exception
+{
+ public MissingRequiredOptionException(string optionName)
+ : base($"Обязательный параметр '--{optionName}' не указан.") { }
+}
diff --git a/ArgumentsToolkit.Core/Parsers/ArgumentsParser.cs b/ArgumentsToolkit.Core/Parsers/ArgumentsParser.cs
new file mode 100644
index 0000000..622bf13
--- /dev/null
+++ b/ArgumentsToolkit.Core/Parsers/ArgumentsParser.cs
@@ -0,0 +1,308 @@
+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)
+ {
+ 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 (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