2016 年 9 月

第 31 卷,第 9 期

必备 .NET - 使用 .NET Core 1.0 进行命令行处理

作者 Mark Michaelis

Mark Michaelis在本月的 Essential .NET 专栏中,我将继续深入探究 .NET Core 的各种功能,这次借助的是完整发布版本(不再使用 beta 版本或正式版之前的候选发布版)。具体来说,我将重点关注其命令行实用程序(可从 github.com/aspnet/Common 的 .NET Core 通用库中找到)以及如何将它们用于分析命令行。.NET Core 中内置了命令行分析支持,实际上自 .NET Framework 1.0 之后我一直希望能够实现此功能,对此我感到非常兴奋。我希望内置了 .NET Core 的库有助于标准化各程序之间的命令行格式/结构,哪怕是一点点帮助。通常情况下,大多数人默认都有一个约定的标准,而不会去创建自己的标准。对于我来说,什么是标准,这一点并不重要。

命令行约定

在 Microsoft.Extensions.CommandLineUtils NuGet 包中可找到大批量的命令行功能。程序集中包含的 CommandLineApplication 类为命令行分析提供了以下支持:选项的短名称和长名称、分配带有冒号或等于号的值(一个或多个),以及用于提供帮助的 -? 符号。谈到帮助,该类还支持自动显示帮助文本。 1 显示受支持的示例命令行。

图 1 示例命令行

选项 Program.exe -f=Inigo, -l Montoya –hello –names Princess –names Buttercup

带有值“Inigo”的选项 -f

带有值“Montoya”的选项 -l

带有值“on”的选项 –hello

带有值“Princess”和“Buttercup”的选项 –names

带参数的命令 Program.exe "hello", "Inigo", "Montoya", "It", "is", "a", "pleasure", "to", "meet", "you."

命令“hello”

参数“Inigo”

参数“Montoya”

带有值“It”、“is”、“a”、“pleasure”、“to”、“meet”、“you”的参数问候语。

符号 Program.exe -? 显示帮助

根据后面的说明得知,存在多种参数类型,其中一个称为“参数”。 指示针对命令行指定的值的术语“参数”的重载与命令行配置数据可能产生严重歧义。因此,在本文后续部分中,我将对任何类型的通用参数(以可执行文件名称指定)以及区分大小写的称为“参数”(首字母大写)的参数类型进行区分。同样,我将使用通常指参数的首字母大写(而不是小写)术语形式来区分其他两种参数类型:选项和命令。请注意这一点,这是本文剩余部分的重点所在。

对每个参数类型的说明如下:

  • 选项: 选项通过名称进行标识,其名称前缀为单划线 (-) 或双划线 (--)。选项名称使用模板通过编程方式进行定义,模板可能包括以下三个指示符中的一个或多个:短名称、长名称、符号。此外,选项可能还具有一个与之关联的值。例如,模板可能为“-n | --name | -# <Full Name>,”,这样一来,就可以通过三个指示符中的任何一个来确定全名选项。(但模板不需要同时包含三个指示符。) 请注意,单划线或双划线用于确定是否指定了短名称或长名称,无需关注名称的实际长度。
    要将某个值与某个选项关联,你可以使用空格或赋值运算符 (=). -f=Inigo 和 -l Montoya,这也是指定选项值的两个示例。
    如果模板中使用了数字,则这些数字将作为短名称或长名称的一部分,而不是符号。
  • 参数: 通过参数显示的顺序(而不是参数名称)可以确定参数。因此,如果命令行上的值的前缀不是选项名称,则为参数。值所对应的参数基于参数显示的顺序(计数中不包括选项和命令)。
  • 命令: 命令提供一组参数和选项。例如,你可以在命令名称“hello”后跟随参数和选项(甚至是子命令)的组合。已配置的关键字(即命令名称)对将成为命令定义一部分的命令名称后跟的所有值进行分组,命令则由该关键字进行标识。

配置命令行

在引用以 CommandLineApplication 类开头的 .NET Core Microsoft.Extensions.CommandLineUtils 后编程命令行。你可以使用此类配置每个命令、选项和参数。在实例化 CommandLineApplication 时,构造函数具有的可选布尔值将命令行配置为在显示的参数未经专门配置情况下(默认)引发一个异常。

