Guide pratique pour lier des arguments à des gestionnaires dans System.CommandLine
Important
System.CommandLine
est actuellement une PRÉVERSION et cette documentation concerne la version 2.0 beta 4.
Certaines informations portent sur la préversion du produit qui est susceptible d’être en grande partie modifiée avant sa publication. Microsoft exclut toute garantie, expresse ou implicite, concernant les informations fournies ici.
Le processus qui consiste à analyser les arguments et à les fournir au code du gestionnaire de commandes s’appelle la liaison de paramètres. System.CommandLine
peut lier de nombreux types d’argument intégrés. Par exemple, les entiers, les enums et les objets de système de fichiers tels que FileInfo et DirectoryInfo peuvent être liés. Plusieurs types System.CommandLine
peuvent également être liés.
Validation des arguments intégrés
Les arguments ont des types et une arité attendus. System.CommandLine
rejette les arguments qui ne correspondent pas à ces attentes.
Par exemple, une erreur d’analyse s’affiche si l’argument d’une option d’entier n’est pas un entier.
myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.
Une erreur d’arité s’affiche si plusieurs arguments sont passés à une option dont l’arité maximale est égale à un :
myapp --delay-option 1 --delay-option 2
Option '--delay' expects a single argument but 2 were provided.
Vous pouvez remplacer ce comportement en affectant la valeur true
à Option.AllowMultipleArgumentsPerToken. Dans ce cas, vous pouvez répéter une option dont l’arité maximale est égale à un, mais seule la dernière valeur de la ligne est acceptée. Dans l’exemple suivant, la valeur three
est passée à l’application.
myapp --item one --item two --item three
Liaison de paramètres jusqu’à 8 options et arguments
L’exemple suivant montre comment lier des options aux paramètres du gestionnaire de commandes, en appelant 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}");
}
Les paramètres lambda sont des variables qui représentent les valeurs des options et des arguments :
(delayOptionValue, messageOptionValue) =>
{
DisplayIntAndString(delayOptionValue, messageOptionValue);
},
Les variables qui suivent le lambda représentent les objets d’option et d’argument qui correspondent aux sources des valeurs d’options et d’arguments :
delayOption, messageOption);
Les options et les arguments doivent être déclarés dans le même ordre dans le lambda et dans les paramètres qui suivent le lambda. Si l’ordre n’est pas cohérent, l’un des scénarios suivants se présente :
- Si les options ou arguments placés dans le désordre sont de types distincts, une exception d’exécution est levée. Par exemple,
int
peut apparaître là oùstring
doit se trouver dans la liste des sources. - Si les options ou arguments placés dans le désordre sont du même type, le gestionnaire récupère les valeurs incorrectes dans les paramètres qui lui sont fournis, sans générer d’erreurs. Par exemple,
string
optionx
peut apparaître là oùstring
optiony
doit se trouver dans la liste des sources. Dans ce cas, la variable de la valeur de l’optiony
obtient la valeur de l’optionx
.
Il existe des surcharges de SetHandler qui prennent en charge jusqu’à 8 paramètres, avec des signatures synchrones et asynchrones.
Liaison de paramètres pour plus de 8 options et arguments
Pour gérer plus de 8 options ou pour construire un type personnalisé à partir de plusieurs options, vous pouvez utiliser InvocationContext
ou un classeur personnalisé.
Utilisez InvocationContext
.
Une surcharge de SetHandler permet d’accéder à l’objet InvocationContext. Vous pouvez utiliser InvocationContext
pour obtenir n’importe quel nombre de valeurs d’options et d’arguments. Pour obtenir des exemples, consultez Définir des codes de sortie et Gérer l’arrêt.
Utiliser un classeur personnalisé
Un binder personnalisé vous permet de combiner plusieurs valeurs d’options ou d’arguments en un type complexe, et de les passer à un seul paramètre de gestionnaire. Supposons que vous ayez un type Person
:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
Créez une classe dérivée de BinderBase<T>, où T
est le type à construire en fonction de l’entrée de ligne de commande :
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)
};
}
Avec le binder personnalisé, vous pouvez faire passer votre type personnalisé à votre gestionnaire de la même façon que vous obtenez des valeurs pour les options et les arguments :
rootCommand.SetHandler((fileOptionValue, person) =>
{
DoRootCommand(fileOptionValue, person);
},
fileOption, new PersonBinder(firstNameOption, lastNameOption));
Voici le programme complet dont les exemples précédents sont extraits :
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)
};
}
}
Définir des codes de sortie
Il existe des surcharges de SetHandler qui retournent Task et acceptent Func. Si votre gestionnaire est appelé depuis du code asynchrone, vous pouvez retourner Task<int>
à partir d’un gestionnaire qui utilise l’un des éléments suivants, et utiliser la valeur int
pour définir le code de sortie du processus, comme dans l’exemple suivant :
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);
}
Toutefois, si le lambda lui-même doit être asynchrone, vous ne pouvez pas retourner Task<int>
. Dans ce cas, utilisez InvocationContext.ExitCode. Vous pouvez faire en sorte que l’instance de InvocationContext
soit injectée dans votre lambda à l’aide d’une surcharge de SetHandler qui spécifie InvocationContext
en tant que paramètre unique. Cette surcharge de SetHandler
ne vous permet pas de spécifier des objets IValueDescriptor<T>
. Toutefois, vous pouvez obtenir des valeurs d’options et d’arguments à partir de la propriété ParseResult de InvocationContext
, comme le montre l’exemple suivant :
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 vous n’avez pas de travail asynchrone à effectuer, vous pouvez utiliser les surcharges de Action. Dans ce cas, définissez le code de sortie en utilisant InvocationContext.ExitCode
de la même manière qu’avec un lambda asynchrone.
Par défaut, le code de sortie a la valeur 1. Si vous ne le définissez pas explicitement, sa valeur est 0 quand votre gestionnaire se ferme normalement. Si une exception est levée, la valeur par défaut est conservée.
Types pris en charge
Les exemples suivants montrent du code qui lie certains types couramment utilisés.
Énumérations
Les valeurs des types enum
sont liées par le nom, et la liaison ne respecte pas la casse :
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);
Voici un exemple d’entrée de ligne de commande et de sortie résultante pour l’exemple précédent :
myapp --color red
myapp --color RED
Red
Red
Tableaux et listes
De nombreux types courants qui implémentent IEnumerable sont pris en charge. Par exemple :
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);
Voici un exemple d’entrée de ligne de commande et de sortie résultante pour l’exemple précédent :
--items one --items two --items three
System.Collections.Generic.List`1[System.String]
one
two
three
Dans la mesure où AllowMultipleArgumentsPerToken a la valeur true
, l’entrée suivante génère la même sortie :
--items one two three
Types de système de fichiers
Les applications en ligne de commande qui fonctionnent avec le système de fichiers peuvent utiliser les types FileSystemInfo, FileInfo et DirectoryInfo. L’exemple suivant montre l’utilisation 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);
Avec FileInfo
et DirectoryInfo
, il n’est pas obligatoire d’utiliser du code incluant des critères spéciaux :
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);
Autres types pris en charge
De nombreux types ayant un constructeur qui accepte un seul paramètre de chaîne peuvent être liés de cette façon. Par exemple, le code qui fonctionne avec FileInfo
fonctionne avec Uri à la place.
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);
En plus des types de système de fichiers et Uri
, les types suivants sont pris en charge :
bool
byte
DateTime
DateTimeOffset
decimal
double
float
Guid
int
long
sbyte
short
uint
ulong
ushort
Utiliser des objets System.CommandLine
Il existe une surcharge SetHandler
qui vous donne accès à l’objet InvocationContext. Cet objet peut ensuite être utilisé pour accéder à d’autres objets System.CommandLine
. Par exemple, vous avez accès aux objets suivants :
InvocationContext
Pour obtenir des exemples, consultez Définir des codes de sortie et Gérer l’arrêt.
CancellationToken
Pour plus d’informations sur l’utilisation de CancellationToken, consultez Guide pratique pour la gestion de l’arrêt.
IConsole
IConsole facilite les tests ainsi que de nombreux scénarios d’extensibilité par rapport à l’utilisation de System.Console
. Il est disponible dans la propriété InvocationContext.Console.
ParseResult
L’objet ParseResult est disponible dans la propriété InvocationContext.ParseResult. Il s’agit d’une structure singleton qui représente les résultats de l’analyse de l’entrée de ligne de commande. Vous pouvez l’utiliser pour vérifier la présence d’options ou d’arguments sur la ligne de commande, ou pour obtenir la propriété ParseResult.UnmatchedTokens. Cette propriété contient une liste des jetons qui ont été analysés mais qui ne correspondent à aucune commande, aucune option ou aucun argument configuré.
La liste des jetons sans correspondance est utile dans les commandes qui se comportent comme des wrappers. Une commande de wrapper prend un ensemble de jetons et les transfère à une autre commande ou application. La commande sudo
dans Linux en est un exemple. Le nom d’un utilisateur dont l’identité doit être empruntée est suivi d’une commande à exécuter. Par exemple :
sudo -u admin apt update
Cette ligne de commande exécute la commande apt update
en tant qu’utilisateur admin
.
Pour implémenter une commande de wrapper comme celle-ci, affectez à la propriété de commande TreatUnmatchedTokensAsErrors la valeur false
. La propriété ParseResult.UnmatchedTokens
contient ensuite tous les arguments qui n’appartiennent pas explicitement à la commande. Dans l’exemple précédent, ParseResult.UnmatchedTokens
contient les jetons apt
et update
. Votre gestionnaire de commandes peut ensuite transférer UnmatchedTokens
vers un nouvel appel d’interpréteur de commandes, par exemple.
Validation et liaison personnalisées
Si vous souhaitez fournir du code de validation personnalisé, appelez AddValidator pour votre commande, option ou argument, comme le montre l’exemple suivant :
var delayOption = new Option<int>("--delay");
delayOption.AddValidator(result =>
{
if (result.GetValueForOption(delayOption) < 1)
{
result.ErrorMessage = "Must be greater than 0";
}
});
Si vous souhaitez analyser et valider l’entrée, utilisez un délégué ParseArgument<T>, comme le montre l’exemple suivant :
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.
}
});
Le code précédent affecte la valeur true
à isDefault
afin que le délégué parseArgument
soit appelé même si l’utilisateur n’a pas entré de valeur pour cette option.
Voici quelques exemples de ce que vous pouvez faire avec ParseArgument<T>
, mais pas avec AddValidator
:
Analyse des types personnalisés, par exemple la classe
Person
dans l’exemple suivant :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 };
Analyse d’autres genres de chaîne d’entrée (par exemple, analyse de « 1,2,3 » dans
int[]
).Arité dynamique. Par exemple, vous avez deux arguments définis sous forme de tableaux de chaînes, et vous devez gérer une séquence de chaînes dans l’entrée de ligne de commande. La méthode ArgumentResult.OnlyTake vous permet de diviser dynamiquement les chaînes d’entrée entre les arguments.