Compartilhar via


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

Importante

System.CommandLine está atualmente em VERSÃO PRÉVIA e essa documentação é para a versão 2.0 beta 5. Algumas informações referem-se ao produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

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 significativas feitas na versão 2.0.0-beta5 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. Alguns não eram consistentes com as convenções de nomenclatura, como o uso do 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 System.CommandLine.Parsing.CommandLineParser
System.CommandLine.Parsing.OptionResult.IsImplicit System.CommandLine.Parsing.OptionResult.Implicit
System.CommandLine.Option.IsRequired System.CommandLine.Option.Required
System.CommandLine.Symbol.IsHidden System.CommandLine.Symbol.Hidden
System.CommandLine.Option.ArgumentHelpName System.CommandLine.Option.HelpName
System.CommandLine.Parsing.OptionResult.Token System.CommandLine.Parsing.OptionResult.IdentifierToken
System.CommandLine.Parsing.ParseResult.FindResultFor System.CommandLine.Parsing.ParseResult.GetResult
System.CommandLine.Parsing.SymbolResult.ErrorMessage System.CommandLine.Parsing.SymbolResult.AddError

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

Expondo coleções mutáveis

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 expostas por meio de propriedades como coleções somente leitura. Por causa disso, era impossível remover itens dessas coleções.

Na versão 2.0.0-beta5, alteramos as APIs 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
System.CommandLine.Command.AddArgument System.CommandLine.Command.Arguments.Add
System.CommandLine.Command.AddOption System.CommandLine.Command.Options.Add
System.CommandLine.Command.AddCommand System.CommandLine.Command.Subcommands.Add
System.CommandLine.Command.AddValidator System.CommandLine.Command.Validators.Add
System.CommandLine.Option.AddValidator System.CommandLine.Option.Validators.Add
System.CommandLine.Argument.AddValidator System.CommandLine.Argument.Validators.Add
System.CommandLine.Command.AddCompletions System.CommandLine.Command.CompletionSources.Add
System.CommandLine.Option.AddCompletions System.CommandLine.Option.CompletionSources.Add
System.CommandLine.Argument.AddCompletions System.CommandLine.Argument.CompletionSources.Add
System.CommandLine.Command.AddAlias System.CommandLine.Command.Aliases.Add
System.CommandLine.Option.AddAlias System.CommandLine.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 Option<T> construtor, o símbolo relatou seu nome como o alias mais longo com prefixos como --, -ou / removido. Isso foi confuso.

Além disso, para obter o valor analisado, os usuários tiveram que 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>). Também separamos o conceito de um nome e aliases; agora os 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 Option.Name não remove mais o ---, ou qualquer / outro prefixo do alias mais longo.
  • A Aliases propriedade exposta e OptionCommand 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 obter 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> expuncionou muitos construtores, alguns dos quais aceitaram o nome. Como o nome agora é obrigatório e esperamos que os aliases sejam fornecidos Option<T>com frequência, há apenas um único construtor. Ele aceita o nome e uma params matriz de aliases.

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 causará um erro de compilador.

O código antigo que usou o construtor com uma descrição deve ser atualizado para usar o novo construtor que usa um nome e aliases e, em seguida, definir 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, os usuários podem 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 de tipo seguro e poderia levar a erros em tempo de execução se o valor não fosse compatível com a opção ou o tipo de argumento:

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

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

Option<T> e Argument<T> as classes agora têm uma DefaultValueFactory propriedade que pode ser usada 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 da linha de comando analisada.

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

Argument<T> e Option<T> também vêm com uma CustomParser propriedade que pode ser usada 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.

