Dela via


Så här binder du argument till hanterare i System.CommandLine

Viktigt!

System.CommandLine är för närvarande i förhandsversion och den här dokumentationen är för version 2.0 beta 4. Viss information gäller förhandsversionsprodukt som kan ändras avsevärt innan den släpps. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, avseende informationen som visas här.

Processen för att parsa argument och ge dem till kommandohanterarkod kallas parameterbindning. System.CommandLine har möjlighet att binda många inbyggda argumenttyper. Till exempel kan heltal, uppräkningar och filsystemobjekt som FileInfo och DirectoryInfo vara bundna. Flera System.CommandLine typer kan också bindas.

Inbyggd argumentverifiering

Argumenten har förväntade typer och aritet. System.CommandLine avvisar argument som inte matchar dessa förväntningar.

Ett parsfel visas till exempel om argumentet för ett heltalsalternativ inte är ett heltal.

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

Ett aritetsfel visas om flera argument skickas till ett alternativ som har maximal aritet på ett:

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

Det här beteendet kan åsidosättas genom att ställa in Option.AllowMultipleArgumentsPerTokentrue. I så fall kan du upprepa ett alternativ som har maximal aritet på ett, men endast det sista värdet på raden accepteras. I följande exempel skickas värdet three till appen.

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

Parameterbindning upp till 8 alternativ och argument

I följande exempel visas hur du binder alternativ till kommandohanterarparametrar genom att anropa 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}");
}

Lambda-parametrarna är variabler som representerar värdena för alternativ och argument:

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

Variablerna som följer lambda representerar alternativet och argumentobjekten som är källorna till alternativet och argumentvärdena:

delayOption, messageOption);

Alternativen och argumenten måste deklareras i samma ordning i lambda och i parametrarna som följer lambda. Om ordningen inte är konsekvent resulterar något av följande scenarier:

  • Om out-of-order-alternativen eller argumenten är av olika typer genereras ett körningsfel. En kan till exempel int visas där en string ska finnas i listan över källor.
  • Om de oordnade alternativen eller argumenten är av samma typ får hanteraren i tysthet fel värden i de parametrar som anges till den. Alternativet x kan till exempel string visas där string alternativet y ska finnas i listan över källor. I så fall hämtar variabeln för alternativvärdet y alternativvärdet x .

Det finns överlagringar av SetHandler som stöder upp till 8 parametrar, med både synkrona och asynkrona signaturer.

Parameterbindning fler än 8 alternativ och argument

Om du vill hantera fler än 8 alternativ eller skapa en anpassad typ från flera alternativ kan du använda InvocationContext eller en anpassad bindemedel.

Använda InvocationContext

En SetHandler överlagring ger åtkomst till InvocationContext objektet och du kan använda InvocationContext för att hämta valfritt antal alternativ och argumentvärden. Exempel finns i Ange utgångskoder och Hantera avslutning.

Använda en anpassad pärm

Med en anpassad pärm kan du kombinera flera alternativ- eller argumentvärden till en komplex typ och skicka det till en enda hanterarparameter. Anta att du har en Person typ:

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

Skapa en klass härledd från BinderBase<T>, där T är den typ som ska konstrueras baserat på kommandoradsindata:

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

Med den anpassade pärmen kan du få din anpassade typ skickad till hanteraren på samma sätt som du får värden för alternativ och argument:

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

Här är det fullständiga programmet som föregående exempel hämtas från:

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

Ange utgångskoder

Det finns Task- returnerar Func-överlagringar av SetHandler. Om hanteraren anropas från asynkron kod kan du returnera en Task<int> från en hanterare som använder någon av dessa och använda int värdet för att ange processens slutkod, som i följande exempel:

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

Men om lambda måste vara asynkron kan du inte returnera en Task<int>. I så fall använder du InvocationContext.ExitCode. Du kan få instansen InvocationContext inmatad i din lambda med hjälp av en SetHandler-överlagring som anger InvocationContext som den enda parametern. Med den här SetHandler överlagringen kan du inte ange IValueDescriptor<T> objekt, men du kan hämta alternativ- och argumentvärden från parseResult-egenskapen för InvocationContext, som du ser i följande exempel:

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

Om du inte har asynkront arbete att utföra kan du använda överlagringarna Action . I så fall anger du slutkoden på InvocationContext.ExitCode samma sätt som med en asynkron lambda.