获取 CommandLineApplication 的实例后,你就可以使用选项、参数和命令这几种方法来配置参数。例如,设想你想要支持如下所示的命令行语法,其中方括号中的项是可选的,尖括号中的项是用户定义的值或参数:

Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>] 
     [-?|-h|--help] [-u|--uppercase]

图 2 配置基本分析功能。

图2 配置命令行

public static void Main(params string[] args)
{
    // Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>]
    // [-?|-h|--help] [-u|--uppercase]
  CommandLineApplication commandLineApplication =
    new CommandLineApplication(throwOnUnexpectedArg: false);
  CommandArgument names = null;
  commandLineApplication.Command("name",
    (target) =>
      names = target.Argument(
        "fullname",
        "Enter the full name of the person to be greeted.",
        multipleValues: true));
  CommandOption greeting = commandLineApplication.Option(
    "-$|-g |--greeting <greeting>",
    "The greeting to display. The greeting supports"
    + " a format string where {fullname} will be "
    + "substituted with the full name.",
    CommandOptionType.SingleValue);
  CommandOption uppercase = commandLineApplication.Option(
    "-u | --uppercase", "Display the greeting in uppercase.",
    CommandOptionType.NoValue);
  commandLineApplication.HelpOption("-? | -h | --help");
  commandLineApplication.OnExecute(() =>
  {
    if (greeting.HasValue())
    {
      Greet(greeting.Value(), names.Values, uppercase.HasValue());
    }
    return 0;
  });
  commandLineApplication.Execute(args);
}
private static void Greet(
  string greeting, IEnumerable<string> values, bool useUppercase)
{
  Console.WriteLine(greeting);
}

示例以 CommandLineApplication 开头

首先,我实例化 CommandLineApplication,同时指定命令行分析是否受限(throwOnUnexpectedArg 为 true 则受限)。如果我在参数异常时指定引发一个异常,则必须显式配置所有参数。或者,如果 throwOnUnexpectedArg 为 false,则配置无法识别的所有参数都将存储到 CommandLineApplication.Remaining­Arguments 字段中。

配置命令及其参数

图 2 中的下一步骤将配置“name”命令。将从一列实参中识别出命令的关键字是命令函数的第一个形参,即 name。第二个形参是一个 Action<CommandLineApplication> 委派调用的配置,其中 name 命令的所有子实参均已配置。在这种情况下,只有一个类型为 CommandArgument 的参数,其变量名为“greeting”。 然而,完全可以在配置委派内添加其他参数、选项甚至是子命令。另外,委派的目标参数 CommandLineApplication 所具有的 Parent 属性指回 commandLineArgument,这是目标的父 CommandLineArgument,其中已配置 name 命令。

请注意,在配置“Argument”名称时,我专门标识了它将支持 multipleValues。这样一来,就允许指定多个值,在这种情况下就有多个名称。其中每个值都出现在“name”参数标识符之后,直到另一个参数或选项标识符出现。实参函数的前两个形参为 name,指示该实参的名称,你可以通过一列实参和说明进行定义。

对于名称命令配置,最后需要指出的一点是,实际上,你需要保存来自参数函数(以及选项函数(如果有))的返回内容。请务必进行保存,这样你就可以在稍后检索与“Argument”名称关联的参数。如果不保存引用,你最终只能通过 commandLineApplication.Commands[0].Arguments 集合进行搜索以检索参数数据。

保存所有命令行数据有一种稳妥的方式,即将其置于由 ASP.NET 基架存储库 (github.com/aspnet/Scaffolding)(具体位于 src/Microsoft.VisualStudio.Web.CodeGeneration.Core/CommandLine 文件夹中)中的属性进行修饰的单独类中。有关详细信息,请参阅“通过 .NET Core 实现命令行类”(bit.ly/296SluA)。

配置选项

