Compartilhar via


System.CommandLine Guia de migração 2.0.0-beta5+

O foco principal da versão 2.0.0-beta5 era melhorar as APIs e dar um passo para liberar uma versão estável de System.CommandLine. As APIs foram simplificadas e tornaram-se mais coerentes e consistentes com as diretrizes de design do Framework. Este artigo descreve as alterações interruptivas que foram feitas em 2.0.0-beta5 e 2.0.0-beta7 e o raciocínio por trás delas.

Renomear

Na versão 2.0.0-beta4, nem todos os tipos e membros seguiram as diretrizes de nomenclatura. Algumas não eram consistentes com as convenções de nomenclatura, como usar o Is prefixo para propriedades boolianas. Na versão 2.0.0-beta5, alguns tipos e membros foram renomeados. A tabela a seguir mostra os nomes antigos e novos:

Nome antigo Novo nome
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)

†Para permitir vários erros para que o mesmo símbolo seja relatado, a ErrorMessage propriedade foi convertida em um método e renomeada para AddError.

Coleções mutáveis de opções e validadores

A versão 2.0.0-beta4 tinha vários Add métodos usados para adicionar itens a coleções, como argumentos, opções, subcomandos, validadores e conclusões. Algumas dessas coleções foram disponibilizadas através de propriedades como coleções de somente leitura. Por causa disso, era impossível remover itens dessas coleções.

Na versão 2.0.0-beta5, as APIs foram alteradas para expor coleções mutáveis em vez de Add métodos e (às vezes) coleções somente leitura. Isso permite que você não apenas adicione itens ou enumere-os, mas também os remova. A tabela a seguir mostra o método antigo e os novos nomes de propriedade:

Nome do método antigo Nova propriedade
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

Os RemoveAlias métodos e os HasAlias métodos também foram removidos, pois a Aliases propriedade agora é uma coleção mutável. Você pode usar o Remove método para remover um alias da coleção. Use o Contains método para verificar se existe um alias.

Nomes e aliases

Antes da versão 2.0.0-beta5, não havia uma separação clara entre o nome e os aliases de um símbolo. Quando name não foi fornecido para o construtor Option<T>, o símbolo relatou seu nome como o aliás mais longo, com prefixos como --, -, ou / removidos. Isso foi confuso.

Além disso, para obter o valor analisado, você precisava armazenar uma referência a uma opção ou um argumento e usá-lo para obter o valor de ParseResult.

Para promover a simplicidade e a explicitação, o nome de um símbolo agora é um parâmetro obrigatório para cada construtor de símbolo (incluindo Argument<T>). O conceito de um nome e aliases agora é separado: aliases são apenas aliases e não incluem o nome do símbolo. Claro, eles são opcionais. Como resultado, as seguintes alterações foram feitas:

  • name agora é um argumento obrigatório para cada construtor público de Argument<T>, Option<T>e Command. No caso de Argument<T>, ele não é usado para análise, mas para gerar a ajuda. No caso de Option<T> e Command, ele é usado para identificar o símbolo durante a análise e também para ajuda e conclusões.
  • A Symbol.Name propriedade não é mais virtual; agora é somente leitura e retorna o nome como foi fornecido quando o símbolo foi criado. Por causa disso, Symbol.DefaultName foi removido e Symbol.Name não remove mais o --, - ou /, nem qualquer outro prefixo do alias mais longo.
  • A propriedade Aliases exposta por Option e Command agora é uma coleção mutável. Essa coleção não inclui mais o nome do símbolo.
  • System.CommandLine.Parsing.IdentifierSymbol foi removido (era um tipo base para ambos Command e Option).

Ter o nome sempre presente permite que você obtenha o valor analisado por nome:

RootCommand command = new("The description.")
{
    new Option<int>("--number")
};

ParseResult parseResult = command.Parse(args);
int number = parseResult.GetValue<int>("--number");

Criando opções com aliases

No passado, Option<T> exibia muitos construtores, alguns dos quais aceitavam o nome. Como o nome agora é obrigatório e os aliases são frequentemente fornecidos, Option<T>há apenas um único construtor. Aceita o nome e uma matriz de apelidos params.

