前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 System.CommandLine 分析命令行

使用 System.CommandLine 分析命令行

作者头像
Edison.Ma
发布2019-08-20 11:42:45
1.1K0
发布2019-08-20 11:42:45
举报

一直回到 .NET Framework 1.0,我一直很震惊的是,开发人员没有什么简单的方法可用来分析应用程序的命令行。应用程序从 Main 方法开始执行,但参数是以数组 (string[] args) 形式传入,并不区分数组中的哪些项是命令、选项和参数等。

我在之前的一篇文章“如何参与 Microsoft 开放源代码软件项目”(msdn.com/magazine/mt830359) 中写过这个问题,并介绍了我与 Microsoft 的 Jon Sequeira 的合作成果。Sequeira 领导一支开放源代码开发人员团队来新建命令行分析程序,此分析程序可以接受命令行参数,并使用名为 System.CommandLine 的 API 分析它们,具体执行以下三个操作:

  • 允许配置命令行。
  • 允许将命令行泛型参数(令牌)分析成不同的构造,其中命令行上的每个单词都是令牌。(从技术上讲,命令行主机允许使用引号将单词组合成一个令牌。)
  • 调用配置为根据命令行值决定是否执行的功能。

支持的构造包括命令、选项、参数、指令、分隔符和别名。下面介绍了每种构造:

命令:这些是应用程序命令行支持的操作。例如,假设为 git。一些适用于 git 的内置命令是 branch、add、status、commit 和 push。从技术上讲,在可执行文件名后面指定的命令实际上是子命令。根命令(可执行文件本身的文件名,例如,git.exe)的子命令可能有自己的子命令。例如,在命令“dotnet add package”中,根命令为“dotnet”,子命令为“add”,要添加的子命令为“package”(可能称为子子命令?)。

选项:使用它们,可以修改命令行为。例如,dotnet build 命令包含 --no-restore 选项,可以指定此选项来禁止隐式运行 restore 命令(而是依赖先前执行的 restore 命令)。顾名思义,选项通常不是命令的必需元素。

参数:命令和选项都可以有关联值。例如,dotnet new 命令包含模板名称。指定新命令时,必须提供此值。同样,选项也可以有关联值。再以 dotnet new 为例,--name 选项有用于指定项目名称的参数。与命令或选项关联的值称为“参数”。

指令:这些是跨所有应用程序的整合命令。例如,redirect 命令可以强制所有输出(stderr 和 stdout)保存为 .xml 格式。因为指令是 System.CommandLine 框架的一部分,所以它们自动包含在内,无需命令行接口开发人员执行任何操作。

分隔符:参数与命令或选项的关联是通过分隔符完成的。常见分隔符包括空格、冒号和等于号。例如,指定 dotnet build 的详细信息时,可使用以下三种变体中的任何一种:--verbosity=diagnostic、--verbosity diagnostic 或 --verbosity:diagnostic。

别名:这些是可用于标识命令和选项的附加名称。例如,使用 dotnet,“classlib”是“类库”的别名,-v 是“--verbosity”的别名。

在 System.CommandLine 推出前,缺少内置分析支持意味着,当应用程序启动时,你作为开发人员不得不分析参数数组,以确定哪个参数对应于哪个参数类型,再将所有值正确地关联在一起。虽然 .NET 在解决这个问题上做出了大量尝试,但没有一个成为默认解决方案,也没有一个能够很好地扩展为同时支持简单和复杂方案。有鉴于此,System.CommandLine 是以 alpha 形式(请访问 github.com/dotnet/command-line-api)开发和发布的。

确保不将简单的事情复杂化

假设正在编写图像转换程序,它根据指定的输出名称将图像文件转换为不同的格式。命令行可能如下所示:

imageconv --input sunrise.CR2 --output sunrise.JPG

考虑到此命令行(有关替代命令行语法,请参阅“将参数传递到 .NET Core 可执行文件”),imageconv 程序开始进入 Main 入口点 static void Main(string[] args),其中有包含四个相应参数的字符串数组。遗憾的是,--input 和 sunrise.CR2 之间或 --output 和 sunrise.JPG 之间没有关联。也并未指明 --input 和 --output 标识选项。

幸运的是,新增的 System.CommandLine API 以前所未有的方式大大改进了这个简单方案。简化的地方是,可使用与命令行匹配的签名对 Main 入口点进行编程。换言之,Main 的签名变成:

static void Main(string input, string output)

