Udostępnij za pośrednictwem


Jak powiązać argumenty z procedurami obsługi w programie System.CommandLine

Ważne

System.CommandLine jest obecnie dostępna w wersji zapoznawczej, a ta dokumentacja dotyczy wersji 2.0 beta 4. Niektóre informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany przed jego wydaniem. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Proces analizowania argumentów i podawania ich do kodu programu obsługi poleceń jest nazywany powiązaniem parametrów. System.CommandLine ma możliwość powiązania wielu typów argumentów wbudowanych. Na przykład liczby całkowite, wyliczenia i obiekty systemu plików, takie jak FileInfo i DirectoryInfo mogą być powiązane. Można również powiązać kilka System.CommandLine typów.

Wbudowana weryfikacja argumentu

Argumenty mają oczekiwane typy i arity. System.CommandLine odrzuca argumenty, które nie pasują do tych oczekiwań.

Na przykład błąd analizy jest wyświetlany, jeśli argument opcji liczby całkowitej nie jest liczbą całkowitą.

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

Błąd arity jest wyświetlany, jeśli wiele argumentów jest przekazywanych do opcji, która ma maksymalną wartość arity jednego:

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

To zachowanie można zastąpić, ustawiając wartość Option.AllowMultipleArgumentsPerToken .true W takim przypadku można powtórzyć opcję, która ma maksymalną wartość jedną, ale akceptowana jest tylko ostatnia wartość w wierszu. W poniższym przykładzie wartość three zostanie przekazana do aplikacji.

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

Powiązanie parametru do 8 opcji i argumentów

W poniższym przykładzie pokazano, jak powiązać opcje z parametrami procedury obsługi poleceń, wywołując metodę 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}");
}

Parametry lambda to zmienne reprezentujące wartości opcji i argumentów:

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

Zmienne, które są zgodne z lambda reprezentują opcję i obiekty argumentów, które są źródłami opcji i wartości argumentów:

delayOption, messageOption);

Opcje i argumenty muszą być zadeklarowane w tej samej kolejności w lambda i w parametrach, które są zgodne z lambda. Jeśli kolejność nie jest spójna, jeden z następujących scenariuszy spowoduje:

  • Jeśli opcje lub argumenty poza kolejnością są różnego typu, zgłaszany jest wyjątek w czasie wykonywania. Na przykład może pojawić się, int gdzie string element powinien znajdować się na liście źródeł.
  • Jeśli opcje lub argumenty poza kolejnością są tego samego typu, procedura obsługi dyskretnie pobiera nieprawidłowe wartości w podanych parametrach. Na przykład opcja x może pojawić się, string gdzie string opcja y powinna znajdować się na liście źródeł. W takim przypadku zmienna dla wartości opcji pobiera wartość opcji yx .

Istnieją przeciążenia SetHandler tej obsługi do 8 parametrów z podpisami synchronicznymi i asynchronicznymi.

Powiązanie parametru więcej niż 8 opcji i argumentów

Aby obsłużyć więcej niż 8 opcji lub utworzyć typ niestandardowy z wielu opcji, można użyć InvocationContext lub niestandardowego powiązania.

Korzystanie z polecenia InvocationContext

SetHandler Przeciążenie zapewnia dostęp do InvocationContext obiektu i umożliwia InvocationContext uzyskanie dowolnej liczby wartości opcji i argumentów. Przykłady można znaleźć w temacie Set exit codes and Handle termination (Ustawianie kodów zakończenia i kończenie obsługi).

Używanie niestandardowego powiązania

Niestandardowy binder umożliwia połączenie wielu wartości opcji lub argumentów w typ złożony i przekazanie go do pojedynczego parametru procedury obsługi. Załóżmy, że masz Person typ:

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

Utwórz klasę pochodzącą z BinderBase<T>klasy , gdzie T jest typem do konstruowania na podstawie danych wejściowych wiersza polecenia:

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

Za pomocą niestandardowego powiązania możesz uzyskać niestandardowy typ przekazany do programu obsługi w taki sam sposób, jak w przypadku opcji i argumentów:

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

Oto kompletny program, z którego pochodzą powyższe przykłady:

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

Ustawianie kodów zakończenia

TaskIstnieją -returning Func przeciążenia SetHandler. Jeśli program obsługi jest wywoływany z kodu asynchronicznego, możesz zwrócić element Task<int> z programu obsługi, który używa jednego z tych elementów, i użyć int wartości , aby ustawić kod zakończenia procesu, jak w poniższym przykładzie:

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

Jeśli jednak sama lambda musi być asynchronizna, nie można zwrócić elementu Task<int>. W takim przypadku użyj polecenia InvocationContext.ExitCode. Wystąpienie wstrzyknięte do lambda można uzyskać InvocationContext przy użyciu przeciążenia programu SetHandler, które określa InvocationContext jako jedyny parametr. To SetHandler przeciążenie nie pozwala określić IValueDescriptor<T> obiektów, ale można uzyskać wartości opcji i argumentów InvocationContextz właściwości ParseResult klasy , jak pokazano w poniższym przykładzie:

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

Jeśli nie masz do wykonania pracy asynchronicznej, możesz użyć Action przeciążeń. W takim przypadku ustaw kod zakończenia przy użyciu InvocationContext.ExitCode tego samego sposobu, jak w przypadku asynchronicznego wyrażenia lambda.