A separação de análise 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 estava 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 apenas analisado e apenas de maneira síncrona.
  • ParseResult agora expõe tanto quanto InvokeInvokeAsync métodos que podem ser usados para invocar o comando. Isso deixa claro que o comando é invocado após a análise e permite a 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 Parser classe 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 tornou-se mutável e CommandLineBuilder foi removido. A criação de uma configuração agora é tão simples quanto criar uma instância CommandLineConfiguration e definir as propriedades que você deseja personalizar. Além disso, a criação de uma nova instância de configuração é o equivalente ao método de chamadaCommandLineBuilder.UseDefaults
  • Cada Parse método agora aceita um parâmetro opcional CommandLineConfiguration que pode ser usado para personalizar a análise. Quando não for fornecido, a configuração padrão será usada.
  • Parser foi renomeado para CommandLineParser desambiguar de outros tipos de analisador para evitar conflitos de nome. 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 (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 CommandLineConfiguration chamado ProcessTerminationTimeout. Ele é habilitado por padrão, com um tempo limite de 2s. Defina-o para null desabilitá-lo.

  • EnableDirectives, UseEnvironmentVariableDirectivee UseParseDirectiveUseSuggestDirective foram removidos. Um novo tipo de diretiva foi introduzido e o RootCommand agora expõe a System.CommandLine.RootCommand.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 não compatíveis agora são expostos pela propriedade ParseResult.UnmatchedTokens .

  • EnablePosixBundling foi removido. O agrupamento agora está habilitado por padrão, você pode desabilitá-lo definindo a propriedade CommandLineConfiguration.EnableBundling como false.

  • 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á-lo definindo a propriedade CommandLineConfiguration.EnableDefaultExceptionHandler como false. Isso é útil quando você deseja lidar com exceções de maneira personalizada, apenas encapsulando os Invoke métodos ou InvokeAsync em um bloco try-catch.

  • 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.

  • 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 ParseErrorAction propriedade exposta.ParseResult.Action

    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 System.CommandLine si, portanto, esse recurso não é mais necessário. Se não houver suporte para seu idioma, relate um problema.

  • UseTokenReplacer foi removido. Os arquivos de resposta são habilitados por padrão, mas você pode desabilitá-los definindo a System.CommandLine.CommandLineConfiguration.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, as IConsole interfaces e todas as interfaces relacionadas (IStandardOut, IStandardError, IStandardIn) foram removidas. System.CommandLine.CommandLineConfiguration expõe duas TextWriter propriedades: Output e Error. Elas podem ser definidas para qualquer TextWriter instância, como uma StringWriter, que pode ser usada para capturar a saída para teste. Nossa motivação foi expor menos tipos e reutilizar abstrações existentes.

Invocação

Na versão 2.0.0-beta4, a ICommandHandler interface foi Invoke exposta e InvokeAsync os métodos que foram 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 System.CommandLine.CommandLineActionbase abstrata e duas classes derivadas: System.CommandLine.SynchronousCommandLineAction e System.CommandLine.AsynchronousCommandLineAction foram introduzidas. 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 System.CommandLine.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 usa um System.CommandLine.ParseResult parâmetro e CancellationToken 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 foi InvokeAsync 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 nossos usuários não estava obtendo esse token e passando-o ainda mais. Fizemos CancellationToken um argumento obrigatório para ações assíncronas, para que o compilador produza um aviso quando ele não é passado mais adiante (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 anteriormente, 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

Esperamos que as alterações feitas na versão 2.0.0-beta5 tornem 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 as 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

Isso nos permitiu reduzir o tamanho da biblioteca em 32% e o tamanho do seguinte aplicativo NativeAOT em 20%:

Option<bool> boolOption = new Option<bool>(new[] { "--bool", "-b" }, "Bool option");
Option<string> stringOption = new Option<string>(new[] { "--string", "-s" }, "String option");

RootCommand command = new RootCommand
{
    boolOption,
    stringOption
};

command.SetHandler<bool, string>(Run, boolOption, stringOption);

return new CommandLineBuilder(command).UseDefaults().Build().Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}
Option<bool> boolOption = new Option<bool>("--bool", "-b") { Description = "Bool option" };
Option<string> stringOption = new Option<string>("--string", "-s") { Description = "String option" };

RootCommand command = new ()
{
    boolOption,
    stringOption,
};

command.SetAction(parseResult => Run(parseResult.GetValue(boolOption), parseResult.GetValue(stringOption)));

return new CommandLineConfiguration(command).Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}

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.

Para o aplicativo mais simples apresentado anteriormente, obtivemos 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 compilarmos o aplicativo com NativeAOT, ele será apenas 3 ms mais lento do que um aplicativo NativeAOT que não analisa os argumentos (EmptyAOT na tabela acima). Além disso, quando excluimos a sobrecarga de um aplicativo vazio (63,58 ms), a análise é 40% mais rápida do que em 2.0.0-beta4 (22.22 ms vs 13.66 ms).

Consulte também