没错,System.CommandLine 允许将 --input 和 --output 选项自动转换为 Main 上的参数,甚至不需要编写标准 Main(string[] args) 入口点。唯一的附加要求是,引用启用此方案的程序集。有关要引用内容的详细信息,可以访问 itl.tc/syscmddf,因为只要程序集在 NuGet 上发布,其中的所有说明可能会很快过时。(不,无需更改语言,即可支持这一点。确切地说,在添加引用时,项目文件被修改为包含生成标准 Main 方法的生成任务,此方法的主体使用反射来调用“自定义”入口点。)

此外,参数并不仅限于字符串。例如,有许多内置转换器(以及自定义转换器支持)允许对输入和输出的参数类型使用 System.IO.FileInfo,如下所示:

static void Main(FileInfo input, FileInfo output)

如本文中的“System.CommandLine 体系结构”部分所述,System.CommandLine 分为核心模块和应用程序提供程序模块。从 Main 配置命令行是应用程序模型实现,而现在我直接将整个 API 集称为 System.CommandLine。

目前,命令行参数和 Main 方法参数之间的映射是基本的,但对于许多程序来说,仍然相对可行。假设 imageconv 命令行稍微复杂一点,以展示一些附加功能。图 1 展示了命令行帮助。

图 1:示例 imageconv 命令行

imageconv:
  Converts an image file from one format to another.
Usage:
  imageconv [options]
Options:
  --input          The path to the image file that is to be converted.
  --output         The target name of the output after conversion.
  --x-crop-size    The X dimension size to crop the picture.
                   The default is 0 indicating no cropping is required.
  --y-crop-size    The Y dimension size to crop the picture.
                   The default is 0 indicating no cropping is required.
  --version        Display version information

启用此更新后命令行的相应 Main 方法如图 2 所示。尽管此示例只不过是完整记录的 Main 方法,但其中自动启用了很多功能。接下来,探索一下使用 System.CommandLine 时的内置功能。

图 2:支持更新后的 imageconv 命令行的 Main 方法

/// <summary>/// Converts an image file from one format to another./// </summary>/// <param name="input">The path to the image file that is to be
    converted.</param>/// <param name="output">The name of the output from the conversion.
    </param>/// <param name="xCropSize">The x dimension size to crop the picture.
    The default is 0 indicating no cropping is required.</param>/// <param name="yCropSize">The x dimension size to crop the picture.
    The default is 0 indicating no cropping is required.</param>
public static void Main(  FileInfo input, FileInfo output,   int xCropSize = 0, int yCropSize = 0)

第一个功能是命令行的帮助输出,它是从 Main 上的 XML 注释推断而来。这些注释不仅便于对程序进行一般性描述(在摘要 XML 注释中指定),还便于使用参数 XML 注释来记录每个参数。必须启用 doc 输出,才能利用 XML 注释,而这已在引用通过 Main 启用配置的程序集时自动为你配置。内置的帮助输出包含以下三个命令行选项中的任何一个:-h、-? 或 --help。例如,图 1 中的帮助是由 System.CommandLine 自动生成。

同样,虽然 Main 上没有 version 参数,但 System.CommandLine 会自动生成 --version 选项,用于输出可执行文件的程序集版本。

另一个功能是命令行语法验证,它检测是否缺少必需参数(没有指定默认值的参数)。如果你没有指定必需参数,System.CommandLine 会自动发出错误消息“选项 --output 缺少必需参数”。虽然有些违反常理,但默认情况下必须有带参数的选项。不过,如果不需要与选项关联的参数值,可使用 C# 默认参数值语法。例如,

int xCropSize = 0

此外,还内置支持分析选项,无论选项在命令行中的顺序如何。值得注意的是,默认情况下,选项和参数之间的分隔符可能是空格、冒号或等于号。最后,将 Main 的参数名称的驼峰式大小写转换为 Posix 样式参数名称(即,命令行中的 xCropSize 转换为 --x-crop-size)。

如果你键入无法识别的选项或命令名称,System.CommandLine 会自动返回命令行错误消息“无法识别命令或参数…”。不过,如果指定的名称与现有选项类似,那么错误消息就会提示建议更正拼写错误。

有一些内置指令可用于所有使用 System.CommandLine 的命令行应用程序。这些指令用方括号括起来,紧跟在应用程序名称后面。例如,[debug] 指令触发断点,可便于你附加调试程序,而 [parse] 则预览如何分析令牌,如下所示:

imageconv [parse] --input sunrise.CR2 --output sunrise.JPG

