Поделиться через


System.CommandLine Руководство по миграции 2.0.0-beta5+

Основной целью выпуска версии 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.CommandLine

  • AddMiddleware удален. Он замедлил запуск приложения, и функции могут быть выражены без него.

  • 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 мс).

См. также