Antes da versão 2.0.0-beta5, Option<T> tinha um construtor que usava um nome e uma descrição. Por causa disso, o segundo argumento agora pode ser tratado como um alias em vez de uma descrição. É a única alteração significativa conhecida na API que não causa um erro de compilador.

Atualize qualquer código que tenha passado uma descrição para o construtor para usar o novo construtor que usa um nome e aliases e, em seguida, defina a Description propriedade separadamente. Por exemplo:

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."
};

Valores padrão e análise personalizada

Na versão 2.0.0-beta4, você pode definir valores padrão para opções e argumentos usando os SetDefaultValue métodos. Esses métodos aceitaram um object valor, que não era tipo seguro e poderia levar a erros de runtime se o valor não fosse compatível com a opção ou o tipo de argumento:

Option<int> option = new("--number");
// This is not type safe, as the value is a string, not an int:
option.SetDefaultValue("text");

Além disso, alguns construtores Option e Argument construtores aceitaram um delegado de análise (parse) e um booliano (isDefault) indicando se o delegado era um analisador personalizado ou um provedor de valor padrão, o que era confuso.

Option<T> e Argument<T> as classes agora têm uma DefaultValueFactory propriedade que você pode usar para definir um delegado que pode ser chamado para obter o valor padrão para a opção ou argumento. Esse delegado é invocado quando a opção ou argumento não é encontrado na entrada de linha de comando analisada.

Option<int> number = new("--number")
{
    DefaultValueFactory = _ => 42
};

Argument<T> e Option<T> também vêm com uma propriedade CustomParser que você pode usar para definir um analisador personalizado para o símbolo.

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;
    }
};

Além disso, CustomParser aceita um delegado do tipo Func<ParseResult,T>, em vez do delegado anterior ParseArgument . Esse e alguns outros delegados personalizados foram removidos para simplificar a API e reduzir o número de tipos expostos pela API, o que reduz o tempo de inicialização gasto durante a compilação JIT.

Para obter mais exemplos de como usar DefaultValueFactory e CustomParser, consulte Como personalizar a análise e a validação em System.CommandLine.

Separação de análise sintática e invocação

Na versão 2.0.0-beta4, era possível separar a análise e a invocação de comandos, mas não ficou claro como fazer isso. Command não expôs um Parse método, mas CommandExtensions forneceu Parse, Invokee InvokeAsync métodos de extensão para Command. Isso foi confuso, pois não ficou claro qual método usar e quando. As seguintes alterações foram feitas para simplificar a API:

  • Command agora expõe um Parse método que retorna um ParseResult objeto. Esse método é usado para analisar a entrada da linha de comando e retornar o resultado da operação de análise. Além disso, deixa claro que o comando não é invocado, mas analisado e apenas de maneira síncrona.
  • ParseResult agora expõe tanto os métodos Invoke quanto InvokeAsync que você pode usar para invocar o comando. Esse padrão deixa claro que o comando é invocado após a análise e permite invocação síncrona e assíncrona.
  • A CommandExtensions classe foi removida, pois não é mais necessária.

Configuração

Antes da versão 2.0.0-beta5, era possível personalizar a análise, mas apenas com alguns dos métodos públicos Parse . Houve uma classe Parser que expôs dois construtores públicos: um aceitando um Command e outro aceitando um CommandLineConfiguration. CommandLineConfiguration era imutável e, para criá-lo, você tinha que usar um padrão de construtor exposto pela CommandLineBuilder classe. As seguintes alterações foram feitas para simplificar a API:

  • CommandLineConfiguration foi dividido em duas classes mutáveis (em 2.0.0-beta7): ParserConfiguration e InvocationConfiguration. A criação de uma configuração de invocação agora é tão simples quanto criar uma instância InvocationConfiguration e definir as propriedades que você deseja personalizar.
  • Cada Parse método agora aceita um parâmetro opcional ParserConfiguration que você pode usar para personalizar a análise. Quando não for fornecido, a configuração padrão será usada.
  • Para evitar conflitos de nome, Parser foi renomeado para CommandLineParser, para desambiguar de outros tipos de analisador. Como é sem estado, agora é uma classe estática com apenas métodos estáticos. Ele expõe dois Parse métodos de análise: um aceitando um IReadOnlyList<string> args e outro aceitando um string args. Este último usa CommandLineParser.SplitCommandLine(String) (também público) para dividir a entrada da linha de comando em tokens antes de analisá-la.

