Como vincular argumentos a manipuladores em System.CommandLine
Importante
Atualmente, System.CommandLine
está em VERSÃO PRÉVIA, e essa documentação é para a versão 2.0 beta 4.
Algumas informações estão relacionadas a produtos de pré-lançamento que poderão ser substancialmente modificados antes do lançamento. A Microsoft não oferece garantias, expressas ou implícitas, das informações aqui fornecidas.
O processo de analisar argumentos e fornecê-los ao código do manipulador de comandos é chamado de associação de parâmetros. System.CommandLine
tem a capacidade de associar muitos tipos de argumentos incorporados. Por exemplo, inteiros, enumerações e objetos do sistema de arquivos, como FileInfo e DirectoryInfo, podem ser associados. Vários tipos System.CommandLine
também podem ser vinculados.
Validação de argumento interno
Os argumentos têm tipos esperados e aridade. System.CommandLine
rejeita argumentos que não correspondem a essas expectativas.
Por exemplo, um erro de análise é exibido se o argumento para uma opção de número inteiro não for um número inteiro.
myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.
Um erro de aridade é exibido se vários argumentos forem passados para uma opção que tenha a aridade máxima de um:
myapp --delay-option 1 --delay-option 2
Option '--delay' expects a single argument but 2 were provided.
Esse comportamento pode ser substituído definindo Option.AllowMultipleArgumentsPerToken como true
. Nesse caso, você pode repetir uma opção que tenha aridade máxima de um, mas apenas o último valor da linha é aceito. No exemplo a seguir, o valor three
seria passado para o aplicativo.
myapp --item one --item two --item three
Associação de parâmetros para até oito opções e argumentos
O exemplo a seguir mostra como associar opções aos parâmetros do manipulador de comandos, chamando SetHandler:
var delayOption = new Option<int>
("--delay", "An option whose argument is parsed as an int.");
var messageOption = new Option<string>
("--message", "An option whose argument is parsed as a string.");
var rootCommand = new RootCommand("Parameter binding example");
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);
rootCommand.SetHandler(
(delayOptionValue, messageOptionValue) =>
{
DisplayIntAndString(delayOptionValue, messageOptionValue);
},
delayOption, messageOption);
await rootCommand.InvokeAsync(args);
public static void DisplayIntAndString(int delayOptionValue, string messageOptionValue)
{
Console.WriteLine($"--delay = {delayOptionValue}");
Console.WriteLine($"--message = {messageOptionValue}");
}
Os parâmetros lambda são variáveis que representam os valores de opções e argumentos:
(delayOptionValue, messageOptionValue) =>
{
DisplayIntAndString(delayOptionValue, messageOptionValue);
},
As variáveis que seguem o lambda representam os objetos de opção e argumento que são as fontes dos valores de opção e argumento:
delayOption, messageOption);
As opções e argumentos devem ser declarados na mesma ordem no lambda e nos parâmetros que seguem o lambda. Se o pedido não for consistente, ocorrerá um dos seguintes cenários:
- Se as opções ou argumentos fora de ordem forem de tipos diferentes, uma exceção de tempo de execução será lançada. Por exemplo, um
int
pode aparecer onde umstring
deveria estar na lista de fontes. - Se as opções ou argumentos fora de ordem forem do mesmo tipo, o manipulador obterá silenciosamente os valores errados nos parâmetros fornecidos a ele. Por exemplo, a
string
opçãox
pode aparecer onde astring
opçãoy
deveria estar na lista de fontes. Nesse caso, a variável para o valor da opçãoy
obtém o valor da opçãox
.
Há sobrecargas de SetHandler que dão suporte a até 8 parâmetros, com assinaturas síncronas e assíncronas.
Parâmetro vinculando mais de 8 opções e argumentos
Para lidar com mais de 8 opções ou para construir um tipo personalizado de várias opções, você pode usar InvocationContext
ou um fichário personalizado.
Use InvocationContext
.
Uma sobrecarga SetHandler fornece acesso ao objeto InvocationContext e você pode usar InvocationContext
para obter qualquer número de valores de opção e argumento. Para obter exemplos, consulte Definir códigos de saída e Lidar com rescisão.
Use um fichário personalizado
Um fichário personalizado permite combinar vários valores de opção ou argumento em um tipo complexo e passá-lo para um único parâmetro de manipulador. Suponha que você tenha um tipo Person
:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
Crie uma classe derivada de BinderBase<T>, onde T
é o tipo a ser construído com base na entrada da linha de comando:
public class PersonBinder : BinderBase<Person>
{
private readonly Option<string> _firstNameOption;
private readonly Option<string> _lastNameOption;
public PersonBinder(Option<string> firstNameOption, Option<string> lastNameOption)
{
_firstNameOption = firstNameOption;
_lastNameOption = lastNameOption;
}
protected override Person GetBoundValue(BindingContext bindingContext) =>
new Person
{
FirstName = bindingContext.ParseResult.GetValueForOption(_firstNameOption),
LastName = bindingContext.ParseResult.GetValueForOption(_lastNameOption)
};
}
Com o fichário personalizado, você pode obter seu tipo personalizado passado para seu manipulador da mesma forma que obtém valores para opções e argumentos:
rootCommand.SetHandler((fileOptionValue, person) =>
{
DoRootCommand(fileOptionValue, person);
},
fileOption, new PersonBinder(firstNameOption, lastNameOption));
Aqui está o programa completo do qual os exemplos anteriores são retirados:
using System.CommandLine;
using System.CommandLine.Binding;
public class Program
{
internal static async Task Main(string[] args)
{
var fileOption = new Option<FileInfo?>(
name: "--file",
description: "An option whose argument is parsed as a FileInfo",
getDefaultValue: () => new FileInfo("scl.runtimeconfig.json"));
var firstNameOption = new Option<string>(
name: "--first-name",
description: "Person.FirstName");
var lastNameOption = new Option<string>(
name: "--last-name",
description: "Person.LastName");
var rootCommand = new RootCommand();
rootCommand.Add(fileOption);
rootCommand.Add(firstNameOption);
rootCommand.Add(lastNameOption);
rootCommand.SetHandler((fileOptionValue, person) =>
{
DoRootCommand(fileOptionValue, person);
},
fileOption, new PersonBinder(firstNameOption, lastNameOption));
await rootCommand.InvokeAsync(args);
}
public static void DoRootCommand(FileInfo? aFile, Person aPerson)
{
Console.WriteLine($"File = {aFile?.FullName}");
Console.WriteLine($"Person = {aPerson?.FirstName} {aPerson?.LastName}");
}
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
public class PersonBinder : BinderBase<Person>
{
private readonly Option<string> _firstNameOption;
private readonly Option<string> _lastNameOption;
public PersonBinder(Option<string> firstNameOption, Option<string> lastNameOption)
{
_firstNameOption = firstNameOption;
_lastNameOption = lastNameOption;
}
protected override Person GetBoundValue(BindingContext bindingContext) =>
new Person
{
FirstName = bindingContext.ParseResult.GetValueForOption(_firstNameOption),
LastName = bindingContext.ParseResult.GetValueForOption(_lastNameOption)
};
}
}
Definir códigos de saída
Há sobrecargas Func de Taskretorno de SetHandler. Se seu manipulador for chamado a partir de um código assíncrono, você poderá retornar um Task<int>
de um manipulador que use um desses e usar o valor int
para definir o código de saída do processo, como no exemplo a seguir:
static async Task<int> Main(string[] args)
{
var delayOption = new Option<int>("--delay");
var messageOption = new Option<string>("--message");
var rootCommand = new RootCommand("Parameter binding example");
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);
rootCommand.SetHandler((delayOptionValue, messageOptionValue) =>
{
Console.WriteLine($"--delay = {delayOptionValue}");
Console.WriteLine($"--message = {messageOptionValue}");
return Task.FromResult(100);
},
delayOption, messageOption);
return await rootCommand.InvokeAsync(args);
}
No entanto, se o próprio lambda precisar ser assíncrono, você não poderá retornar um Task<int>
. Nesse caso, use InvocationContext.ExitCode. Você pode obter a instância InvocationContext
injetada em seu lambda usando uma sobrecarga SetHandler que especifica o InvocationContext
como o único parâmetro. Essa sobrecarga SetHandler
não permite especificar objetos IValueDescriptor<T>
, mas você pode obter valores de opção e argumento da propriedade ParseResult de InvocationContext
, conforme mostrado no exemplo a seguir:
static async Task<int> Main(string[] args)
{
var delayOption = new Option<int>("--delay");
var messageOption = new Option<string>("--message");
var rootCommand = new RootCommand("Parameter binding example");
rootCommand.Add(delayOption);
rootCommand.Add(messageOption);
rootCommand.SetHandler(async (context) =>
{
int delayOptionValue = context.ParseResult.GetValueForOption(delayOption);
string? messageOptionValue = context.ParseResult.GetValueForOption(messageOption);
Console.WriteLine($"--delay = {delayOptionValue}");
await Task.Delay(delayOptionValue);
Console.WriteLine($"--message = {messageOptionValue}");
context.ExitCode = 100;
});
return await rootCommand.InvokeAsync(args);
}
Se você não tiver trabalho assíncrono para fazer, poderá usar as sobrecargas Action. Nesse caso, defina o código de saída usando InvocationContext.ExitCode
da mesma forma que faria com um lambda assíncrono.
O código de saída é padronizado para 1. Se você não definir explicitamente, seu valor será definido como 0 quando seu manipulador sair normalmente. Se uma exceção for lançada, ela mantém o valor padrão.
Tipos com suporte
Os exemplos a seguir mostram o código que associa alguns tipos comumente usados.
Enumerações
Os valores dos tipos enum
são vinculados por nome e a vinculação não diferencia maiúsculas de minúsculas:
var colorOption = new Option<ConsoleColor>("--color");
var rootCommand = new RootCommand("Enum binding example");
rootCommand.Add(colorOption);
rootCommand.SetHandler((colorOptionValue) =>
{ Console.WriteLine(colorOptionValue); },
colorOption);
await rootCommand.InvokeAsync(args);
Aqui está uma amostra de entrada de linha de comando e a saída resultante do exemplo anterior:
myapp --color red
myapp --color RED
Red
Red
Matrizes e listas
Muitos tipos comuns que implementam IEnumerable são suportados. Por exemplo:
var itemsOption = new Option<IEnumerable<string>>("--items")
{ AllowMultipleArgumentsPerToken = true };
var command = new RootCommand("IEnumerable binding example");
command.Add(itemsOption);
command.SetHandler((items) =>
{
Console.WriteLine(items.GetType());
foreach (string item in items)
{
Console.WriteLine(item);
}
},
itemsOption);
await command.InvokeAsync(args);
Aqui está uma amostra de entrada de linha de comando e a saída resultante do exemplo anterior:
--items one --items two --items three
System.Collections.Generic.List`1[System.String]
one
two
three
Como AllowMultipleArgumentsPerToken está definido como true
, a entrada a seguir resulta na mesma saída:
--items one two three
Tipos de sistema de arquivos
Os aplicativos de linha de comando que funcionam com o sistema de arquivos podem usar os tipos FileSystemInfo, FileInfo e DirectoryInfo. O exemplo a seguir mostra o uso de FileSystemInfo
:
var fileOrDirectoryOption = new Option<FileSystemInfo>("--file-or-directory");
var command = new RootCommand();
command.Add(fileOrDirectoryOption);
command.SetHandler((fileSystemInfo) =>
{
switch (fileSystemInfo)
{
case FileInfo file :
Console.WriteLine($"File name: {file.FullName}");
break;
case DirectoryInfo directory:
Console.WriteLine($"Directory name: {directory.FullName}");
break;
default:
Console.WriteLine("Not a valid file or directory name.");
break;
}
},
fileOrDirectoryOption);
await command.InvokeAsync(args);
Com FileInfo
e DirectoryInfo
o código de correspondência de padrões não é necessário:
var fileOption = new Option<FileInfo>("--file");
var command = new RootCommand();
command.Add(fileOption);
command.SetHandler((file) =>
{
if (file is not null)
{
Console.WriteLine($"File name: {file?.FullName}");
}
else
{
Console.WriteLine("Not a valid file name.");
}
},
fileOption);
await command.InvokeAsync(args);
Outros tipos suportados
Muitos tipos que possuem um construtor que recebe um único parâmetro de string podem ser vinculados dessa maneira. Por exemplo, o código que funcionaria com FileInfo
funciona com um Uri.
var endpointOption = new Option<Uri>("--endpoint");
var command = new RootCommand();
command.Add(endpointOption);
command.SetHandler((uri) =>
{
Console.WriteLine($"URL: {uri?.ToString()}");
},
endpointOption);
await command.InvokeAsync(args);
Além dos tipos de sistema de arquivos e Uri
, os seguintes tipos são suportados:
bool
byte
DateTime
DateTimeOffset
decimal
double
float
Guid
int
long
sbyte
short
uint
ulong
ushort
Use objetos System.CommandLine
Há uma sobrecarga SetHandler
que dá acesso ao objeto InvocationContext. Esse objeto pode então ser usado para acessar outros objetos System.CommandLine
. Por exemplo, você tem acesso aos seguintes objetos:
InvocationContext
Para obter exemplos, consulte Definir códigos de saída e Lidar com rescisão.
CancellationToken
Para obter informações sobre como usar CancellationToken, consulte Como lidar com rescisão.
IConsole
IConsole torna o teste e muitos cenários de extensibilidade mais fáceis do que usar System.Console
. Está disponível na propriedade InvocationContext.Console.
ParseResult
O objeto ParseResult está disponível na propriedade InvocationContext.ParseResult. É uma estrutura singleton que representa os resultados da análise da entrada da linha de comando. Você pode usá-lo para verificar a presença de opções ou argumentos na linha de comando ou para obter a propriedade ParseResult.UnmatchedTokens. Essa propriedade contém uma lista dos tokens que foram analisados, mas não corresponderam a nenhum comando, opção ou argumento configurado.
A lista de tokens sem correspondência é útil em comandos que se comportam como wrappers. Um comando wrapper pega um conjunto de tokens e os encaminha para outro comando ou aplicativo. O comando sudo
no Linux é um exemplo. É preciso o nome de um usuário para representar seguido por um comando para ser executado. Por exemplo:
sudo -u admin apt update
Essa linha de comando executaria o apt update
comando como o usuário admin
.
Para implementar um comando de wrapper como este, defina a propriedade de comando TreatUnmatchedTokensAsErrors como false
. Em seguida, a propriedade ParseResult.UnmatchedTokens
conterá todos os argumentos que não pertencem explicitamente ao comando. No exemplo anterior, ParseResult.UnmatchedTokens
conteria os tokens apt
e update
. Seu manipulador de comandos poderia então encaminhar o UnmatchedTokens
para uma nova invocação de shell, por exemplo.
Validação e associação personalizadas
Para fornecer um código de validação personalizado, chame AddValidator em seu comando, opção ou argumento, conforme mostrado no exemplo a seguir:
var delayOption = new Option<int>("--delay");
delayOption.AddValidator(result =>
{
if (result.GetValueForOption(delayOption) < 1)
{
result.ErrorMessage = "Must be greater than 0";
}
});
Se você quiser analisar e validar a entrada, use um delegado ParseArgument<T>, conforme mostrado no exemplo a seguir:
var delayOption = new Option<int>(
name: "--delay",
description: "An option whose argument is parsed as an int.",
isDefault: true,
parseArgument: result =>
{
if (!result.Tokens.Any())
{
return 42;
}
if (int.TryParse(result.Tokens.Single().Value, out var delay))
{
if (delay < 1)
{
result.ErrorMessage = "Must be greater than 0";
}
return delay;
}
else
{
result.ErrorMessage = "Not an int.";
return 0; // Ignored.
}
});
O código anterior define isDefault
como true
para que o delegado parseArgument
seja chamado mesmo que o usuário não insira um valor para essa opção.
Aqui estão alguns exemplos do que você pode fazer com ParseArgument<T>
que não pode fazer com AddValidator
:
Análise de tipos personalizados, como a classe
Person
no exemplo a seguir:public class Person { public string? FirstName { get; set; } public string? LastName { get; set; } }
var personOption = new Option<Person?>( name: "--person", description: "An option whose argument is parsed as a Person", parseArgument: result => { if (result.Tokens.Count != 2) { result.ErrorMessage = "--person requires two arguments"; return null; } return new Person { FirstName = result.Tokens.First().Value, LastName = result.Tokens.Last().Value }; }) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = true };
Análise de outros tipos de strings de entrada (por exemplo, analisar "1,2,3" em
int[]
).Aridade dinâmica. Por exemplo, você tem dois argumentos que são definidos como matrizes de strings e precisa manipular uma sequência de strings na entrada da linha de comando. O método ArgumentResult.OnlyTake permite dividir dinamicamente as strings de entrada entre os argumentos.