Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Основной целью выпуска версии 2.0.0-beta5 является улучшение API и переход к выпуску стабильной версии System.CommandLine. API были упрощены и сделаны более последовательными и согласованы с рекомендациями по проектированию Платформы. В этой статье описываются критические изменения, внесенные в версии 2.0.0-beta5 и 2.0.0-beta7, а также причины их возникновения.
Переименование
В версии 2.0.0-beta4 не все типы и члены следуют рекомендациям по именованию. Некоторые не были согласованы с соглашениями об именовании, например, с использованием префикса Is для логических свойств. В версии 2.0.0-beta5 были переименованы некоторые типы и члены. В следующей таблице показаны старые и новые имена:
| Старое имя | Новое имя |
|---|---|
System.CommandLine.Parsing.Parser |
CommandLineParser |
System.CommandLine.Parsing.OptionResult.IsImplicit |
Implicit |
System.CommandLine.Option.IsRequired |
Required |
System.CommandLine.Symbol.IsHidden |
Hidden |
System.CommandLine.Option.ArgumentHelpName |
HelpName |
System.CommandLine.Parsing.OptionResult.Token |
IdentifierToken |
System.CommandLine.Parsing.ParseResult.FindResultFor |
GetResult |
System.CommandLine.Parsing.SymbolResult.ErrorMessage |
AddError(String)† |
Для того чтобы разрешить сообщение о нескольких ошибках для одного и того же символа, свойство ErrorMessage было преобразовано в метод и переименовано в AddError.
Изменяемые коллекции параметров и проверяющих элементов
Версия 2.0.0-beta4 имела множество Add методов, которые использовались для добавления элементов в коллекции, таких как аргументы, параметры, подкоманды, валидаторы и завершения. Некоторые из этих коллекций были представлены через свойства как коллекции, доступные только для чтения. Из-за этого невозможно было удалить элементы из этих коллекций.
В версии 2.0.0-beta5 API были изменены для предоставления изменяемых коллекций вместо Add методов и (иногда) коллекций только для чтения. Это позволяет не только добавлять элементы или перечислять их, но и удалять их. В следующей таблице показаны старые методы и новые имена свойств:
| Старое имя метода | Новое свойство |
|---|---|
Command.AddArgument |
Command.Arguments.Add |
Command.AddOption |
Command.Options.Add |
Command.AddCommand |
Command.Subcommands.Add |
Command.AddValidator |
Command.Validators.Add |
Option.AddValidator |
Option.Validators.Add |
Argument.AddValidator |
Argument.Validators.Add |
Command.AddCompletions |
Command.CompletionSources.Add |
Option.AddCompletions |
Option.CompletionSources.Add |
Argument.AddCompletions |
Argument.CompletionSources.Add |
Command.AddAlias |
Command.Aliases.Add |
Option.AddAlias |
Option.Aliases.Add |
Методы RemoveAlias и HasAlias также были удалены, так как свойство Aliases теперь является изменяемой коллекцией. Вы можете использовать метод Remove для удаления псевдонима из коллекции.
Contains Используйте метод для проверки наличия псевдонима.
Имена и псевдонимы
До 2.0.0-beta5 не было четкого разделения между именем и псевдонимами символа. Когда name не был предоставлен конструктору Option<T>, символ отобразил свое имя как самый длинный псевдоним, из которого удалены такие префиксы, как --, -, или /. Это было запутано.
Кроме того, чтобы получить проанализированное значение, необходимо было сохранить ссылку на параметр или аргумент, а затем использовать его для получения значения из ParseResult.
Для повышения простоты и явности имя символа теперь является обязательным параметром для каждого конструктора символов (включая Argument<T>). Концепция имени и псевдонимов теперь отделена: псевдонимы являются только псевдонимами и не включают имя символа. Конечно, они необязательные. В результате были внесены следующие изменения:
-
nameтеперь является обязательным аргументом для каждого общедоступного конструктора Argument<T>, Option<T>а также Command. В случаеArgument<T>, он не используется для синтаксического анализа, а для создания справки. В случаеOption<T>иCommandэто используется для идентификации символа во время синтаксического анализа, а также для помощи и автозавершения. - Свойство Symbol.Name больше
virtualне является; теперь оно доступно только для чтения и возвращает имя, как оно было указано при создании символа. Из-за этогоSymbol.DefaultNameбыл удалён, а Symbol.Name больше не удаляет--,-,/или любой другой префикс из самого длинного псевдонима. - Свойство
Aliases, предоставляемоеOptionиCommand, теперь является изменяемой коллекцией. Эта коллекция больше не включает имя символа. -
System.CommandLine.Parsing.IdentifierSymbolудален (он был базовым типом для обоихCommandиOption).
Всегда присутствующее имя позволяет получить проанализированное значение по имени:
RootCommand command = new("The description.")
{
new Option<int>("--number")
};
ParseResult parseResult = command.Parse(args);
int number = parseResult.GetValue<int>("--number");
Создание параметров с помощью псевдонимов
В прошлом Option<T> были доступны многочисленные конструкторы, некоторые из которых принимали имя. Так как имя теперь является обязательным, и псевдонимы часто предоставляются для Option<T>, существует только один конструктор. Он принимает имя и массив params псевдонимов.
До 2.0.0-beta5 Option<T> имел конструктор, который принимал имя и описание. Из-за этого второй аргумент теперь может рассматриваться как псевдоним, а не описание. Это единственное известное критическое изменение в API, которое не приводит к ошибке компилятора.
Обновите любой код, который передал описание конструктору, чтобы использовать новый конструктор, который принимает имя и псевдонимы, а затем задать Description свойство отдельно. Рассмотрим пример.
Option<bool> beta4 = new("--help", "An option with aliases.");
beta4b.Aliases.Add("-h");
beta4b.Aliases.Add("/h");
Option<bool> beta5 = new("--help", "-h", "/h")
{
Description = "An option with aliases."
};
Значения по умолчанию и пользовательский анализ
В версии 2.0.0-beta4 можно задать значения по умолчанию для параметров и аргументов с помощью SetDefaultValue методов. Эти методы принимали значение object, которое не было типобезопасным и могло привести к ошибкам во время выполнения, если значение окажется несовместимым с опцией или типом аргумента.
Option<int> option = new("--number");
// This is not type safe, as the value is a string, not an int:
option.SetDefaultValue("text");
Кроме того, некоторые OptionArgument из конструкторов приняли делегат синтаксического анализа () и логическое значение (parseisDefault), указывающее, был ли делегат пользовательским анализатором или поставщиком значений по умолчанию, который запутался.
Option<T> и Argument<T> классы теперь имеют DefaultValueFactory свойство, которое можно использовать для задания делегата, который можно вызвать, чтобы получить значение по умолчанию для параметра или аргумента. Этот делегат вызывается, когда параметр или аргумент не найден в входных данных командной строки синтаксического анализа.
Option<int> number = new("--number")
{
DefaultValueFactory = _ => 42
};
Argument<T> и Option<T> также оснащены свойством CustomParser, которое можно использовать для задания пользовательского средства синтаксического анализа для символа.
Argument<Uri> uri = new("arg")
{
CustomParser = result =>
{
if (!Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uriValue))
{
result.AddError("Invalid URI format.");
return null;
}
return uriValue;
}
};
Кроме того, CustomParser принимает делегат типа Func<ParseResult,T>, а не делегат предыдущего типа ParseArgument. Это и несколько других пользовательских делегатов были удалены, чтобы упростить API и уменьшить количество типов, предоставляемых API, что сокращает время запуска, затраченное на компиляцию JIT.
Дополнительные примеры использования DefaultValueFactory и CustomParserсм. в разделе "Настройка синтаксического анализа и проверки в System.CommandLine".
Разделение синтаксического анализа и вызова
В 2.0.0-beta4 можно было разделить синтаксический анализ и вызов команд, но это не было ясно, как это сделать.
Command не предоставлял Parse метод, но CommandExtensions предоставлял Parse, Invoke, и InvokeAsync методы расширения для Command. Это было запутано, так как не было ясно, какой метод следует использовать и когда. Для упрощения API были внесены следующие изменения:
-
Command теперь предоставляет
Parseметод, возвращающийParseResultобъект. Этот метод используется для анализа входных данных командной строки и возврата результата операции синтаксического анализа. Более того, становится ясно, что команда не вызывается, а только анализируется, и это происходит исключительно синхронно. -
ParseResultтеперь предоставляет методыInvokeиInvokeAsync, которые можно использовать для вызова команды. Эта схема ясно показывает, что команда вызывается после разбора и допускает как синхронный, так и асинхронный вызов. - Класс
CommandExtensionsбыл удален, так как он больше не нужен.
Конфигурация
До 2.0.0-beta5 можно было настроить синтаксический анализ, но только с некоторыми из общедоступных Parse методов.
Parser Существовал класс, который предоставлял два публичных конструктора: один, принимающий Command, и другой, принимающий CommandLineConfiguration.
CommandLineConfiguration был неизменяемым, и для его создания необходимо было использовать шаблон построителя, предоставляемый классом CommandLineBuilder . Для упрощения API были внесены следующие изменения:
-
CommandLineConfigurationбыл разделен на два изменяемых класса (в версии 2.0.0-beta7): ParserConfiguration и InvocationConfiguration. Создание конфигурации вызова теперь так же просто, как создание экземпляраInvocationConfigurationи настройка свойств, которые требуется настроить. - Каждый
Parseметод теперь принимает необязательный ParserConfiguration параметр, который можно использовать для настройки синтаксического анализа. Если он не указан, используется конфигурация по умолчанию. - Чтобы избежать конфликтов имен,
Parserбыло переименовано в CommandLineParser для устранения неоднозначности с другими типами парсеров. Так как он без состояния, теперь это статический класс только со статическими методами. Он открывает два методаParseсинтаксического анализа: один принимаетIReadOnlyList<string> args, другой принимаетstring args. Последний использует CommandLineParser.SplitCommandLine(String) (также общедоступный метод) для разделения входных данных командной строки на токены перед анализом.
CommandLineBuilderExtensions также удален. Вот как можно сопоставить свои методы с новыми API:
CancelOnProcessTerminationтеперь является свойством InvocationConfiguration, называемым ProcessTerminationTimeout. Он включен по умолчанию с 2 секундой ожидания. Чтобы отключить его, задайте для него значениеnull. Дополнительные сведения см. в разделе "Время ожидания завершения процесса".EnableDirectives, ,UseEnvironmentVariableDirectiveUseParseDirectiveиUseSuggestDirectiveбыли удалены. Был введен новый тип директивы , и RootCommand теперь предоставляет Directives свойство. С помощью этой коллекции можно добавлять, удалять и итерировать директивы. Директива предложения включена по умолчанию; Вы также можете использовать другие директивы, такие как DiagramDirective или EnvironmentVariablesDirective.EnableLegacyDoubleDashBehaviorудален. Все несопоставленные токены теперь открыты свойством ParseResult.UnmatchedTokens. Дополнительные сведения см. в разделе "Несовпаденные токены".EnablePosixBundlingудален. Объединение теперь включено по умолчанию, его можно отключить, задав ParserConfiguration.EnablePosixBundling для свойства значениеfalse. Дополнительные сведения см. в разделе EnablePosixBundling.RegisterWithDotnetSuggestбыл удален при выполнении дорогостоящих операций, как правило, во время запуска приложения. Теперь необходимо зарегистрировать командыdotnet suggestвручную.UseExceptionHandlerудален. Обработчик исключений включается по умолчанию; его можно отключить, задав свойству InvocationConfiguration.EnableDefaultExceptionHandler значениеfalse. Это полезно, если вы хотите обрабатывать исключения особым образом, просто обернув методыInvokeилиInvokeAsyncв блок try-catch. Дополнительные сведения см. в разделе EnableDefaultExceptionHandler.UseHelpиUseVersionбыли удалены. Теперь справка и версия предоставляются общедоступными типами HelpOptionVersionOption . Они оба включены по умолчанию в параметры, определенные RootCommand. Дополнительные сведения см. в разделе "Настройка выходных данных справки " и параметра "Версия".UseHelpBuilderудален. Дополнительные сведения о настройке выходных данных справки см. в разделе "Настройка справки".System.CommandLineAddMiddlewareудален. Он замедлил запуск приложения, и функции могут быть выражены без него.UseParseErrorReportingиUseTypoCorrectionsбыли удалены. Ошибки синтаксического анализа теперь сообщаются по умолчанию при вызовеParseResult. Его можно настроить с помощью действия ParseErrorAction, предоставляемого свойством ParseResult.Action.ParseResult result = rootCommand.Parse("myArgs", config); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = true; parseError.ShowHelp = false; }UseLocalizationResourcesиLocalizationResourcesбыли удалены. Эта функция использовалась в основном CLI для добавления отсутствующих переводов вdotnet. Все эти переводы были перенесены в сам System.CommandLine, поэтому эта функция больше не нужна. Если поддержка вашего языка отсутствует, сообщите о проблеме.UseTokenReplacerудален. Файлы ответа включены по умолчанию, но их можно отключить, изменив значение свойства на ResponseFileTokenReplacernull. Вы также можете предоставить пользовательскую реализацию для настройки обработки файлов ответов.
Последнее, но не последнее, IConsole и все связанные интерфейсы (IStandardOut, IStandardError, ) IStandardInбыли удалены.
InvocationConfiguration предоставляет два TextWriter свойства: Output и Error. Эти свойства можно задать для любого TextWriter экземпляра, например StringWriterэкземпляра, который можно использовать для записи выходных данных для тестирования. Мотивация этого изменения заключалась в том, чтобы предоставить меньше типов и повторно использовать существующие абстракции.
Инвокация
В версии 2.0.0-beta4 интерфейс ICommandHandler предоставил методы Invoke и InvokeAsync, которые использовались для вызова разобранной команды. Это позволило легко смешивать синхронный и асинхронный код, например путем определения синхронного обработчика для команды, а затем вызова его асинхронно (что может привести к взаимоблокировки). Кроме того, можно было определить обработчик только для команды, но не для опции (например, справка) или директив.
Появился новый абстрактный базовый класс CommandLineAction и два производных класса SynchronousCommandLineActionAsynchronousCommandLineAction. Первый используется для синхронных действий, возвращающих int код выхода, а последний используется для асинхронных действий, возвращающих Task<int> код выхода.
Для определения действия не требуется создать производный тип. Вы можете использовать метод Command.SetAction, чтобы задать действие для команды. Синхронное действие может быть делегатом, который принимает System.CommandLine.ParseResult параметр и возвращает int код выхода (или ничего не возвращает, в этом случае возвращается код выхода по умолчанию 0). Асинхронное действие может быть делегатом, который принимает System.CommandLine.ParseResult и CancellationToken параметры и возвращает Task<int> (или Task для получения кода выхода по умолчанию).
rootCommand.SetAction(ParseResult parseResult =>
{
FileInfo parsedFile = parseResult.GetValue(fileOption);
ReadFile(parsedFile);
});
В прошлом CancellationToken, переданный через InvokeAsync, был предоставлен обработчику через метод InvocationContext.
rootCommand.SetHandler(async (InvocationContext context) =>
{
string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption);
var token = context.GetCancellationToken();
returnCode = await DoRootCommand(urlOptionValue, token);
});
Большинство пользователей не получали этот токен и не передавали его дальше.
CancellationToken теперь является обязательным аргументом для асинхронных действий, таким образом, что компилятор создает предупреждение, когда оно не передается дальше (см. CA2016).
rootCommand.SetAction((ParseResult parseResult, CancellationToken token) =>
{
string? urlOptionValue = parseResult.GetValue(urlOption);
return DoRootCommandAsync(urlOptionValue, token);
});
В результате этих и других вышеупомянутых изменений InvocationContext класс также был удален. Элемент ParseResult теперь передается непосредственно в действие, так что вы можете получить доступ к проанализированным значениям и параметрам непосредственно из действия.
Чтобы суммировать эти изменения, выполните следующие действия.
- Интерфейс
ICommandHandlerбыл удален.SynchronousCommandLineActionиAsynchronousCommandLineActionбыли введены. - Метод
Command.SetHandlerбыл переименован в SetAction. - Свойство
Command.Handlerбыло переименовано Command.Actionв .Optionбыл расширен с Option.Action. -
InvocationContextудален. ТеперьParseResultпередается непосредственно в действие.
Дополнительные сведения об использовании действий см. в статье "Анализ и вызов команд в System.CommandLine".
Преимущества упрощенного API
Изменения, внесенные в версии 2.0.0-beta5, делают API более последовательным, подготовленным к будущему и упрощают его использование как для существующих, так и для новых пользователей.
Новые пользователи должны узнать меньше понятий и типов, так как количество общедоступных интерфейсов снизилось с 11 до 0, а общедоступные классы (и структуры) снизились с 56 до 38. Число общедоступных методов сократилось с 378 до 235, а общедоступные свойства — с 118 до 99.
Число сборок, на которые ссылается System.CommandLine, уменьшено с 11 до 6.
System.Collections
- System.Collections.Concurrent
- System.ComponentModel
System.Console
- System.Diagnostics.Process
System.Linq
System.Memory
- System.Net.Primitives
System.Runtime
- System.Runtime.Serialization.Formatters
+ System.Runtime.InteropServices
- System.Threading
Размер библиотеки сокращается (на 32%), а вместе с ним и размер приложений NativeAOT, использующих эту библиотеку.
Простота также улучшила производительность библиотеки (это побочный эффект работы, а не основной цели). Тесты показывают, что синтаксический анализ и вызов команд теперь быстрее, чем в версии 2.0.0-beta4, особенно для больших команд с множеством параметров и аргументов. Улучшения производительности отображаются как в синхронных, так и в асинхронных сценариях.
Самое простое приложение, представленное ранее, приводит к следующим результатам:
BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4061/24H2/2024Update/HudsonValley)
AMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.300
[Host] : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
Job-JJVAFK : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
EvaluateOverhead=False OutlierMode=DontRemove InvocationCount=1
IterationCount=100 UnrollFactor=1 WarmupCount=3
| Method | Args | Mean | StdDev | Ratio |
|------------------------ |--------------- |----------:|---------:|------:|
| Empty | --bool -s test | 63.58 ms | 0.825 ms | 0.83 |
| EmptyAOT | --bool -s test | 14.39 ms | 0.507 ms | 0.19 |
| SystemCommandLineBeta4 | --bool -s test | 85.80 ms | 1.007 ms | 1.12 |
| SystemCommandLineNow | --bool -s test | 76.74 ms | 1.099 ms | 1.00 |
| SystemCommandLineNowR2R | --bool -s test | 69.35 ms | 1.127 ms | 0.90 |
| SystemCommandLineNowAOT | --bool -s test | 17.35 ms | 0.487 ms | 0.23 |
Как видно, время запуска (тесты сообщают время, необходимое для выполнения данного исполняемого файла), улучшилось на 12% по сравнению с 2.0.0-beta4. Если вы компилируете приложение с помощью NativeAOT, это всего 3 мс медленнее, чем приложение NativeAOT, которое не анализирует аргы вообще (EmptyAOT в таблице выше). Кроме того, если исключить затраты на пустое приложение (63,58 мс), разбор на 40% быстрее, чем в 2.0.0-beta4 (22.22 мс против 13.66 мс).