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
gdziestring
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
gdziestring
opcjay
powinna znajdować się na liście źródeł. W takim przypadku zmienna dla wartości opcji pobiera wartość opcjiy
x
.
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 InvocationContext
z 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 true
wartość , 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 Uri
obsł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ą AddValidator
polecenia :
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.