使用 CommandLineApplication 类创建专业的控制台程序

闲话

在很久很久以前,电脑是命令行/终端/控制台的天下,那屏幕上的光标在行云流水般的键盘敲击下欢快地飞跃着,那一行行的字符输出唰唰唰地滚动着……直到 Windows 95 的出现(那时候我还不知道苹果电脑和它的操作系统),我的鼠标终于不再召灰,开始有了用武之地,然后就是 GUI 的天下……

然而世事就是这样,锦绣繁华之后就开始返璞归真,大鱼大肉太多就向往点粗茶淡饭,开车开久了就怀念起自行车,GUI 充斥的 Windows 的世界里似乎也开始挂起一阵控制台的清风。毕竟,一旦你熟悉了各种命令和参数,敲键盘的速度还是胜过鼠标的,只是现在的人都太懒或者太忙,总是宁愿牺牲效率而不愿意去多记一点东西。

我个人还是很喜欢命令行的,尤其是远程访问一个系统的时候,一个简单的 ssh 命令直接登录到远程 Linux,一个简单的 scp 命令就可以互相传输文件,这种便利、快捷是 Windows 远程桌面所无法比拟的。GUI 也许是 Windows 的设计哲学, 做什么事情都要靠 GUI。 没错,这大大降低了各种操作门槛,但是作为一个程序员,GUI 工具并非总是最佳选择,但除了 GUI 工具,替代选择并不多——直到 .NET Core 的出现。

专业的控制台程序

首先我们要有个标准,怎样才算“专业的控制台程序”?

平常无论是写着玩还是工作需要,我都做过一些控制台程序,在启动参数的传入、解析和执行上都比较随意,类似 MyProgram abc 123 这样,MyProgram 是程序名,abc123 是参数值,内部直接用 args[0]args[1]取得参数值并使用。仅此而已。时间长了,自己都搞不懂每个参数什么意思,参数有哪些有效值,都得查源代码才知道,每个参数的顺序也很重要,颠倒不得。而内部实现上,至少是 Main 方法里是典型的面条式代码。由此可见,我的这些控制台程序,无论是外在,还是内在,都业余得很。

那一个专业的控制台程序应该是什么样的呢?

完善的帮助信息

当你面对一个陌生的命令行程序,或者重新面对你自己2个月之前写的命令行程序,你心里第一反应会是什么呢?让我猜猜,你的第一反应一定是“这货到底怎么用?”(不要告诉我我猜错了,我不相信💢),下一个想法就是这个程序能告诉我用法就好了。一个专业的控制台程序必然会满足你的这个需要——它可以提供完善的帮助信息。比如 git:

git

有了这些帮助信息,我们自然信心倍增,心里有谱多了。

我希望我写的控制台程序也能做到这点!

符合“国际惯例”的调用方式

如果你留意一下 Linux 平台下的一众控制台程序,你会发现他们的参数组织和调用方式十分类似,这种约定俗成的“国际惯例”十分有助于降低熟悉各个控制台程序的学习成本。

我们还是以 git 为例,从上面的截图可以看出,它有非常多的参数可用。虽然都是参数,但根据作用不同,可以分为 command, argument, option 三类。我不是控制台程序达人,对这3类参数的区别与联系还在深入理解、学习中。目前的理解是(以 git 为例):

  • command

    一个复杂的控制台程序可以提供多个子命令,而 command 就代表这些子命令

    比如 cloneinit

  • argument

    是一个 command 需要的参数。

    比如执行 clone 的时候需要指定一个 repository 的地址,这个地址就是一个 argument

  • option

    调整 command 的行为。

    比如对于 clone 可以增加 --verbose 参数使其输出更详尽的信息。

    通常以两个短横线开头后跟参数名,比如 --verbose,对于常用的 option 还会有简写形式,就是一个短横线后跟简写形式的参数名,比如 --verbose 支持简写形式 -v,在帮助里通常以 --verbose, -v 或者 --verbose|-v 的形式说明。

    option 可以有值也可以没有值,有值的时候,其赋值方式不一而足,常见的有用空格的 --branch dev,用等号的(注意等号两边没有空格) --branch=dev,用冒号的(冒号两边没有空格) --branch:dev

对于相对简单的控制台程序,可能只有 argument 和 option 而并不包括 command。

看看那些有名的控制台程序,基本上也都遵循这个套路。在这方面,我不想做个怪胎,所以,我希望我写的控制台程序也能遵照这些“国际惯例”!

易于维护的内部实现

在控制台程序的内部实现上,以前的做法非常简单粗暴,用一堆 if 或者 switch 配合各种 &&|| 成功地做到了一开始只有“上帝”和我明白,1个月后只有“上帝”明白的效果。随着程序参数增多、逻辑越来越复杂,这么搞下去,“上帝”依然可以很潇洒,我会被搞死的。为了不让我变成秃头,为了我可以有更多的时间玩游戏,控制台程序的内部实现必须井井有条、易于维护!

问题来了

了解了一个专业的控制台程序应具备的素质,那么接下来有一个问题萦绕在我心头久久不肯散去……

HOW

