Compartilhar via


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 um string 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ção x pode aparecer onde a string opção y deveria estar na lista de fontes. Nesse caso, a variável para o valor da opção y obtém o valor da opção x.

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.

Confira também

System.CommandLine overview