如何將引數繫結至 System.CommandLine 中的處理常式
重要
System.CommandLine
目前為預覽版,而此文件適用於版本 2.0 搶鮮版 (Beta) 4。
部分資訊涉及發行前產品,在發行之前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。
剖析引數並將其提供給命令處理常式程式碼的程序稱為參數繫結。 System.CommandLine
能夠繫結許多內建的引數類型。 例如,整數、列舉和檔案系統物件 (如 FileInfo 和 DirectoryInfo) 可以加以繫結。 也可以繫結數個 System.CommandLine
類型。
內建引數驗證
引數具有預期的類型和 Arity。 System.CommandLine
會拒絕不符合這些期望的引數。
例如,如果整數選項的引數不是整數,則會顯示剖析錯誤。
myapp --delay not-an-int
Cannot parse argument 'not-an-int' as System.Int32.
如果多個引數傳遞給具有最大 arity 為 1 的選項,則會顯示 arity 錯誤:
myapp --delay-option 1 --delay-option 2
Option '--delay' expects a single argument but 2 were provided.
可以將 Option.AllowMultipleArgumentsPerToken 設為 true
來覆寫此行為。 在該情況下,您可以重複具有最大 arity 為 1 的選項,但只會接受該行的最後一個值。 在下列範例中,會將值 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}");
}
Lambda 參數是代表選項和引數值的變數:
(delayOptionValue, messageOptionValue) =>
{
DisplayIntAndString(delayOptionValue, messageOptionValue);
},
Lambda 後面的變數代表作為選項和物件值來源的選項和引數物件:
delayOption, messageOption);
選項和引數必須在 lambda 和跟在 lambda 之後的參數中以相同的順序宣告。 如果順序不一致,則將導致以下其中一種情況:
- 如果順序錯亂的選項或引數屬於不同的類型,則會擲回執行階段例外狀況。 例如,
int
可能會出現在string
應該在來源清單中的位置。 - 如果順序錯亂的選項或引數屬於相同的類型,則處理常式會在提供給它的參數中在無訊息的模式下收到錯誤值。 例如,
string
選項x
可能會出現在string
選項y
應該在來源清單中的位置。 在該情況下,選項y
值的變數會收到選項x
值。
有一些支援最多 8 個參數的 SetHandler 的多載 (同時具有同步和非同步簽章)。
繫結超過 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)
};
}
}
設定結束碼
有一些 SetHandler 的 Task 傳回之 Func 多載。 如果您的處理常式是從非同步程式碼呼叫,您可以從使用這些其中一個的處理常式傳回 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);
}
但是,如果 lambda 本身需要非同步,則您無法傳回 Task<int>
。 在該情況下,請使用 InvocationContext.ExitCode。 您可以使用將 InvocationContext
指定為唯一參數的 SetHandler 多載來取得注入到您的 lambda 中的 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 多載。 在該情況下,請以與使用非同步 lambda 設定的相同方式,來利用 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
檔案系統類型
使用檔案系統的命令列應用程式可以使用 FileSystemInfo、FileInfo 和 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
與使用 System.Console
相比,IConsole 能讓測試以及許多擴充性情境變得更容易。 它可在 InvocationContext.Console 屬性中取得。
ParseResult
ParseResult 物件可在 InvocationContext.ParseResult 屬性中取得。 它是單一結構,代表剖析命令列輸入的結果。 您可以使用它來檢查命令列上是否有選項或引數,或取得 ParseResult.UnmatchedTokens 屬性。 此屬性包含已剖析但不符合任何已設定命令、選項或引數的權杖清單。
不相符的標記清單在行為動作類似於包裝函式的命令中很有用。 包裝函式命令會採用一組權杖,並將其轉送到另一個命令或應用程式。 Linux 中的 sudo
命令就是一個例子。 它會使用要模擬的使用者名稱,後面接著要執行的命令。 例如:
sudo -u admin apt update
此命令列會以使用者 admin
的身分來執行 apt update
命令。
若要像這樣來實作包裝函式命令,請將命令屬性 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 方法可讓您動態分割引數之間的輸入字串。