Slutkoden är som standard 1. Om du inte anger det explicit anges dess värde till 0 när hanteraren avslutas normalt. Om ett undantag utlöses behåller det standardvärdet.

Typer som stöds

I följande exempel visas kod som binder vissa vanliga typer.

Uppräkningar

Värdena för enum typerna är bundna efter namn och bindningen är skiftlägesokänslig:

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

Här är exempel på kommandoradsindata och resulterande utdata från föregående exempel:

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

Matriser och listor

Många vanliga typer som implementeras IEnumerable stöds. Till exempel:

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

Här är exempel på kommandoradsindata och resulterande utdata från föregående exempel:

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

Eftersom AllowMultipleArgumentsPerToken är inställt truepå resulterar följande indata i samma utdata:

--items one two three

Filsystemtyper

Kommandoradsprogram som fungerar med filsystemet kan använda typerna FileSystemInfo, FileInfooch DirectoryInfo . I följande exempel visas användningen av 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);

Med FileInfo och DirectoryInfo mönstermatchningskoden krävs inte:

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

Andra typer som stöds

Många typer som har en konstruktor som tar en enskild strängparameter kan bindas på det här sättet. Kod som skulle fungera med fungerar till exempel med FileInfo en Uri i stället.

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

Förutom filsystemtyperna och Uristöds följande typer:

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

Använda System.CommandLine objekt

Det finns en SetHandler överlagring som ger dig åtkomst till objektet InvocationContext . Objektet kan sedan användas för att komma åt andra System.CommandLine objekt. Du har till exempel åtkomst till följande objekt:

InvocationContext

Exempel finns i Ange utgångskoder och Hantera avslutning.

CancellationToken

Information om hur du använder CancellationTokenfinns i Hantera avslutning.

IConsole

IConsole gör testning och många utökningsscenarier enklare än att använda System.Console. Den är tillgänglig i egenskapen InvocationContext.Console .

ParseResult

Objektet ParseResult är tillgängligt i egenskapen InvocationContext.ParseResult . Det är en singleton-struktur som representerar resultatet av parsning av kommandoradsindata. Du kan använda den för att kontrollera om det finns alternativ eller argument på kommandoraden eller för att hämta ParseResult.UnmatchedTokens egenskapen. Den här egenskapen innehåller en lista över de token som parsades men som inte matchade något konfigurerat kommando, alternativ eller argument.

Listan över omatchade token är användbar i kommandon som fungerar som omslutningar. Ett omslutningskommando tar en uppsättning token och vidarebefordrar dem till ett annat kommando eller en annan app. Kommandot sudo i Linux är ett exempel. Det tar namnet på en användare att personifiera följt av ett kommando som ska köras. Till exempel:

sudo -u admin apt update

Den här kommandoraden apt update skulle köra kommandot som användaren admin.

Om du vill implementera ett omslutningskommando som det här anger du kommandoegenskapen TreatUnmatchedTokensAsErrors till false. ParseResult.UnmatchedTokens Sedan innehåller egenskapen alla argument som inte uttryckligen tillhör kommandot. I föregående exempel ParseResult.UnmatchedTokens skulle innehålla apt token och update . Kommandohanteraren kan sedan vidarebefordra UnmatchedTokens till ett nytt gränssnittsanrop, till exempel.

Anpassad validering och bindning

Om du vill ange anpassad valideringskod anropar AddValidator du kommandot, alternativet eller argumentet enligt följande exempel:

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

Om du vill parsa och verifiera indata använder du ett ParseArgument<T> ombud, som du ser i följande exempel:

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

Föregående kod anges isDefault till true så att ombudet parseArgument anropas även om användaren inte angav något värde för det här alternativet.

Här är några exempel på vad du kan göra med ParseArgument<T> som du inte kan göra med AddValidator:

  • Parsning av anpassade typer, till exempel Person klassen i följande exempel:

    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
    };
    
  • Parsning av andra typer av indatasträngar (till exempel parsa "1,2,3" i int[]).

  • Dynamisk aritet. Du har till exempel två argument som definieras som strängmatriser, och du måste hantera en sekvens med strängar i kommandoradsindata. Med ArgumentResult.OnlyTake metoden kan du dynamiskt dela upp indatasträngarna mellan argumenten.

Se även

System.CommandLine Översikt