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 um string 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ção x pode aparecer onde string a opção y deve 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 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

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.Consoledo . 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.

Consulte também

System.CommandLine Visão geral