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 unstring
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ónx
podría aparecer dondestring
la opcióny
debe estar en la lista de orígenes. En ese caso, la variable del valor de opcióny
obtiene el valor de la opciónx
.
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 isDefault
true
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.