CommandLineBuilderExtensions também foi removido. Veja como mapear seus métodos para as novas APIs:

  • CancelOnProcessTermination agora é uma propriedade do InvocationConfiguration chamado ProcessTerminationTimeout. Ele é habilitado por padrão, com um tempo limite de 2 segundos. Para desabilitá-lo, defina-o como null. Para obter mais informações, consulte o tempo limite de término do processo.

  • EnableDirectives, UseEnvironmentVariableDirectivee UseParseDirectiveUseSuggestDirective foram removidos. Um novo tipo de diretiva foi introduzido e RootCommand agora expõe uma Directives propriedade. Você pode adicionar, remover e iterar diretivas usando essa coleção. A diretiva Suggest é incluída por padrão; você também pode usar outras diretivas, como DiagramDirective ou EnvironmentVariablesDirective.

  • EnableLegacyDoubleDashBehavior foi removido. Todos os tokens incompatíveis agora são expostos pela propriedade ParseResult.UnmatchedTokens. Para obter mais informações, consulte tokens não correspondentes.

  • EnablePosixBundling foi removido. O agrupamento agora está habilitado por padrão, você pode desabilitá-lo definindo a ParserConfiguration.EnablePosixBundling propriedade como false. Para obter mais informações, consulte EnablePosixBundling.

  • RegisterWithDotnetSuggest foi removido à medida que executava uma operação cara, normalmente durante a inicialização do aplicativo. Agora você deve registrar comandos dotnet suggestmanualmente.

  • UseExceptionHandler foi removido. O manipulador de exceção padrão agora está habilitado por padrão; você pode desabilitá-la definindo a InvocationConfiguration.EnableDefaultExceptionHandler propriedade como false. Isso é útil quando você deseja tratar exceções de maneira personalizada, apenas encapsulando os métodos Invoke ou InvokeAsync em um bloco try-catch. Para obter mais informações, consulte EnableDefaultExceptionHandler.

  • UseHelp e UseVersion foram removidos. A ajuda e a versão agora são expostas pelos tipos públicos HelpOption e VersionOption. Ambos são incluídos por padrão nas opções definidas por RootCommand. Para mais informações, consulte Personalizar a saída de ajuda e Opção de Versão.

  • UseHelpBuilder foi removido. Para obter mais informações sobre como personalizar a saída da ajuda, consulte Como personalizar a ajuda em System.CommandLine.

  • AddMiddleware foi removido. Ele desacelerou a inicialização do aplicativo e os recursos podem ser expressos sem ele.

  • UseParseErrorReporting e UseTypoCorrections foram removidos. Os erros de análise agora são relatados por padrão ao invocar ParseResult. Você pode configurá-lo usando a ação ParseErrorAction que a propriedade ParseResult.Action expõe.

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResources e LocalizationResources foram removidos. Esse recurso foi usado principalmente pela dotnet CLI para adicionar traduções ausentes a System.CommandLine. Todas essas traduções foram movidas para o próprio System.CommandLine, portanto, essa funcionalidade não é mais necessária. Se o suporte ao seu idioma estiver ausente, relate um problema.

  • UseTokenReplacer foi removido. Os arquivos de resposta são habilitados por padrão, mas você pode desabilitá-los definindo a ResponseFileTokenReplacer propriedade como null. Você também pode fornecer uma implementação personalizada para personalizar como os arquivos de resposta são processados.

Por fim, mas não menos importante, a IConsole e todas as interfaces relacionadas (IStandardOut, IStandardError, IStandardIn) foram removidas. InvocationConfiguration expõe duas TextWriter propriedades: Output e Error. Você pode definir essas propriedades para qualquer TextWriter instância, como uma StringWriter, que você pode usar para capturar a saída para teste. A motivação para essa alteração foi expor menos tipos e reutilizar abstrações existentes.

Invocação

Na versão 2.0.0-beta4, a interface ICommandHandler expôs os métodos Invoke e InvokeAsync que eram usados para invocar o comando analisado. Isso facilitou a combinação de código síncrono e assíncrono, por exemplo, definindo um manipulador síncrono para um comando e invocando-o de forma assíncrona (o que poderia levar a um deadlock). Além disso, era possível definir um manipulador apenas para um comando, mas não para uma opção (como ajuda, que exibe ajuda) ou uma diretiva.

