如何将自变量绑定到 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 值。

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

设置退出代码

存在 SetHandlerTask-returning 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 重载将 InvocationContext 实例注入到 lambda 中。 此 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 设置退出代码,就像使用异步 lambda 一样。

退出代码默认为 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

IConsole 使测试和许多扩展性方案比使用 System.Console 更容易。 InvocationContext.Console 属性中提供了该对象。

ParseResult

InvocationContext.ParseResult 属性中提供了 ParseResult 对象。 它是表示命令行输入分析结果的单一结构。 可用其检查命令行上是否存在选项或自变量,或获取 ParseResult.UnmatchedTokens 属性。 此属性包含已分析但不匹配任何已配置的命令、选项或自变量的令牌列表。

不匹配的令牌列表在行为类似于包装器的命令中非常有用。 包装器命令采用一组令牌,并将其转发到另一个命令或应用。 Linux 中的 sudo 命令是一个示例。 它采用要模拟的用户的名称,后接要运行的命令。 例如:

sudo -u admin apt update

此命令行以用户 apt update 的身份运行 admin 命令。

若要实现类似此命令的包装器命令,请将命令属性 TreatUnmatchedTokensAsErrors 设置为 false。 然后,ParseResult.UnmatchedTokens 属性将包含所有未显式属于此命令的自变量。 在上述示例中,ParseResult.UnmatchedTokens 将包含 aptupdate 令牌。 例如,命令处理程序随后可以将 UnmatchedTokens 转发到新的 shell 调用。

自定义验证和绑定

若要提供自定义验证代码,请对命令、选项或自变量调用 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 概述