Compartir a través de


Cómo enlazar argumentos a controladores en System.CommandLine

Importante

System.CommandLine se encuentra actualmente en versión preliminar y esta documentación es para la versión 2.0 beta 4. Parte de la información hace referencia a la versión preliminar del producto, que puede haberse modificado sustancialmente antes de lanzar la versión definitiva. Microsoft no otorga ninguna garantía, explícita o implícita, con respecto a la información proporcionada aquí.

El proceso de análisis de argumentos y proporcionarlos al código del controlador de comandos se denomina enlace de parámetros. System.CommandLine tiene la capacidad de enlazar muchos tipos de argumento integrados. Por ejemplo, enteros, enumeraciones y objetos del sistema de archivos como FileInfo y DirectoryInfo se pueden enlazar. También se pueden enlazar varios System.CommandLine tipos.

Validación de argumentos integrada

Los argumentos tienen tipos y aridad esperados. System.CommandLine rechaza los argumentos que no coinciden con estas expectativas.

Por ejemplo, se muestra un error de análisis si el argumento de una opción de entero no es un entero.

myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.

Se muestra un error de aridad si se pasan varios argumentos a una opción que tiene la aridad máxima de uno:

myapp --delay-option 1 --delay-option 2
Option '--delay' expects a single argument but 2 were provided.

Este comportamiento puede invalidarse configurando Option.AllowMultipleArgumentsPerToken a true. En ese caso, puedes repetir una opción que tenga la aridad máxima de uno, pero solo se acepta el último valor de la línea. En el ejemplo siguiente, el valor three se pasa a la aplicación.

myapp --item one --item two --item three

Enlace de parámetros hasta 8 opciones y argumentos

En el ejemplo siguiente se muestra cómo enlazar opciones a los parámetros del controlador de comandos mediante una llamada a 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}");
}

Los parámetros lambda son variables que representan los valores de opciones y argumentos:

(delayOptionValue, messageOptionValue) =>
{
    DisplayIntAndString(delayOptionValue, messageOptionValue);
},

Las variables que siguen a la expresión lambda representan los objetos opción y argumento que son los orígenes de los valores de la opción y del argumento:

delayOption, messageOption);

Las opciones y los argumentos deben declararse en el mismo orden en la expresión lambda y en los parámetros que siguen a la expresión lambda. Si el orden no es coherente, se producirá uno de los siguientes escenarios:

  • Si las opciones o argumentos desordenados son de tipos diferentes, se produce una excepción en tiempo de ejecución. Por ejemplo, podría aparecer un int donde debería haber un string en la lista de orígenes.
  • Si las opciones o argumentos desordenados son del mismo tipo, el controlador obtiene silenciosamente los valores incorrectos en los parámetros proporcionados. Por ejemplo, string la opción x podría aparecer donde string la opción y debe estar en la lista de orígenes. En ese caso, la variable del valor de opción y obtiene el valor de la opción x.

Hay sobrecargas de SetHandler que admiten hasta 8 parámetros, con firmas sincrónicas y asincrónicas.

Enlazar parámetros hasta más de 8 opciones y argumentos

Para controlar más de 8 opciones, o para construir un tipo personalizado a partir de varias opciones, puedes usar InvocationContext o un enlazador personalizado.

Use InvocationContext

Una SetHandler sobrecarga proporciona acceso al InvocationContext objeto y puede usar InvocationContext para obtener cualquier número de valores de opción y argumento. Para obtener ejemplos, consulta Establecer códigos de salida y Controlar la terminación.

Uso de un enlazador personalizado

Un enlazador personalizado te permite combinar varios valores de opción o argumento en un tipo complejo y pasarlos a un único parámetro de controlador. Supongamos que tienes un tipo Person:

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

Crea una clase derivada de BinderBase<T>, donde T es el tipo que se va a construir en función de la entrada de la línea de comandos:

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)
        };
}

Con el enlazador personalizado, puedes pasar el tipo personalizado al controlador de la misma manera que obtienes valores para las opciones y los argumentos:

rootCommand.SetHandler((fileOptionValue, person) =>
    {
        DoRootCommand(fileOptionValue, person);
    },
    fileOption, new PersonBinder(firstNameOption, lastNameOption));

Este es el programa completo del que se toman los ejemplos anteriores:

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)
            };
    }
}

Configurar los códigos de salida

Hay Tasksobrecargas de Func de devolución de SetHandler. Si se llama al controlador desde el código asincrónico, puedes devolver un Task<int> desde un controlador que use uno de estos y usar el int valor para establecer el código de salida del proceso, como en el ejemplo siguiente:

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);
}

Sin embargo, si la propia expresión lambda debe ser asincrónica, no se puede devolver Task<int>. En ese caso, usa InvocationContext.ExitCode. Puedes insertar la instancia en la InvocationContext expresión lambda mediante una sobrecarga SetHandler que especifica como InvocationContext único parámetro. Esta SetHandler sobrecarga no te permite especificar IValueDescriptor<T> objetos, pero puedes obtener valores de opción y argumento de la propiedad ParseResult de InvocationContext, como se muestra en el ejemplo siguiente:

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);
}

Si no tienes trabajo asincrónico que hacer, puedes usar las Action sobrecargas. En ese caso, establece el código de salida con InvocationContext.ExitCode la misma manera que lo haría con una lambda asincrónica.

