共用方式為


如何將引數繫結至 System.CommandLine 中的處理常式

重要

System.CommandLine 目前為預覽版,而此文件適用於版本 2.0 搶鮮版 (Beta) 4。 部分資訊涉及發行前產品,在發行之前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

剖析引數並將其提供給命令處理常式程式碼的程序稱為參數繫結System.CommandLine 能夠繫結許多內建的引數類型。 例如,整數、列舉和檔案系統物件 (如 FileInfoDirectoryInfo) 可以加以繫結。 也可以繫結數個 System.CommandLine 類型。

內建引數驗證

引數具有預期的類型和 AritySystem.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)
            };
    }
}

設定結束碼

有一些 SetHandlerTask 傳回之 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> 物件,但您可以從 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 多載。 在該情況下,請以與使用非同步 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

檔案系統類型

使用檔案系統的命令列應用程式可以使用 FileSystemInfoFileInfoDirectoryInfo 類型。 下列範例示範 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);

使用 FileInfoDirectoryInfo 時,不需要模式比對程式碼:

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 會包含 aptupdate 權杖。 您的命令處理常式因此可以將 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 概觀