Condividi tramite


Come associare argomenti ai gestori in System.CommandLine

Importante

System.CommandLine è attualmente in ANTEPRIMA e questa documentazione si riferisce alla versione 2.0 beta 4. Alcune informazioni riguardano il prodotto in versione non definitiva che potrebbe essere modificato in modo sostanziale prima del rilascio. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Il processo di analisi degli argomenti e la relativa fornitura al codice del gestore comandi è detto associazione di parametri. System.CommandLine ha la possibilità di associare molti tipi di argomento incorporati. Ad esempio, numeri interi, enumerazioni e oggetti file system come FileInfo e DirectoryInfo possono essere associati. È anche possibile associare diversi tipi System.CommandLine.

Convalida dell'argomento predefinita

Gli argomenti presentano tipi e arità previsti. System.CommandLine rifiuta argomenti che non corrispondono a tali aspettative.

Ad esempio, viene visualizzato un errore di analisi se l'argomento per un'opzione integer non è un numero intero.

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

Viene visualizzato un errore di arità se vengono passati più argomenti a un'opzione che presenta massima arità pari a uno:

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

Questo comportamento può essere sottoposto a override impostando Option.AllowMultipleArgumentsPerToken su true. In tal caso è possibile ripetere un'opzione che presenta massima arità pari a uno, ma viene accettato solo l'ultimo valore relativo alla riga. Nell'esempio seguente il valore three viene passato all'app.

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

Associazione di parametri fino a 8 opzioni e argomenti

Nell'esempio seguente viene illustrato come associare le opzioni ai parametri del gestore comandi chiamando 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}");
}

I parametri lambda sono variabili che rappresentano i valori delle opzioni e degli argomenti:

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

Le variabili che seguono l'espressione lambda rappresentano gli oggetti opzione e argomento che rappresentano le origini dei valori delle opzioni e degli argomenti:

delayOption, messageOption);

Le opzioni e gli argomenti devono essere dichiarati nello stesso ordine nell'espressione lambda e nei parametri che seguono l'espressione lambda. Se l'ordine non è coerente, verrà generato uno degli scenari seguenti:

  • Se le opzioni o gli argomenti non ordinati sono di tipi diversi, viene generata un'eccezione di runtime. Ad esempio, potrebbe essere visualizzato un oggetto int laddove string deve trovarsi nell'elenco delle origini.
  • Se le opzioni o gli argomenti non in ordine sono dello stesso tipo, il gestore ottiene automaticamente i valori errati nei parametri forniti. Ad esempio, string l’opzione x potrebbe essere visualizzata laddove string l'opzione y deve trovarsi nell'elenco delle origini. In tal caso, la variabile relativa al valore dell’opzione y ottiene il valore dell'opzione x.

Esistono overload di SetHandler che supportano fino a 8 parametri, con firme sincrone e asincrone.

Associazione di parametri con più di 8 opzioni e argomenti

Per gestire più di 8 opzioni o per costruire un tipo personalizzato da più opzioni, è possibile usare InvocationContext o un binder personalizzato.

Utilizzare InvocationContext.

Un overload SetHandler fornisce l'accesso all'oggetto InvocationContext ed è possibile usare InvocationContext per ottenere un numero qualsiasi di valori di opzioni e argomenti. Per esempio, vedere Impostare i codici di uscita e Gestire la terminazione.

Utilizzare un binder personalizzato

Un binder personalizzato consente di combinare più valori di opzione o argomenti in un tipo complesso e di passarli in un singolo parametro del gestore. Si supponga di avere un tipo Person:

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

Creare una classe derivata da BinderBase<T>, dove T è il tipo da costruire in base all'input della riga di 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)
        };
}

Con ilbinder personalizzato, è possibile passare il tipo personalizzato al gestore nello stesso modo in cui si ottengono valori per opzioni e argomenti:

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

Ecco il programma completo da cui sono tratti gli esempi precedenti:

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

Impostare i codici di uscita

Esistono overload Func che restituiscono Task di SetHandler. Se il gestore viene chiamato dal codice asincrono, è possibile restituire un oggetto Task<int> da un gestore che usa uno di essi e utilizzare il valore int per impostare il codice di uscita del processo, come nell'esempio seguente:

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

Tuttavia, se l'espressione lambda stessa deve essere asincrona, non è possibile restituire un oggetto Task<int>. In tal caso, utilizzare InvocationContext.ExitCode. È possibile inserire l'istanza InvocationContext nell'espressione lambda usando un overload SetHandler che specifica InvocationContext come unico parametro. Questo overload SetHandler non consente di specificare oggetti IValueDescriptor<T>, ma è possibile ottenere valori di opzione e argomento dalla proprietà ParseResult di InvocationContext, come illustrato nell'esempio seguente:

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 non si dispone di operazioni asincrone da eseguire, è possibile usare gli overload Action. In tal caso, impostare il codice di uscita usando InvocationContext.ExitCode nello stesso modo che con un'espressione lambda asincrona.

