Поделиться через


Как привязать аргументы к обработчикам System.CommandLine

Внимание

System.CommandLine в настоящее время находится в предварительной версии, и эта документация предназначена для версии 2.0 бета-версии 4. Некоторые сведения относятся к предварительному выпуску продукта, который может быть существенно изменен до его выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

Процесс синтаксического анализа аргументов и их предоставление коду обработчика команд называется привязкой параметров. System.CommandLine имеет возможность привязать множество типов аргументов, встроенных. Например, целые числа, перечисления и объекты файловой системы, такие как FileInfo и DirectoryInfo могут быть привязаны. Можно также привязать несколько System.CommandLine типов.

Встроенная проверка аргументов

Аргументы имеют ожидаемые типы и arity. System.CommandLine отклоняет аргументы, которые не соответствуют этим ожиданиям.

Например, ошибка синтаксического анализа отображается, если аргумент для целочисленного параметра не является целым числом.

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

Ошибка arity отображается, если несколько аргументов передаются в параметр, имеющий максимальное число аргументов:

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

Это поведение можно переопределить, установив для trueпараметра Option.AllowMultipleArgumentsPerToken значение . В этом случае можно повторить параметр, имеющий максимальное значение одного, но принимается только последнее значение строки. В следующем примере значение three будет передано приложению.

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

Привязка параметров до 8 параметров и аргументов

В следующем примере показано, как привязать параметры к параметрам обработчика команд путем вызова 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}");
}

Лямбда-параметры — это переменные, представляющие значения параметров и аргументов:

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

Переменные, следуйте лямбда-коду, представляют объекты параметра и аргумента, которые являются источниками значений параметра и аргументов:

delayOption, messageOption);

Параметры и аргументы должны быть объявлены в том же порядке в лямбда-файле и в параметрах, следуют лямбда-лямбда-аргументам. Если порядок не согласован, один из следующих сценариев приведет к следующему:

  • Если параметры или аргументы вне порядка имеют разные типы, создается исключение во время выполнения. Например, может появиться место int , где string должен находиться список источников.
  • Если параметры или аргументы вне порядка имеют одинаковый тип, обработчик автоматически получает неправильные значения в параметрах, предоставленных ему. Например, параметр x может отображаться, string где string должен находиться параметр y в списке источников. В этом случае переменная для значения параметра y получает значение параметра x .

Существуют перегрузки SetHandler , поддерживающие до 8 параметров, с синхронными и асинхронными сигнатурами.

Привязка параметров более 8 параметров и аргументов

Для обработки более 8 параметров или создания пользовательского типа из нескольких параметров можно использовать InvocationContext или пользовательский привязку.

Использование InvocationContext

Перегрузка SetHandler предоставляет доступ к объекту InvocationContext , и можно использовать InvocationContext для получения любого количества значений параметра и аргументов. Примеры см. в разделе "Настройка кодов выхода" и "Обработка завершения".

Использование пользовательского привязчика

Пользовательская привязка позволяет объединять несколько значений параметров или аргументов в сложный тип и передавать их в один параметр обработчика. Предположим, у вас есть Person тип:

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

Создайте класс, производный от BinderBase<T>типа T для создания на основе входных данных командной строки:

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

С помощью пользовательского привязчика вы можете получить настраиваемый тип, переданный обработчику так же, как и значения параметров и аргументов:

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

Ниже приведена полная программа, из которых взяты предыдущие примеры:

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

Установка кодов выхода

Существуют возвращающие Taskперегрузки SetHandlerFunc. Если обработчик вызывается из асинхронного кода, можно вернуть Task<int> из обработчика, использующего одно из них, и использовать int значение для задания кода выхода процесса, как показано в следующем примере:

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

Однако если лямбда-лямбда-сам должен быть асинхронным, вы не можете вернуть .Task<int> В этом случае используйте InvocationContext.ExitCode. Экземпляр, внедренный в лямбда-файл, можно получить InvocationContext с помощью перегрузки SetHandler, указывающей InvocationContext в качестве единственного параметра. Эта SetHandler перегрузка не позволяет указывать IValueDescriptor<T> объекты, но вы можете получить значения параметров и аргументов из свойства InvocationContextParseResult, как показано в следующем примере:

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

