Arguments to Config — простая и мощная библиотека для парсинга аргументов в CLI-приложении на C#

от автора

В одной из прошлых статей я писал о том, как рефакторил CLI-сервис на C#, в котором не был реализован маппинг аргументов в класс конфигураций. Пришлось писать свой.

Есть разные библиотеки для этого, в т.ч. от MS. Но эта слишком сложная, другие не очень понравились. А главное то, что нет реализаций пайплайна, т.е. в сервис передаётся набор команд с параметрами и значениями, которые потом выполняются по порядку.

Прошло время и мне пришло в голову своё решение. Я намеренно не смотрел, как было сделано у других, а просто начал собирать то, что могло смапить самые популярные команды, типа git commit со всеми параметрами. А, главное, он должен в конечном итоге решать мою задачу. Шаг за шагом я собрал свою библиотеку, обложив её тестами.

Ключевой особенностью является то, что в ней нет других зависимостей и нет настроек — всё необходимое описывается через понятные атрибуты. А полученные значения можно сохранить даже в словарь коллекций, используя всего два атрибута (один получает значение, другой его раскладывает).

И, конечно же, пайплайны.

Представьте. Вы теперь легко можете написать свой сервис, который будет одной командой по порядку собирать проект на python, запускать тесты. закидывать в docker-контейнер и публиковать в Azure, а потом ещё и curl’ом сходит посмотреть, перемешав всё это с необходимой бизнес-логикой. Но команды могут и повторяться.

Я очень люблю, когда к проекту написан подробный README с работающими примерами (ну, или хотя бы есть ссылка на подробную документацию), поэтому снабдил свой проект подробным описанием со всеми возможными случаями использования. В тестах также можно увидеть множество вариантов использования библиотеки. Я там постарался учесть все возможные случаи.

Тут исходники: https://github.com/Mansiper/ArgsToConfig
Тут пакет: https://www.nuget.org/packages/Mansiper.ArgsToConfig

Фичи библиотеки:

  • поддержка множества базовых типов, а также возможность конвертации в вообще любой;

  • базовая поддержка типа tuple (больше нет ни у кого);

  • поддержка команд —help и —version;

  • возможность преобразования обратно в команду;

  • генерация справки;

  • поддержка pathspec;

  • поддержка любых атрибутов, унаследованных от ValidationAttribute;

  • поддержка списков команд (пайплайнов)

  • поддержка коллекций;

  • поддержка вложенных классов;

  • гибкая поддержка самых разных типов

  • и мн. др.

Пример кода:

// Команда: app connect -u alice -p secret runclass AppConfig{    [ArgsObject("connect", Description = "Параметры подключения")]    public ConnectionConfig Connect { get; set; } = null!;    [ArgsHasParameter("run")]    public bool? Run { get; set; }}record ConnectionConfig{    [ArgsValueFor("-u")]    public string User { get; set; } = null!;    [ArgsValueFor("-p")]    public string Pass { get; set; } = null!;}// А для преобразования просто вызываем в самом начале программы:var (config, errors, position) = ArgumentsReader.ToObject<AppConfig>(args);

Пример для пайплайна:

//команда: app pipeline pull --fetch commit -m "fix" push runclass ExecConfig{    [ArgsHasParameter("pipeline")]    public bool? Pipeline { get; set; }    [ArgsPipeline]    public IPipelineCommand[]? Commands { get; set; }    [ArgsHasParameter("run")]    public bool? Run { get; set; }}interface IPipelineCommand { }[ArgsPipelineCommand("pull")]class PullCommand : IPipelineCommand{    [ArgsHasParameter("--fetch")]    public bool? Fetch { get; set; }}[ArgsPipelineCommand("commit")]class CommitCommand : IPipelineCommand{    [ArgsValueFor("-m")]    public string? Message { get; set; }}[ArgsPipelineCommand("push")]class PushCommand : IPipelineCommand{    [ArgsHasParameter("--force")]    public bool? Force { get; set; }}

Что мне помогло всё это сделать?

Помимо того, что я бы заколебался писать всю эту логику преобразования на 1600 строк без помощи LLM, добавляя туда всё новые фичи, мне помог Claude с анализом других сервисов. Когда я закончил всё то, что придумал изначально, закинул в Claude ссылку на свою библиотеку с уже готовым подробным описанием в README. Он «прочитал» о моей библиотеке и сравнил с существующими, предложив ещё несколько фич, о которых я даже не подозревал. Также он косвенно подкинул пару других идей. Например, поддержку ValidationAttribute. Реализовав почти все из них, я попросил ещё раз сравнить с другими решениями, на что он мне выдал следующую сравнительную таблицу.