此外,还支持通过 IConsole 接口和 TestConsole 类实现进行自动测试。若要将 TestConsole 注入命令行管道,请将 IConsole 参数添加到 Main 中,如下所示:

public static void Main(
  FileInfo input, FileInfo output,
  int xCropSize = 0, int yCropSize = 0,
    IConsole console = null)

若要利用 console 参数,请将对 System.Console 的调用替换为 IConsole 参数。请注意,直接通过命令行(而不是单元测试)调用时,IConsole 参数会进行自动设置,所以即使参数的默认赋值为 NULL,它也不得有 NULL 值,除非你编写以这种方式调用它的测试代码。或者,考虑将 IConsole 参数放在首位。

我最喜欢的功能之一是,支持 Tab 自动完成,最终用户可以通过运行激活命令来选择启用此功能(请访问 bit.ly/2sSRsQq)。这是可选择启用的方案,因为用户往往会防护对 shell 的隐式更改。选项和命令名称的 Tab 自动完成是自动进行的,而参数的 Tab 自动完成则是通过建议执行的。配置命令或选项时,Tab 自动完成值可以来自静态值列表(如 q、m、n、d),也可以来自 --verbosity 的诊断值。也可以在运行时动态提供这些值,如通过在参数是 NuGet 引用时返回可用 NuGet 包列表的 REST 调用。

将 Main 方法用作命令行规范只是可使用 System.CommandLine 进行编程的几种方法之一。体系结构非常灵活,可通过其他方式来定义和使用命令行。

System.CommandLine 体系结构

System.CommandLine 体系结构重心是核心程序集,其中包括用于配置命令行的 API,以及将命令行参数分析成数据结构的分析程序。除了为 Main 启用不同的方法签名外,上一部分中列出的所有功能都可以通过核心程序集启用。不过,命令行配置支持(特别是使用类似于 Main 的方法等域特定语言)是由应用程序模型启用。(用于前面所述类似于 Main 的方法实现的应用程序模型的代号为“DragonFruit”。)不过,System.CommandLine 体系结构支持其他应用程序模型(如图 3 所示)。

图 3:System.CommandLine 体系结构

例如,可以编写应用程序模型,以使用 C# 类模型来定义应用程序的命令行语法。在此类模型中,属性名可能对应于选项名称,属性类型可能对应于要将参数转换为的数据类型。例如,模型可能还会利用属性来定义别名。也可以编写模型来分析 docopt 文件(请访问 docopt.org)的配置。其中每个应用程序模型都会调用 System.CommandLine 配置 API。当然,开发人员可能首选直接从应用程序(而不是通过应用程序模型)调用 System.CommandLine,这种方法也受支持。

将参数传递到 .NET Core 可执行文件

指定结合使用命令行参数和 dotnet run 命令时,完整的命令行如下所示:

dotnet run --project imageconv.csproj -- --input sunrise.CR2
  --output sunrise.JPG

不过,若要从 csproj 文件所在的同一目录运行 dotnet,命令行如下所示:

dotnet run -- --input sunrise.CR2 --output sunrise.JPG

dotnet run 命令使用“--”作为标识符,指明应将其他所有参数都传递到可执行文件以供分析。

自 .NET Core 2.2 起,还支持独立式应用程序(甚至是在 Linux 上)。使用独立式应用程序,可以在不使用 dotnet run 的情况下启动它,而只需依赖生成的可执行文件,如下所示:

imageconv.exe --input sunrise.CR2 --output sunrise.JPG

显然,这是 Windows 用户预期的行为。

允许复杂化

我在前面提到过,确保不将简单的事情复杂化是基本功能。这是因为,通过 Main 方法启用命令行分析仍缺少部分人可能认为重要的一些功能。例如,无法配置(子)命令或选项别名。如果遇到这些限制,可以生成自己的应用程序模型,也可以直接调用 Core(System.CommandLine 程序集)。

System.CommandLine 包含表示命令行构造的类。这包括 Command(和 RootCommand)、Option 以及 Argument。图 4 提供了一些示例代码,用于直接调用 System.CommandLine,并将它配置为完成图 1 内帮助文本中定义的基本功能。