Если у вас нет асинхронной работы, можно использовать Action перегрузки. В этом случае задайте код выхода с помощью InvocationContext.ExitCode асинхронного лямбда-кода.

Код выхода по умолчанию — 1. Если он не задан явным образом, его значение равно 0 при выходе обработчика. Если создается исключение, оно сохраняет значение по умолчанию.

Поддерживаемые типы

В следующих примерах показан код, который привязывает некоторые часто используемые типы.

Перечисления

Значения enum типов привязаны по имени, и привязка не учитывает регистр:

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

Ниже приведен пример входных данных командной строки и выходные данные из предыдущего примера:

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

Массивы и списки

Поддерживаются многие распространенные типы, реализующие IEnumerable . Например:

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

Ниже приведен пример входных данных командной строки и выходные данные из предыдущего примера:

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

Так как AllowMultipleArgumentsPerToken задано значение true, следующие входные данные приводят к тому же выводу:

--items one two three

Типы файловой системы

Приложения командной строки, работающие с файловой системой, могут использовать FileSystemInfoFileInfoи DirectoryInfo типы. В следующем примере показано использование 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);

При FileInfo использовании и DirectoryInfo коде сопоставления шаблонов не требуется:

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

Другие поддерживаемые типы

Многие типы с конструктором, принимаюющим один строковый параметр, можно связать таким образом. Например, код, который будет работать с FileInfo работой 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);

Помимо типов файловой системы и Uriподдерживаются следующие типы:

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

Использование System.CommandLine объектов

SetHandler Существует перегрузка, которая предоставляет доступ к объектуInvocationContext. Затем этот объект можно использовать для доступа к другим System.CommandLine объектам. Например, у вас есть доступ к следующим объектам:

InvocationContext

Примеры см. в разделе "Настройка кодов выхода" и "Обработка завершения".

CancellationToken

Сведения об использовании CancellationTokenсм. в разделе "Обработка завершения".

IConsole

IConsole упрощает тестирование, а также множество сценариев расширяемости, чем использование System.Console. Он доступен в свойстве InvocationContext.Console .

ParseResult

Объект ParseResult доступен в свойстве InvocationContext.ParseResult . Это одноэлементная структура, представляющая результаты анализа входных данных командной строки. Его можно использовать для проверка для наличия параметров или аргументов в командной строке или для получения ParseResult.UnmatchedTokens свойства. Это свойство содержит список маркеров , которые были проанализированы, но не соответствовали какой-либо настроенной команде, параметру или аргументу.

Список несовпаденных маркеров полезен в командах, которые ведут себя как оболочки. Команда оболочки принимает набор маркеров и перенаправит их в другую команду или приложение. Команда sudo в Linux является примером. Он принимает имя пользователя для олицетворения, за которым следует выполнить команду. Например:

sudo -u admin apt update

Эта командная строка будет выполнять apt update команду от имени пользователя admin.

Чтобы реализовать команду-оболочку, например эту, задайте для свойства TreatUnmatchedTokensAsErrors команды значение false. ParseResult.UnmatchedTokens Затем свойство будет содержать все аргументы, которые явно не принадлежат команде. В предыдущем примере ParseResult.UnmatchedTokens будет содержаться apt и update маркеры. Обработчик команд может перенаправить UnmatchedTokens вызов оболочки в новый вызов оболочки, например.

Настраиваемая проверка и привязка

Чтобы предоставить пользовательский код проверки, вызовите AddValidator команду, параметр или аргумент, как показано в следующем примере:

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

Если вы хотите проанализировать, а также проверить входные данные, используйте ParseArgument<T> делегат, как показано в следующем примере:

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

Предыдущий код задает isDefault значение true , чтобы parseArgument делегат был вызван, даже если пользователь не ввел значение для этого параметра.

Ниже приведены некоторые примеры того, что вы можете сделать с ParseArgument<T> этим не AddValidatorудается:

  • Анализ пользовательских типов, таких как Person класс в следующем примере:

    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
    };
    
  • Анализ других типов входных строк (например, синтаксический анализ "1,2,3" в int[]).

  • Динамическая arity. Например, у вас есть два аргумента, которые определяются как массивы строк, и необходимо обрабатывать последовательность строк в входных данных командной строки. Метод ArgumentResult.OnlyTake позволяет динамически разделить входные строки между аргументами.

См. также

Обзор System.CommandLine