Тут очень большая таблица

Mansiper.ArgsToConfig

(моя библиотека)

CommandLineParser

System.CommandLine

McMaster.Extensions.CommandLineUtils

Spectre.Console.Cli

NuGet package

Mansiper.ArgsToConfig

CommandLineParser

System.CommandLine

McMaster.Extensions.CommandLineUtils

Spectre.Console.Cli

Maintainer

Community (Mansiper)

Community

Microsoft

Community (natemcmaster)

Community (.NET Foundation)

Stable since

< 1.0 (pre-release)

2005

2024 (beta → stable)

2017

2021

API style

Attribute-only

Attribute + Fluent

Imperative builder

Attribute + Builder

Attribute (opinionated)

License

MIT

MIT

MIT

Apache 2.0

MIT

Target frameworks

.NET 8+ (modern)

.NET Standard 2.0, .NET Fx 4.0+

.NET 6+

.NET Standard 2.0+

.NET Standard 2.0, .NET 8+

External dependencies

None

None

None

None

Spectre.Console

Attribute-based declaration

⚠️ Partial (via DragonFruit)

Fluent builder API

Method-parameter inference

Returns errors without exceptions

⚠️ (callback-based)

⚠️ (exception or callback)

⚠️

⚠️

Object-to-args round-trip (ToArgs)

Synopsis / usage string generation

Named flags (--verbose)

Positional arguments

True/false flag pairs (--sign/--no-sign)

Short + long aliases (-v|--verbose)

Combined short flags (-am → -a -m)

--flag=value syntax

-fflag inline value (-o./bin)

Repeated flags as collection

Pathspec (-- separator)

Enum mapping from flags

Bit-flag (Flags) enum

Tuple splitting from single arg

Dictionary from repeated flags

✅ (via IDictionary option)

Pipeline command sequences

Primitives (stringintbooldouble, …)

DateTimeDateOnlyTimeOnlyTimeSpan

⚠️ DateTime only

⚠️ DateTime only

⚠️ via custom converter

GuidUriVersionIPAddress

⚠️ Uri via ctor

✅ Uri, others via converter

⚠️

⚠️

FileInfoDirectoryInfo

Any IConvertible

⚠️

⚠️

⚠️

Custom converter

✅ (TypeConverter)

Value tuples

Nullable types (optional by nullability)

Records & structs as config objects

Subcommand support

✅ (verbs)

Arbitrary nesting depth

Record/struct as subcommand

Global/recursive options

Async command execution

DI integration

✅ (via DI)

System.ComponentModel.DataAnnotations

✅ ([Required][Range])

Allowed-values list

⚠️ Custom validator

⚠️ Custom validator

Existing file validation

⚠️

⚠️ Custom

Existing directory validation

⚠️

⚠️ Custom

Legal filename characters

Mutual exclusion ([ArgsOneOf])

Mutual requirement ([ArgsMutuallyRequired])

Conditional dependency ([ArgsIfSet])

Ordering constraint ([ArgsAfter])

Custom Validate() override

Error position reporting

Env var fallback per argument

.env file auto-loading

Config file / appsettings.json support

⚠️ Via host builder

✅ (via IConfiguration)

Response file support (@file)

Auto-generated help (--help)

--version flag

Help grouping / sections

Cached help generation

Unknown-argument callback

Rich terminal output (colors, tables)

✅ (via Spectre.Console)

Shell tab completion

❌ (via Cocona: yes)

NuGet downloads (approx.)

< 1 K (new)

80 M+

50 M+

25 M+

30 M+

GitHub stars (approx.)

0

4 K+

3 K+

2 K+

1 K+ (part of Spectre.Console 10 K+)

Active maintenance

⚠️ Slow

⚠️ Slow

.NET Foundation member

Test helper package

✅ (Spectre.Console.Testing)

F# support

✅ (separate pkg)

Благодаря этому проекту я узнал немало нового о возможностях командных строк. Раньше я их просто использовал, не особо задумываясь, как они работают.

Пользуйтесь. Принимаются любые предложения по улучшению и баг-репорты (мог упустить особо мудрёные случаи, хотя, кажется, уже должно покрываться 99,9% случаев).

ссылка на оригинал статьи https://habr.com/ru/articles/1037556/