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.
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 deArgument<T>
,Option<T>
eCommand
. 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 é maisvirtual
; agora é somente leitura e retorna o nome como foi fornecido quando o símbolo foi criado. Por causa disso,Symbol.DefaultName
foi removido eOption.Name
não remove mais o--
-
, ou qualquer/
outro prefixo do alias mais longo. - A
Aliases
propriedade exposta eOption
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 ambosCommand
eOption
).
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
, Invoke
e 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 umParse
método que retorna umParseResult
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 quantoInvoke
InvokeAsync
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 eCommandLineBuilder
foi removido. A criação de uma configuração agora é tão simples quanto criar uma instânciaCommandLineConfiguration
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 opcionalCommandLineConfiguration
que pode ser usado para personalizar a análise. Quando não for fornecido, a configuração padrão será usada. -
Parser
foi renomeado paraCommandLineParser
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 doisParse
métodos de análise: um aceitando umIReadOnlyList<string> args
e outro aceitando umstring args
. Este último usaCommandLineParser.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 doCommandLineConfiguration
chamado ProcessTerminationTimeout. Ele é habilitado por padrão, com um tempo limite de 2s. Defina-o paranull
desabilitá-lo.EnableDirectives
,UseEnvironmentVariableDirective
eUseParseDirective
UseSuggestDirective
foram removidos. Um novo tipo de diretiva foi introduzido e o RootCommand agora expõe aSystem.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 ouEnvironmentVariablesDirective
.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 comofalse
.RegisterWithDotnetSuggest
foi removido à medida que executava uma operação cara, normalmente durante a inicialização do aplicativo. Agora você deve registrar comandosdotnet suggest
manualmente.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 comofalse
. Isso é útil quando você deseja lidar com exceções de maneira personalizada, apenas encapsulando osInvoke
métodos ouInvokeAsync
em um bloco try-catch.UseHelp
eUseVersion
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
eUseTypoCorrections
foram removidos. Os erros de análise agora são relatados por padrão ao invocarParseResult
. Você pode configurá-lo usando aParseErrorAction
propriedade exposta.ParseResult.Action
ParseResult result = rootCommand.Parse("myArgs", config); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = true; parseError.ShowHelp = false; }
UseLocalizationResources
eLocalizationResources
foram removidos. Esse recurso foi usado principalmente peladotnet
CLI para adicionar traduções ausentes aSystem.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 aSystem.CommandLine.CommandLineConfiguration.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, 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.CommandLineAction
base 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
eAsynchronousCommandLineAction
foram introduzidos. - O
Command.SetHandler
método foi renomeado paraSetAction
. - A
Command.Handler
propriedade foi renomeada paraCommand.Action
.Option
foi estendido comOption.Action
. -
InvocationContext
foi removido. OParseResult
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
- visão geral System.CommandLine