Partager via


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 option x peut apparaître là où string option y doit se trouver dans la liste des sources. Dans ce cas, la variable de la valeur de l’option y obtient la valeur de l’option x.

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.

Voir aussi

Vue d’ensemble de System.CommandLine