Como vincular argumentos a manipuladores em System.CommandLine
Importante
System.CommandLine
está atualmente em PREVIEW, e esta documentação é para a versão 2.0 beta 4.
Algumas informações estão relacionadas ao produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado. A Microsoft não faz garantias, de forma expressa ou implícita, em relação à informação aqui apresentada.
O processo de analisar argumentos e fornecê-los ao código do manipulador de comandos é chamado de vinculação de parâmetros. System.CommandLine
tem a capacidade de vincular muitos tipos de argumento incorporados. Por exemplo, inteiros, enums e objetos do sistema de arquivos, como FileInfo e DirectoryInfo podem ser vinculados. Vários System.CommandLine
tipos também podem ser vinculados.
Validação de argumento integrada
Os argumentos têm tipos e aridade esperados. System.CommandLine
rejeita argumentos que não correspondem a essas expectativas.
Por exemplo, um erro de análise será exibido se o argumento para uma opção inteira não for um inteiro.
myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.
Um erro de aridade é exibido se vários argumentos são passados para uma opção que tem 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 na linha é aceito. No exemplo a seguir, o valor three
seria passado para o aplicativo.
myapp --item one --item two --item three
Vinculação de parâmetros até 8 opções e argumentos
O exemplo a seguir mostra como vincular 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 os argumentos devem ser declarados na mesma ordem no lambda e nos parâmetros que seguem o lambda. Se a ordem não for consistente, um dos seguintes cenários resultará:
- Se as opções ou argumentos fora de ordem forem de tipos diferentes, uma exceção em tempo de execução será lançada. Por exemplo, um
int
pode aparecer onde umstring
deve 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,
string
a opçãox
pode aparecer ondestring
a opçãoy
deve 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 que SetHandler suportam até 8 parâmetros, com assinaturas síncronas e assíncronas.
Vinculação de parâmetros mais de 8 opções e argumentos
Para lidar com mais de 8 opções, ou para construir um tipo personalizado a partir de várias opções, você pode usar InvocationContext
ou um fichário personalizado.
Utilizar o comando InvocationContext
Uma SetHandler sobrecarga fornece acesso ao InvocationContext objeto 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 Manipular terminação.
Usar um fichário personalizado
Um fichário personalizado permite combinar vários valores de opção ou argumento em um tipo complexo e passar isso para um único parâmetro de manipulador. Suponha que você tenha um Person
tipo:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
Crie uma classe derivada de , onde T
é o tipo a ser construído com base na entrada da linha de BinderBase<T>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 passar seu tipo personalizado para o 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 foram 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á Task-retornando Func sobrecargas de SetHandler. Se o manipulador for chamado a partir de um código assíncrono, você poderá retornar um Task<int>
de um manipulador que usa um desses manipuladores e usar o int
valor 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 lambda em si precisar ser assíncrono, você não poderá retornar um Task<int>
arquivo . Nesse caso, use InvocationContext.ExitCode. Você pode obter a instância injetada InvocationContext
em seu lambda usando uma sobrecarga SetHandler que especifica o InvocationContext
parâmetro as only. Essa SetHandler
sobrecarga não permite especificar IValueDescriptor<T>
objetos, 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 Action sobrecargas. Nesse caso, defina o código de saída usando InvocationContext.ExitCode
da mesma forma que faria com um lambda assíncrono.
O padrão do código de saída é 1. Se você não defini-lo explicitamente, seu valor será definido como 0 quando o manipulador sair normalmente. Se uma exceção for lançada, ela manterá o valor padrão.
Tipos suportados
Os exemplos a seguir mostram o código que vincula alguns tipos comumente usados.
Enumerações
Os valores dos tipos são vinculados por nome, e a associação não diferencia maiúsculas de enum
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á a entrada de linha de comando de exemplo 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á a entrada de linha de comando de exemplo 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 seguinte entrada resulta na mesma saída:
--items one two three
Tipos de sistema de arquivos
Os aplicativos de linha de comando que trabalham com o sistema de arquivos podem usar os FileSystemInfotipos , FileInfoe 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ão 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 têm um construtor que usa um único parâmetro string podem ser vinculados dessa maneira. Por exemplo, o código que funcionaria com FileInfo
funciona com um Uri em vez disso.
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
Usar System.CommandLine objetos
Há uma SetHandler
sobrecarga que lhe dá acesso ao InvocationContext objeto. Esse objeto pode então ser usado para acessar outros System.CommandLine
objetos. Por exemplo, você tem acesso aos seguintes objetos:
InvocationContext
Para obter exemplos, consulte Definir códigos de saída e Manipular terminação.
CancellationToken
Para obter informações sobre como usar CancellationTokeno , consulte Como lidar com a rescisão.
IConsole
IConsole torna o teste, bem como muitos cenários de extensibilidade mais fáceis do que o uso System.Console
do . Está disponível na InvocationContext.Console propriedade.
ParseResult
O ParseResult objeto está disponível na InvocationContext.ParseResult propriedade. É 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 ParseResult.UnmatchedTokens propriedade. Esta 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 incomparáveis é ú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 sudo
comando no Linux é um exemplo. Ele usa 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 wrapper como este, defina a propriedade TreatUnmatchedTokensAsErrors command como false
. Em seguida, a ParseResult.UnmatchedTokens
propriedade conterá todos os argumentos que não pertencem explicitamente ao comando. No exemplo anterior, ParseResult.UnmatchedTokens
conteria os apt
tokens e update
. Seu manipulador de comando poderia então encaminhar o UnmatchedTokens
para uma nova chamada de shell, por exemplo.
Validação e vinculação personalizadas
Para fornecer código de validação personalizado, chame AddValidator 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 ParseArgument<T> delegado, 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 é definido isDefault
para true
que o parseArgument
delegado seja chamado mesmo que o usuário não tenha inserido um valor para essa opção.
Aqui estão alguns exemplos do que você pode fazer com ParseArgument<T>
o que você não pode fazer com AddValidator
:
Análise de tipos personalizados, como a
Person
classe 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 cadeias de caracteres 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 cadeia de caracteres e você tem que manipular uma sequência de cadeias de caracteres na entrada de linha de comando. O ArgumentResult.OnlyTake método permite que você divida dinamicamente as cadeias de caracteres de entrada entre os argumentos.