Kod zakończenia jest domyślnie ustawiona na 1. Jeśli nie ustawisz go jawnie, jego wartość jest ustawiona na 0, gdy program obsługi kończy działanie normalnie. Jeśli zostanie zgłoszony wyjątek, zachowa wartość domyślną.

Obsługiwane typy

W poniższych przykładach pokazano kod, który wiąże niektóre powszechnie używane typy.

Wyliczenia

Wartości enum typów są powiązane według nazwy, a powiązanie jest bez uwzględniania wielkości liter:

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

Oto przykładowe dane wejściowe wiersza polecenia i wynikowe dane wyjściowe z poprzedniego przykładu:

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

Tablice i listy

Obsługiwane są wiele typowych typów implementujących IEnumerable . Na przykład:

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

Oto przykładowe dane wejściowe wiersza polecenia i wynikowe dane wyjściowe z poprzedniego przykładu:

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

Ponieważ AllowMultipleArgumentsPerToken jest ustawiona na truewartość , następujące dane wejściowe są wynikiem tych samych danych wyjściowych:

--items one two three

Typy systemów plików

Aplikacje wiersza polecenia, które współpracują z systemem plików, mogą używać FileSystemInfotypów , FileInfoi DirectoryInfo . W poniższym przykładzie pokazano użycie elementu 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);

W przypadku FileInfo elementów i DirectoryInfo kod pasujący do wzorca nie jest wymagany:

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

Inne obsługiwane typy

W ten sposób można powiązać wiele typów, które mają konstruktor, który przyjmuje pojedynczy parametr ciągu. Na przykład kod, który będzie współdziałał z FileInfo elementem , działa z elementem 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);

Oprócz typów systemu plików i Uriobsługiwane są następujące typy:

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

Korzystanie z System.CommandLine obiektów

SetHandler Istnieje przeciążenie, które zapewnia dostęp do InvocationContext obiektu. Ten obiekt może następnie służyć do uzyskiwania dostępu do innych System.CommandLine obiektów. Na przykład masz dostęp do następujących obiektów:

InvocationContext

Przykłady można znaleźć w temacie Set exit codes and Handle termination (Ustawianie kodów zakończenia i kończenie obsługi).

CancellationToken

Aby uzyskać informacje o sposobie używania programu CancellationToken, zobacz Jak obsługiwać kończenie żądań.

IConsole

IConsole sprawia, że testowanie, a także wiele scenariuszy rozszerzalności jest łatwiejsze niż korzystanie z programu System.Console. Jest ona dostępna we InvocationContext.Console właściwości .

ParseResult

Obiekt ParseResult jest dostępny we InvocationContext.ParseResult właściwości . Jest to pojedyncza struktura, która reprezentuje wyniki analizowania danych wejściowych wiersza polecenia. Można go użyć, aby sprawdzić obecność opcji lub argumentów w wierszu polecenia lub uzyskać ParseResult.UnmatchedTokens właściwość. Ta właściwość zawiera listę tokenów, które zostały przeanalizowane , ale nie są zgodne z żadnym skonfigurowanym poleceniem, opcją lub argumentem.

Lista niezgodnych tokenów jest przydatna w poleceniach, które zachowują się jak otoki. Polecenie otoki pobiera zestaw tokenów i przekazuje je do innego polecenia lub aplikacji. Polecenie sudo w systemie Linux jest przykładem. Pobiera nazwę użytkownika, aby personifikować, a następnie polecenie do uruchomienia. Na przykład:

sudo -u admin apt update

Ten wiersz polecenia uruchomi apt update polecenie jako użytkownik admin.

Aby zaimplementować polecenie otoki podobne do tego, ustaw właściwość TreatUnmatchedTokensAsErrors polecenia na false. ParseResult.UnmatchedTokens Następnie właściwość będzie zawierać wszystkie argumenty, które nie należą jawnie do polecenia . W poprzednim przykładzie ParseResult.UnmatchedTokens będzie zawierać apt tokeny i .update Program obsługi poleceń może następnie przekazać element UnmatchedTokens do nowej wywołania powłoki, na przykład.

Walidacja niestandardowa i powiązanie

Aby podać niestandardowy kod weryfikacji, wywołaj AddValidator polecenie, opcję lub argument, jak pokazano w poniższym przykładzie:

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

Jeśli chcesz przeanalizować, a także zweryfikować dane wejściowe, użyj delegata ParseArgument<T> , jak pokazano w poniższym przykładzie:

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

Powyższy kod ustawia wartość isDefault tak true , aby parseArgument delegat był wywoływany, nawet jeśli użytkownik nie wprowadzi wartości dla tej opcji.

Poniżej przedstawiono kilka przykładów tego, co można zrobić z tym, czego nie można zrobić za ParseArgument<T> pomocą AddValidatorpolecenia :

  • Analizowanie typów niestandardowych, takich jak Person klasa w poniższym przykładzie:

    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
    };
    
  • Analizowanie innych rodzajów ciągów wejściowych (na przykład analizowanie wartości "1,2,3" do int[]elementu ).

  • Dynamiczna arytacja. Na przykład masz dwa argumenty zdefiniowane jako tablice ciągów i musisz obsługiwać sekwencję ciągów w danych wejściowych wiersza polecenia. Metoda ArgumentResult.OnlyTake umożliwia dynamiczne dzielenie ciągów wejściowych między argumentami.

Zobacz też

System.CommandLine Przegląd