Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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:
-
nameagora é um argumento obrigatório para cada construtor público de Argument<T>, Option<T>e Command. No caso deArgument<T>, ele não é usado para análise, mas para gerar a ajuda. No caso deOption<T>eCommand, 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.DefaultNamefoi removido e Symbol.Name não remove mais o--,-ou/, nem qualquer outro prefixo do alias mais longo. - A propriedade
Aliasesexposta porOptioneCommandagora é uma coleção mutável. Essa coleção não inclui mais o nome do símbolo. -
System.CommandLine.Parsing.IdentifierSymbolfoi removido (era um tipo base para ambosCommandeOption).
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
Parsemétodo que retorna umParseResultobjeto. 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. -
ParseResultagora expõe tanto os métodosInvokequantoInvokeAsyncque 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
CommandExtensionsclasse 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:
-
CommandLineConfigurationfoi 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ânciaInvocationConfiguratione definir as propriedades que você deseja personalizar. - Cada
Parsemé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,
Parserfoi 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 doisParsemétodos de análise: um aceitando umIReadOnlyList<string> argse outro aceitando umstring 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:
CancelOnProcessTerminationagora é uma propriedade do InvocationConfiguration chamado ProcessTerminationTimeout. Ele é habilitado por padrão, com um tempo limite de 2 segundos. Para desabilitá-lo, defina-o comonull. Para obter mais informações, consulte o tempo limite de término do processo.EnableDirectives,UseEnvironmentVariableDirectiveeUseParseDirectiveUseSuggestDirectiveforam 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.EnableLegacyDoubleDashBehaviorfoi removido. Todos os tokens incompatíveis agora são expostos pela propriedade ParseResult.UnmatchedTokens. Para obter mais informações, consulte tokens não correspondentes.EnablePosixBundlingfoi removido. O agrupamento agora está habilitado por padrão, você pode desabilitá-lo definindo a ParserConfiguration.EnablePosixBundling propriedade comofalse. Para obter mais informações, consulte EnablePosixBundling.RegisterWithDotnetSuggestfoi removido à medida que executava uma operação cara, normalmente durante a inicialização do aplicativo. Agora você deve registrar comandosdotnet suggestmanualmente.UseExceptionHandlerfoi removido. O manipulador de exceção padrão agora está habilitado por padrão; você pode desabilitá-la definindo a InvocationConfiguration.EnableDefaultExceptionHandler propriedade comofalse. Isso é útil quando você deseja tratar exceções de maneira personalizada, apenas encapsulando os métodosInvokeouInvokeAsyncem um bloco try-catch. Para obter mais informações, consulte EnableDefaultExceptionHandler.UseHelpeUseVersionforam 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.UseHelpBuilderfoi removido. Para obter mais informações sobre como personalizar a saída da ajuda, consulte Como personalizar a ajuda em System.CommandLine.AddMiddlewarefoi removido. Ele desacelerou a inicialização do aplicativo e os recursos podem ser expressos sem ele.UseParseErrorReportingeUseTypoCorrectionsforam removidos. Os erros de análise agora são relatados por padrão ao invocarParseResult. 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; }UseLocalizationResourceseLocalizationResourcesforam removidos. Esse recurso foi usado principalmente peladotnetCLI para adicionar traduções ausentes aSystem.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.UseTokenReplacerfoi removido. Os arquivos de resposta são habilitados por padrão, mas você pode desabilitá-los definindo a ResponseFileTokenReplacer propriedade comonull. 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
ICommandHandlerinterface foi removida.SynchronousCommandLineActioneAsynchronousCommandLineActionforam introduzidos. - O
Command.SetHandlermétodo foi renomeado para SetAction. - A
Command.Handlerpropriedade foi renomeada para Command.Action.Optionfoi estendido com Option.Action. -
InvocationContextfoi removido. OParseResultagora é 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
- visão geral System.CommandLine