图 4:直接使用 System.CommandLine

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
...
public static async Task<int> Main(params string[] args)
{
  RootCommand rootCommand = new RootCommand(
    description: "Converts an image file from one format to another."
    , treatUnmatchedTokensAsErrors: true);
  Option inputOption = new Option(
    aliases: new string[] { "--input", "-i" }
    , description: "The path to the image file that is to be converted."
    , argument: new Argument<FileInfo>());
  rootCommand.AddOption(inputOption);
  Option outputOption = new Option(
    aliases: new string[] { "--output", "-o" }
    , description: "The target name of the output file after conversion."
    , argument: new Argument<FileInfo>());
  rootCommand.AddOption(outputOption);
  Option xCropSizeOption = new Option(
    aliases: new string[] { "--x-crop-size", "-x" }
    , description: "The x dimension size to crop the picture. 
      The default is 0 indicating no cropping is required."
    , argument: new Argument<FileInfo>());
  rootCommand.AddOption(xCropSizeOption);
  Option yCropSizeOption = new Option(
    aliases: new string[] { "--y-crop-size", "-y" }
    , description: "The Y dimension size to crop the picture. 
      The default is 0 indicating no cropping is required."
    , argument: new Argument<FileInfo>());
  rootCommand.AddOption(yCropSizeOption);
  rootCommand.Handler =
    CommandHandler.Create<FileInfo, FileInfo, int, int>(Convert);
  return await rootCommand.InvokeAsync(args);
}
static public void Convert(
  FileInfo input, FileInfo output, int xCropSize = 0, int yCropSize = 0)
{
  // Convert...
}

在此示例中,每个构造都是显式实例化,而不是依赖 Main 应用程序模型来定义命令行配置。唯一功能差异在于,添加每个选项的别名。不过,与使用类似于 Main 的方法相比,直接利用 Core API 可以提供更多控制。

例如,可以定义子命令,如 image-­enhance 命令,它包含与 enhance 操作相关的一组选项和参数。复杂的命令行程序有多个子命令,甚至还有子子命令。例如,dotnet 命令有 dotnet sln add 命令,其中 dotnet 是根命令,sln 是众多子命令之一,add(或 list 和 remove)是 sln 的子命令。

对 InvokeAsync 的最终调用会自动隐式设置许多功能,包括:

  • 分析和调试指令。
  • 配置 help 和 version 选项。
  • Tab 自动完成和拼写错误更正。

如果需要更细致的控制,还有针对每个功能的单独扩展方法。Core API 还公开了其他许多配置功能。这些工作包括:

  • 处理配置显示不匹配的令牌。
  • 启用 Tab 自动完成的建议处理程序,它根据当前命令行字符串和游标位置返回可能值列表。
  • 不希望使用 Tab 自动完成或帮助发现的隐藏命令。

此外,虽然用于控制使用 System.CommandLine 进行命令行分析的方法有很多,但也提供了方法优先这种方法。实际上,这是内部使用的方法,用来绑定到类似于 Main 的方法。使用方法优先这种方法,可以在图 4 的底部使用 Convert 等方法来配置分析程序(如图 5 所示)。

图 5:使用方法优先这种方法配置 System.CommandLine

public static async Task<int> Main(params string[] args)
{
  RootCommand rootCommand = new RootCommand(
    description: "Converts an image file from one format to another."
    , treatUnmatchedTokensAsErrors: true);
  MethodInfo method = typeof(Program).GetMethod(nameof(Convert));
  rootCommand.ConfigureFromMethod(method);
  rootCommand.Children["--input"].AddAlias("-i");
  rootCommand.Children["--output"].AddAlias("-o");
  return await rootCommand.InvokeAsync(args);
}

在这种情况下,请注意,Convert 方法用于初始配置,然后你将根命令的对象模型导航为添加别名。子可索引属性包含所有附加到根命令的选项和命令。

总结

我非常着迷于 System.CommandLine 提供的功能。只需很少的代码,即可实现本文探讨的简单方案,这一点非常棒。此外,实现的功能数(包括 Tab 自动完成、参数转换和自动测试支持等,只是举个例子)意味着,可以在所有 dotnet 应用程序中轻松获取功能完备的命令行支持。

最后,System.CommandLine 是开放源代码的。也就是说,如果缺少需要的功能,可以开发增强功能,并将它作为拉取请求提交回社区。我个人希望添加的一些功能是,不用总在命令行上指定选项或命令名称,而是可以依赖参数位置来暗指名称是什么。此外,如果在使用类似于 Main 的方法或方法优先这种方法时,可以声明方式添加其他别名(如短别名),那就很棒。


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

衷心感谢以下 Microsoft 技术专家对本文的审阅:Kevin Bost、Kathleen Dollard 和 Jon Sequeira

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet技术平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 确保不将简单的事情复杂化
  • System.CommandLine 体系结构
  • 将参数传递到 .NET Core 可执行文件
  • 允许复杂化
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档