Как привязать аргументы к обработчикам 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>
объекты, но вы можете получить значения параметров и аргументов из свойства InvocationContext
ParseResult, как показано в следующем примере:
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 позволяет динамически разделить входные строки между аргументами.