Uma nova classe CommandLineAction base abstrata e duas classes derivadas, SynchronousCommandLineAction e AsynchronousCommandLineAction. O primeiro é usado para ações síncronas que retornam um int código de saída, enquanto o último é usado para ações assíncronas que retornam um Task<int> código de saída.

Você não precisa criar um tipo derivado para definir uma ação. Você pode usar o Command.SetAction método para definir uma ação para um comando. A ação síncrona pode ser um delegado que usa um System.CommandLine.ParseResult parâmetro e retorna um int código de saída (ou nada, e um código de saída padrão 0 é retornado). A ação assíncrona pode ser um delegado que aceita os parâmetros System.CommandLine.ParseResult e CancellationToken e retorna um Task<int> (ou Task para obter o código de saída padrão retornado).

rootCommand.SetAction(ParseResult parseResult =>
{
    FileInfo parsedFile = parseResult.GetValue(fileOption);
    ReadFile(parsedFile);
});

No passado, o CancellationToken passado para InvokeAsync foi exposto ao manipulador por meio de um método de InvocationContext.

rootCommand.SetHandler(async (InvocationContext context) =>
{
    string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption);
    var token = context.GetCancellationToken();
    returnCode = await DoRootCommand(urlOptionValue, token);
});

A maioria dos usuários não estava obtendo esse token e passando-o ainda mais. CancellationToken agora é um argumento obrigatório para ações assíncronas, de modo que o compilador produz um aviso quando não é passado mais adiante (consulte CA2016).

rootCommand.SetAction((ParseResult parseResult, CancellationToken token) =>
{
    string? urlOptionValue = parseResult.GetValue(urlOption);
    return DoRootCommandAsync(urlOptionValue, token);
});

Como resultado dessas e de outras alterações mencionadas acima, a InvocationContext classe também foi removida. O ParseResult agora é passado diretamente para a ação, para que você possa acessar os valores e opções analisados diretamente dela.

Para resumir essas alterações:

  • A ICommandHandler interface foi removida. SynchronousCommandLineAction e AsynchronousCommandLineAction foram introduzidos.
  • O Command.SetHandler método foi renomeado para SetAction.
  • A Command.Handler propriedade foi renomeada para Command.Action. Option foi estendido com Option.Action.
  • InvocationContext foi removido. O ParseResult agora é passado diretamente para a ação.

Para obter mais detalhes sobre como usar ações, consulte Como analisar e invocar comandos em System.CommandLine.

Os benefícios da API simplificada

As alterações feitas no 2.0.0-beta5 tornam a API mais consistente, à prova de futuro e mais fácil de usar para usuários existentes e novos.

Os novos usuários precisam aprender menos conceitos e tipos, pois o número de interfaces públicas diminuiu de 11 para 0 e as classes públicas (e structs) diminuíram de 56 para 38. A contagem de métodos públicos caiu de 378 para 235, e a contagem de propriedades públicas de 118 para 99.

O número de assemblies referenciados por System.CommandLine é reduzido de 11 para 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

O tamanho da biblioteca é reduzido (em 32%) e, portanto, o tamanho dos aplicativos NativeAOT que usam a biblioteca.

A simplicidade também melhorou o desempenho da biblioteca (é um efeito colateral do trabalho, não o principal objetivo dela). Os parâmetros de comparação mostram que a análise e a invocação de comandos agora são mais rápidas do que na versão 2.0.0-beta4, especialmente para comandos grandes com muitas opções e argumentos. As melhorias de desempenho são visíveis em cenários síncronos e assíncronos.

O aplicativo mais simples, apresentado anteriormente, produz os seguintes resultados:

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 |

Como você pode ver, o tempo de inicialização (os parâmetros de comparação relatam o tempo necessário para executar determinado executável) melhorou em 12% em comparação com 2.0.0-beta4. Se você compilar o aplicativo com NativeAOT, ele será apenas 3 ms mais lento que um aplicativo NativeAOT que não analisa os argumentos (EmptyAOT na tabela acima). Além disso, se você excluir a sobrecarga de um aplicativo vazio (63,58 ms), a análise será 40% mais rápida do que em 2.0.0-beta4 (22.22 ms vs 13.66 ms).

Consulte também