El código de salida tiene como valor predeterminado 1. Si no lo estableces explícitamente, su valor se establece en 0 cuando el controlador sale normalmente. Si se produce una excepción, mantiene el valor predeterminado.

Tipos admitidos

En los ejemplos siguientes se muestra código que enlaza algunos tipos usados habitualmente.

Enumeraciones

Los valores de enum los tipos están enlazados por nombre y el enlace no distingue mayú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);

Esta es una entrada de línea de comandos de ejemplo y la salida resultante del ejemplo anterior:

myapp --color red
myapp --color RED
Red
Red

Matrices y listas

Se admiten muchos tipos comunes que implementan IEnumerable. Por ejemplo:

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);

Esta es una entrada de línea de comandos de ejemplo y la salida resultante del ejemplo anterior:

--items one --items two --items three
System.Collections.Generic.List`1[System.String]
one
two
three

Dado que AllowMultipleArgumentsPerToken se establece en true, la entrada siguiente da como resultado la misma salida:

--items one two three

Tipo de sistema de archivos

Las aplicaciones de línea de comandos que funcionan con el sistema de archivos pueden usar los FileSystemInfotipos, FileInfoy DirectoryInfo. En el ejemplo siguiente se muestra el 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);

Con FileInfo y DirectoryInfo el código de coincidencia de patrones no es necesario:

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);

Otros los tipos admitidos

Muchos tipos que tienen un constructor que toma un único parámetro de cadena se pueden enlazar de esta manera. Por ejemplo, el código con FileInfo el que trabajaría funciona con un en Uri su lugar.

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);

Además de los tipos de sistema de archivos y Uri, se admiten los siguientes tipos:

  • bool
  • byte
  • DateTime
  • DateTimeOffset
  • decimal
  • double
  • float
  • Guid
  • int
  • long
  • sbyte
  • short
  • uint
  • ulong
  • ushort

Uso de System.CommandLine objetos

Hay una SetHandler sobrecarga que proporciona acceso al objeto InvocationContext. Después, ese objeto se puede usar para tener acceso a otros System.CommandLine objetos. Por ejemplo, tienes acceso a los objetos siguientes:

InvocationContext

Para obtener ejemplos, consulta Establecer códigos de salida y Controlar la terminación.

CancellationToken

Para obtener información sobre cómo usar CancellationToken, consulta Cómo controlar la terminación.

IConsole

IConsole facilita las pruebas, así como muchos escenarios de extensibilidad que el uso de System.Console. Está disponible en la propiedad InvocationContext.Console.

ParseResult

El ParseResult objeto está disponible en la propiedad InvocationContext.ParseResult. Es una estructura singleton que representa los resultados del análisis de la entrada de la línea de comandos. Puedes usarlo para comprobar la presencia de opciones o argumentos en la línea de comandos o para obtener la ParseResult.UnmatchedTokens propiedad. Esta propiedad contiene una lista de los tokens que se analizaron, pero que no coincidieron con ningún comando, opción o argumento configurados.

La lista de tokens no coincidentes es útil en los comandos que se comportan como contenedores. Un comando contenedor toma un conjunto de tokens y los reenvía a otro comando o aplicación. El sudo comando en Linux es un ejemplo. Toma el nombre de un usuario para suplantar seguido de un comando que se va a ejecutar. Por ejemplo:

sudo -u admin apt update

Esta línea de comandos ejecutaría el apt update comando como el usuario admin.

Para implementar un comando contenedor como este, establece la propiedad de comando TreatUnmatchedTokensAsErrors en false. A continuación, la ParseResult.UnmatchedTokens propiedad contendrá todos los argumentos que no pertenecen explícitamente al comando. En el ejemplo anterior, ParseResult.UnmatchedTokens contendrá los tokens apt y update. Después, el controlador de comandos podría reenviar a UnmatchedTokens una nueva invocación de shell, por ejemplo.

Validación y enlaces personalizados

Para proporcionar código de validación personalizado, llama a AddValidator en el comando, la opción o el argumento, como se muestra en el ejemplo siguiente:

var delayOption = new Option<int>("--delay");
delayOption.AddValidator(result =>
{
    if (result.GetValueForOption(delayOption) < 1)
    {
        result.ErrorMessage = "Must be greater than 0";
    }
});

Si deseas analizar y validar la entrada, usa un ParseArgument<T> delegado, como se muestra en el ejemplo siguiente:

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.
          }
      });

El código anterior establece isDefaulttrue en para que se llame al parseArgument delegado incluso si el usuario no ha especificado un valor para esta opción.

Estos son algunos ejemplos de lo que puedes hacer con ParseArgument<T> que no puedes hacer con AddValidator:

  • Análisis de tipos personalizados, como la Person clase en el ejemplo siguiente:

    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álisis de otros tipos de cadenas de entrada (por ejemplo, analizar «1,2,3» en int[]).

  • Aridad dinámica. Por ejemplo, tienes dos argumentos que se definen como matrices de cadenas y tienes que controlar una secuencia de cadenas en la entrada de la línea de comandos. El método ArgumentResult.OnlyTake permite dividir dinámicamente las cadenas de entrada entre los argumentos.

Vea también

Información general de System.CommandLine