图 2 中配置的下一个参数是 greeting 选项,其类型为 CommandOption。选项的配置通过选项函数来完成,其中第一个参数是一个称为“模板”的字符串参数。请注意,你可以为选项指定三个不同的名称(例如,-$、-g 和 -greeting),每个名称都将用于标识参数列表中的选项。另外,模板可以选择性地指定与其关联的值,方法是在选项标识符后跟一个带尖括号的名称。在说明参数之后,选项函数包括一个必需的 CommandOptionType 参数。此选项标识以下内容:

  1. 是否在选项标识符后指定任何值。如果指定了 NoValue 的 CommandOptionType,且选项在参数列表中出现,则会将 CommandOption.Value 函数设置为“on”。即使在在选项标识符后指定了不同值,并且无论是否实际指定了值,都会返回值“on”。若要参阅示例,请查看图 2 中的大写形式的选项。
  2. 或者,如果 CommandOptionType 为 SingleValue,且指定了选项标识符但没有出现值,则会引发 CommandParsingException 以确定未标识的选项,因为它与模板不匹配。也就是说,SingleValue 提供了一种方式来检查是否提供了值,前提是假定选项标识符肯定会出现。
  3. 最后,你可以提供 Multiple­Value 的 CommandOptionType。然而,与关联到命令的多个值不同的是,对于选项而言,多个值允许多次指定同一选项。例如,program.exe -name Inigo -name Montoya。

请注意,将不会配置任何配置选项,因此选项是必需的。实际上,这也同样适用于参数。若要在指定了值时引发错误,你需要检查在返回 false 时,HasValue 函数是否会报告错误。对于 CommandArgument 来说,Value 属性将在未指定值时返回 null。若要报告错误,请考虑在帮助文本后显示一条错误消息,使用户获得有关更正问题的必要操作的更多信息。

CommandLineApplication 分析机制的另一个重要行为是,它区分大小写。另外,实际上此时已没有可让你不区分大小写的简单配置选项。因此,你将需要事先(通过我所简述的 Execute 方法)更改传递到 CommandLineApplication 的实际参数的大小写以达到不区分大小写的目的。(或者,你可以尝试在 github.com/aspnet/Common 上提交一个拉取请求来启用此选项。)

显示帮助和版本

CommandLineApplication 中内置的 ShowHelp 函数可自动显示与命令行配置相关联的帮助文本。例如,图 3 显示图 2 的 ShowHelp 输出。

图 3 ShowHelp 显示输出

Usage:  [options] [command]
Options:
  -$|-g |--greeting <greeting>  The greeting to display. 
                                The greeting supports a format string 
                                where {fullname} will be substituted 
                                with the full name.
  -u | --uppercase              Display the greeting in uppercase.
  -? | -h | --help              Show help information
Commands:
  name 
Use " [command] --help" for more information about a command.

很遗憾,显示的帮助未能确定选项或命令实际是否可选。换言之,帮助文本(通过方括号)假定并显示所有选项和命令均为可选。

尽管你可以显式调用 ShowHelp,例如在处理自定义命令行错误时,每当指定了与 HelpOption 模板匹配的参数时,都将自动进行调用。此外,通过 CommandLineApplication.HelpOption 方法的参数指定 HelpOption 模板。

同样,可以使用 ShowVersion 方法显示你的应用程序版本。与 ShowHelp 类似,可以通过以下两种方法中的一种进行配置:

public CommandOption VersionOption(
  string template, string shortFormVersion, string longFormVersion = null).
public CommandOption VersionOption(
  string template, Func<string> shortFormVersionGetter,
  Func<string> longFormVersionGetter = null)

请注意,这两种方法都需要提供你希望显示以在对 VerisionOption 的调用中指定的版本信息。

分析和读取命令行数据

到目前为止,我已经详细介绍了如何配置 CommandLineApplication,但尚未探讨触发命令行分析的关键流程,或者说是在分析调用后会随即发生的情况。

若要触发命令行分析,你需要调用 CommandLineApplication.Execute 函数并传递为命令行指定的参数列表。在图 1 中,实参在 Main 的 Args 形参中指定,因此会直接传递给 Execute 函数(请记住在不需要区分大小写时首先处理大小写)。通过 Execute 方法设置与已配置的每个参数和选项关联的命令行数据。