作为一个小白,要实现一个有详细说明信息、调用方式符合“国际惯例”、还能优雅地处理各种参数的控制台程序何其困难?而如此套路化的东西难道没有一套成型的东西供参考吗?

人生为何如此艰难

CommandLineApplication

天无绝人之路,一次偶然的邂逅,遇到了它—— CommandLineApplication。如果你用 .NET Core 的话,它可以在你构建专业控制台程序的路上助你一臂之力。

它全名是 Microsoft.Extensions.CommandLineUtils.CommandLineApplication,家住 GitHub 省 aspnet 市 Common 区 src 路 Microsoft.Extensions.CommandLineUtils 大院 CommandLine 室.如果你路盲,这里有个传送门

实践是检验真理的唯一标准 - MathForKids 程序

让我们从头开始,利用 dotnet cli 和 Visual Studio Code 亲自体验一下它到底有多强大。我们将创建一个 MathForKids 程序,它可以根据参数输出一些加减乘除的算式,让孩子算算结果。

MathForKids 程序的功能

  • 它只输出算式,所以我们不要搞得太复杂,不需要什么子命令 command。

    想了解带 command 的用法,可以参考本文最后附上的 CommandLineApplication 的官方测试代码 😛

  • 它在执行的时候需要指定输出的算式是加、减、乘、除还是这4种运算符的组合,因此我们可以设置一个 argument: operator.

    这个 argument 可以允许同时设置多个值。

  • 它可以设置生成的数字的最大值和最小值,因为它们只是调整输出的算式种数字的大小,因此我将其归为 options: minValue, maxValue

    minValue 的默认值是0

    maxValue 的默认值是100

  • 它可以设置生成的算式个数,这也只是调整输出结果,因此我也将其归为 option: count

    count 的默认值是 10

MathForKids 程序的用法

在我们看代码之前,先看看这个程序用起来应该是什么样的。

注意:在本文发布之时,.NET Core 处于 RC 2 阶段,还不支持编译为本地可执行文件。所以目前必须使用 dotnet MathForKids.dll 来运行。图中红色下划线表示输入的命令

首先,它可以提供帮助信息:

MathForKids 帮助信息

从帮助信息中我们可以看到它支持的所有参数,并且支持参数的全写和简写,比如我们可以写 --minValue 也可以简写为 -min

当我们调用它只生成加法和乘法,其它选项默认时:

MathForKids 使用默认 option 调用

注意,我们一次传入了多个参数: 加 乘

当我们设置一些 option 来改变输出结果时:

MathForKids 传入多个 options

注意,这里演示了使用全写和简写添加 option 以及以多种方式赋值(使用冒号、等号和空格)。

以上使用方法看起来是不是有点“专业”的味道了?

Show me the code

为了使用 CommandLineApplication 类,我们需要添加对 Microsoft.Extensions.CommandLineUtils 的引用:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.0-rc2-3002702"
    },
    "Microsoft.Extensions.CommandLineUtils": "1.0.0-rc2-final" //<--- add this dependency
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}

这是 MathForKids 程序的主体部分,说明都在注释里:

using System;
using Microsoft.Extensions.CommandLineUtils;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CommandLineApplication app = new CommandLineApplication();
            app.HelpOption("--help|-h|-?"); // 使其支持显示帮助信息
            app.VersionOption("--version|-v", "1.0.0"); // 使其支持显示版本信息。为了简化起见,直接返回静态的 1.0.0

            // 添加 argument,这里我们允许传入这个 argument 的多个值。
            CommandArgument argOperator = app.Argument("operator", "算式类型,有效值:加、减、乘、除,可以设置多个类型", multipleValues: true);

            // 添加多个 options,注意设置全写和简写的方式,很简单。这应该是基于约定的解析处理方式。
            CommandOption optMin = app.Option("--minValue -min <value>", "最小值,默认为0", CommandOptionType.SingleValue);
            CommandOption optMax = app.Option("--maxValue -max <value>", "最大值,默认为100", CommandOptionType.SingleValue);
            CommandOption optCount = app.Option("--count -c <value>", "生成的算式数量,默认为10", CommandOptionType.SingleValue);

            // 传入一个委托方法,当下面的 Execute 执行后会执行我们的委托方法,完成我们需要处理的工作。 委托方法需要返回一个 int,反映执行结果,一如经典的控制台程序需要的那样。
            app.OnExecute(() =>
            {
                return OnAppExecute(argOperator, optMin, optMax, optCount);
            });

            // 开始执行,把控制台传入的参数直接传递给 CommandLineApplication。
            app.Execute(args);
        }

        private static int OnAppExecute(CommandArgument argOperator, CommandOption optMin, CommandOption optMax, CommandOption optCount)
        {
            // 此处省略 100 行以以下示例取代
            List<string> operators = argOperator.Values;

            string max;
            if(optMax.HasValue())
                max = optMax.Value();

            return 0;
        }
    }
}

CommandArgumentCommandOption 的使用非常简单,有 HasValueValue 等方法可以判断和取值。

所有代码放在我的 GitHub 里。

官方的测试类也是一个很好的参考资源。

posted on 2016-06-21 19:00  零度的火  阅读(2511)  评论(6编辑  收藏  举报

导航