В одной из прошлых статей я писал о том, как рефакторил 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 run class 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 run class 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 |
|
|
|
|
|
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 ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Synopsis / usage string generation |
✅ |
❌ |
❌ |
❌ |
❌ |
Named flags ( |
✅ |
✅ |
✅ |
✅ |
✅ |
Positional arguments |
✅ |
✅ |
✅ |
✅ |
✅ |
True/false flag pairs ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Short + long aliases ( |
✅ |
✅ |
✅ |
✅ |
✅ |
Combined short flags ( |
✅ |
✅ |
✅ |
✅ |
✅ |
|
✅ |
✅ |
✅ |
✅ |
✅ |
|
✅ |
✅ |
❌ |
❌ |
❌ |
Repeated flags as collection |
✅ |
✅ |
✅ |
✅ |
✅ |
Pathspec ( |
✅ |
❌ |
✅ |
❌ |
❌ |
Enum mapping from flags |
✅ |
✅ |
✅ |
✅ |
✅ |
Bit-flag (Flags) enum |
✅ |
❌ |
❌ |
❌ |
❌ |
Tuple splitting from single arg |
✅ |
❌ |
❌ |
❌ |
❌ |
Dictionary from repeated flags |
✅ |
❌ |
❌ |
❌ |
✅ (via IDictionary option) |
Pipeline command sequences |
✅ |
❌ |
❌ |
❌ |
❌ |
Primitives ( |
✅ |
✅ |
✅ |
✅ |
✅ |
|
✅ |
⚠️ |
✅ |
⚠️ |
⚠️ via custom converter |
|
✅ |
⚠️ |
✅ |
⚠️ |
⚠️ |
|
✅ |
❌ |
✅ |
✅ |
❌ |
Any |
✅ |
⚠️ |
⚠️ |
⚠️ |
❌ |
Custom converter |
✅ |
✅ |
✅ |
✅ |
✅ ( |
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) |
✅ |
|
✅ |
❌ |
❌ |
✅ ( |
❌ |
Allowed-values list |
✅ |
❌ |
⚠️ Custom validator |
❌ |
⚠️ Custom validator |
Existing file validation |
✅ |
❌ |
⚠️ |
❌ |
⚠️ Custom |
Existing directory validation |
✅ |
❌ |
⚠️ |
❌ |
⚠️ Custom |
Legal filename characters |
✅ |
❌ |
❌ |
❌ |
❌ |
Mutual exclusion ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Mutual requirement ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Conditional dependency ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Ordering constraint ( |
✅ |
❌ |
❌ |
❌ |
❌ |
Custom |
❌ |
❌ |
✅ |
❌ |
✅ |
Error position reporting |
✅ |
❌ |
❌ |
❌ |
❌ |
Env var fallback per argument |
✅ |
❌ |
✅ |
❌ |
❌ |
|
✅ |
❌ |
❌ |
❌ |
❌ |
Config file / |
❌ |
❌ |
⚠️ Via host builder |
✅ (via |
❌ |
Response file support ( |
❌ |
❌ |
✅ |
❌ |
❌ |
Auto-generated help ( |
✅ |
✅ |
✅ |
✅ |
✅ |
|
✅ |
✅ |
✅ |
✅ |
✅ |
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 |
❌ |
❌ |
❌ |
❌ |
✅ ( |
F# support |
❌ |
✅ (separate pkg) |
✅ |
❌ |
❌ |
Благодаря этому проекту я узнал немало нового о возможностях командных строк. Раньше я их просто использовал, не особо задумываясь, как они работают.
Пользуйтесь. Принимаются любые предложения по улучшению и баг-репорты (мог упустить особо мудрёные случаи, хотя, кажется, уже должно покрываться 99,9% случаев).
Комментарии (10)

navferty
21.05.2026 06:18Совместима ли эта библиотека с Cocona? Так чтобы можно было регистрировать эти опции в DI-контейнере, и использовать их стандартным образом, через внедрение зависимостей

ProgerMan Автор
21.05.2026 06:18Эта штука как-то прошла мимо меня. Возможности использования через DI нет, т.к. подразумевалось мапить аргументы первой же командой, а потом уже работать с тем, что получили.

iamkisly
21.05.2026 06:18Чем мне нравится экосистема dotnet это тем, что дефолтные решения существуют неплохого качества и закрывают топ потребностей разработчика. Если они этого не делают, то ваша тема или супернишевая, или вам "захотелось странного". Это я к тому что есть System.CommandLine и Spectre, они максимально гибкие, первый является стандартом для внутренних разработок ms, наример, используется в dotnet core tools, и из статьи не понятно в чем преимущество стороннего решения. Таблица не помогает, и к тому же требует проверки. Где вот этот наглядный импрув, чтобы читатель сразу сказал, ну да это однозначно надо звезду поставить и плюсик в карму ?
Лично я бы не стал пользоваться. Минимальная гибкость, смешение ответственности, глобальные статические коллбеки.. отсутствие контекстной конфигурации или стратегий, возможности подкинуть свой сериализатор, а еще все обмазано рефлексей вместо кодогенерации.. список можно и дальше продолжать.

ProgerMan Автор
21.05.2026 06:18Мне не нравится, когда мы описываем каждую команду через пачку кода. Мне хотелось сделать максимально компактное решение. Плюсом оно сразу проверяет порядок аргументов, повторяемость, некоторую логику (определённый аргумент можно указать только после другого или только один из множества). А в случае ошибки получаем ещё и позицию аргумента, где она возникла.
Если же мы выполняем аргументы сразу по мере их чтения, то это очень плохая идея. Мы можем что-то выполнить (удалить файл, например), а потом узнать, что у нас ошибка в аргументах. На мой взгляд, надо сначала прочитать все аргументы, провалидировать их, а потом уже работать. Моя библиотека решает первый шаг целиком и второй частично (какая-то сложная валидация остаётся на пользователя).

iamkisly
21.05.2026 06:18Мне не нравится, когда мы описываем каждую команду через пачку кода
Пишем свой кодогенератор для System.CommandLine, или берем из nuget.. или смотрим что там написали до нас и пишем свой более легковесный, не принципиально. Вы потратите столько же строк если не меньше.
Плюсом оно сразу проверяет порядок аргументов, повторяемость, некоторую логику (определённый аргумент можно указать только после другого или только один из множества). А в случае ошибки получаем ещё и позицию аргумента, где она возникла.
И все это умеет System.CommandLine, вам только следует описать обертку если вы хотите модно использовать дженерики. Главное, что вы берете готовый активно поддерживаемый инструмент с перспективами. Долгосрочная же поддержка ваших велосипедов объективно сомнительна. Не воспринимайте как личную обиду, но это же opensource, даже если проект выстрелил и стал хитом сейчас, это не гарантирует его поддержку.. а включение в тулинг ms - да.
Если же мы выполняем аргументы сразу по мере их чтения, то это очень плохая идея. Мы можем что-то выполнить (удалить файл, например), а потом узнать, что у нас ошибка в аргументах
Мне из текста не понятна ситуация, где аргументы "выполняются" сразу после чтения. Для чего? Понимаете кто пишет такой код? Вы исправляли баги за пьяным орангутаном который это написал, и в процессе сделали решение которое делать было не надо.

Kerman
21.05.2026 06:18Вот жалко, что в таблице нет Native AOT compability. Да в принципе я уже вижу, что у вас примерно всё на рефлекшене, а значит в native aot работать не будет.
Жаль. Как раз сейчас мне нужен парсинг команд cli для native aot приложения.
impwx
В вашем примере с
app pipeline pull --fetch commit -m "fix" push runвы объявляете иrun, и--fetchпараметром. Непонятно, как оно делит поток на команды и их аргументы, и что будет делать в случае неоднозначности (например, если объявить[ArgsPipelineCommand("run")], то оно воспримется как отдельная команда или как параметр вExecConfig?ProgerMan Автор
Всё, что между командами, является их параметрами. На push команды заканчиваются и дальше может быть что угодно. В данном случае это поле корневого класса. В случае неоднозначности будет ошибка. Нельзя сделать и команду, и поле run. Но у разных команд параметры могут повторяться.
iamkisly
Абсолютно контринтуитивно