Per impostazione predefinita, il codice di uscita è 1. Se non viene impostato in modo esplicito, il relativo valore viene impostato su 0 quando il gestore viene chiuso normalmente. Se viene generata un'eccezione, mantiene il valore predefinito.

Tipi supportati

Negli esempi seguenti viene illustrato il codice che associa alcuni tipi di uso comune.

Enumerazioni

I valori dei tipi enum sono associati per nome e l'associazione non fa distinzione tra maiuscole e minuscole:

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

Ecco l'input della riga di comando di esempio e l'output risultante dall'esempio precedente:

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

Matrici ed elenchi

Sono supportati molti tipi comuni che implementano IEnumerable. Ad esempio:

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

Ecco l'input della riga di comando di esempio e l'output risultante dall'esempio precedente:

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

Poiché AllowMultipleArgumentsPerToken è impostato su true, l'input seguente restituisce lo stesso output:

--items one two three

Tipi di file system

Le applicazioni della riga di comando che usano il file system possono usare i tipi FileSystemInfo, FileInfo e DirectoryInfo. L'esempio seguente illustra l'utilizzo di 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 e DirectoryInfo il codice dei criteri di ricerca non è obbligatorio:

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

Altri tipi supportati

Molti tipi che presentano un costruttore che accetta un singolo parametro stringa possono essere associati in questo modo. Ad esempio, il codice che funzionerebbe con FileInfo funziona invece con un oggetto Uri.

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

Oltre ai tipi di file system e Uri, sono supportati i tipi seguenti:

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

Utilizzare gli oggetti System.CommandLine

Esiste un overload SetHandler che consente di accedere all'oggetto InvocationContext. Tale oggetto può quindi essere utilizzato per accedere ad altri oggetti System.CommandLine. Ad esempio, si ha accesso agli oggetti seguenti:

InvocationContext

Per esempio, vedere Impostare i codici di uscita e Gestire la terminazione.

CancellationToken

Per informazioni su come usare CancellationToken, vedere Come gestire la terminazione.

IConsole

IConsole semplifica i test e molti scenari di estendibilità rispetto all'uso di System.Console. È disponibile nella proprietà InvocationContext.Console.

ParseResult

L'oggetto ParseResult è disponibile nella proprietà InvocationContext.ParseResult. Si tratta di una struttura singleton che rappresenta i risultati dell'analisi dell'input della riga di comando. È possibile utilizzarla per verificare la presenza di opzioni o argomenti nella riga di comando o per ottenere la proprietà ParseResult.UnmatchedTokens. Questa proprietà contiene un elenco dei token analizzati ma che non corrispondono ad alcun comando, opzione o argomento configurati.

L'elenco di token non corrispondenti è utile nei comandi che si comportano come wrapper. Un comando wrapper accetta un set di token e li inoltra a un altro comando o app. Il comando sudo in Linux rappresentano un esempio. Accetta il nome di un utente da rappresentare seguito da un comando da eseguire. Ad esempio:

sudo -u admin apt update

Questa riga di comando eseguirà il comando apt update come utente admin.

Per implementare un comando wrapper come questo, impostare la proprietà del comando TreatUnmatchedTokensAsErrors su false. La proprietà ParseResult.UnmatchedTokens conterrà quindi tutti gli argomenti che non appartengono in modo esplicito al comando. Nell'esempio precedente, ParseResult.UnmatchedTokens conterrà i token apt e update. Il gestore dei comandi potrebbe quindi inoltrare UnmatchedTokens a una nuova chiamata della shell, ad esempio.

Convalida personalizzata e binding

Per fornire codice di convalida personalizzato, chiamare AddValidator in relazione al comando, l'opzione o l'argomento, come illustrato nell'esempio seguente:

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

Se si vuole analizzare e convalidare l'input, utilizzare un delegato ParseArgument<T>, come illustrato nell'esempio seguente:

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

Il codice precedente imposta isDefault su true in modo che il delegato parseArgument venga chiamato anche se l'utente non ha immesso un valore per questa opzione.

Di seguito sono riportati alcuni esempi di ciò che è possibile eseguire con ParseArgument<T> ma che non è possibile eseguire con AddValidator:

  • Analisi di tipi personalizzati, ad esempio la classe Person nell'esempio seguente:

    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
    };
    
  • Analisi di altri tipi di stringhe di input (ad esempio, analizzare "1,2,3" in int[]).

  • Arità dinamica. Ad esempio, si dispone di due argomenti definiti come matrici di stringhe ed è necessario gestire una sequenza di stringhe nell'input della riga di comando. Il metodo ArgumentResult.OnlyTake consente di dividere dinamicamente le stringhe di input tra gli argomenti.

Vedi anche

System.CommandLine panoramica