请注意,CommandLineAppliction 包含一个 OnExecute(Func<int> invoke) 函数,你可以向其传递一个 Func<int> 委派,分析完成后将自动执行该 Func<int> 委派。在图 2 中,OnExecute 方法获取一个简单的委派来检查是否在调用 Greet 函数之前指定了 greet 命令。

另外请注意,从调用委派返回的 int 可作为从 Main 指定返回值的一种方式。实际上,从调用返回的值将与从 Execute 返回的值相符。再者,由于将分析视作一种相对较慢的操作(我认为是相对来说),Execute 支持获取 Func<Task<int>> 的重载,从而启用命令行分析的异步调用。

指南: 命令、参数和选项

根据所提供的三种命令类型,快速查看适用情形。

通过语义方式标识编译、导入或备份等操作时使用命令。

使用选项为整个程序或特定命令提供配置信息。

建议使用动词作为命令名称,使用形容词或名词作为选项名称(如 -color、-parallel、-projectname)。

无论配置哪种参数类型,都可以考虑以下指南:

仔细查看参数标识符名称的大小写。如果命令行查找不同的大小写,那么用户对于指定 -FullName 还是 -fullname 会非常迷惑。

为命令行分析编写测试。可以借助 Execute 和 OnExecute 等方法相对容易地完成编写。

如果你认为通过名称标识特定参数非常繁琐,或者认为允许多个值但使用选项标识符作为每个值的前缀非常麻烦,则使用参数。

考虑利用 IntelliTect.AssertConsole (itl.tc/Command­LineUtils) 重定向控制台输入和输出,以便注入和捕获控制台使其可供测试。

使用 .NET Core Command­LineUtils 可能有一个缺点:它基于英文语言,未经本地化。在 ShowHelp 中找到的内容(以及通常未本地化的异常消息)等文本均为英文内容。通常这不会成为问题,但由于命令行作为应用程序面向用户的界面上的一部分,可能会出现不接受仅英文版本的情况。为此:

如果本地化非常重要,则考虑编写适用于 ShowHelp 和 ShowHint 的自定义函数。

如果将 CommandLineApplication 配置为不引发异常 (throwOnUnexpectedArg = false),则检查 CommandLineApplication.RemainingArguments。

总结

在过去三年中,.NET Framework 经历了一些重大转变:

  • 现在提供跨平台支持,包括对 iOS、Android 和 Linux 的支持,这真是太棒了!
  • 它从一种机密、专有的方法迁移为开发完全开放(开放源代码)的模块。
  • BCL API 进行了重大重构,将 .NET 标准库打造为高度模块化(跨)平台,适用于广泛的现有应用程序类型,例如服务型软件、移动应用程序、本地应用程序、物联网、桌面应用程序等。
  • 在 Windows 8 的时代,.NET 被忽视,针对 .NET 的策略或记录路线图少之又少,不过继此之后,.NET 得以重生。

综上,如果你尚未开始深入了解全新的 .NET Core 1.0,现在是绝佳时机,你可以获取最长时间跨度来分摊学习过程。换句话说,如果你考虑从早期版本升级到 .NET Core 1.0,立即行动起来吧!机不可失,请尽早升级,越早升级,越可以尽早利用其新功能。


Mark Michaelis 是 IntelliTect 的创始人,担任首席技术架构师和培训师。在近二十年的时间里,他一直是 Microsoft MVP,并且自 2007 年以来一直担任 Microsoft 区域总监。Michaelis 还是多个 Microsoft 软件设计评审团队(包括 C#、Microsoft Azure、SharePoint 和 Visual Studio ALM)的成员。他在开发者会议上发表了演讲,并撰写了大量书籍,包括最新的“必备 C# 6.0(第 5 版)”(itl.tc/EssentialCSharp)。可通过他的 Facebook facebook.com/Mark.Michaelis、博客 IntelliTect.com/Mark、Twitter @markmichaelis 或电子邮件 mark@IntelliTect.com 与他取得联系。

感谢以下 IntelliTect 技术专家对本文的审阅: Phil Spokas 和 Michael Stokesbary