65.9K
CodeProject 正在变化。 阅读更多。
Home

基于枚举的命令行实用程序

2011 年 1 月 9 日

CPOL

19分钟阅读

viewsIcon

110441

downloadIcon

957

枚举类应用示例,系列第三篇文章

EnumerationCommandLine/EnumerationCommandLine.png

目录

1. 引言

本文是我在 CodeProject 上为会员们呈现的一系列关于枚举类型的简短文章中的第三篇。

  1. 枚举类型不枚举!.NET 和语言限制的变通方法;
  2. 人类可读的枚举元数据;
  3. 本文;
  4. 用于 PropertyGrid 和 Visual Studio 的位枚举编辑器.

我将使用相同的代码库,并升级了新功能(版本 3.0.0.0)。如有需要,我还会引用我之前的作品,以便对该主题有充分的理解。

前两篇文章致力于通过基于枚举的泛型类来扩展 .NET 功能,而第三篇文章则关于前面两篇文章中引入的所有功能的实际应用。这是一个非常有趣的应用。

我的命令行实用工具不包含广泛的各种选项和验证方法,不像某些高级实用工具那样。恰恰相反,它仅限于一小组非常实用但简单的选项和命令行参数的格式化风格。

我的实用工具的真正价值在于易用性和健壮性。这将在下一节中解释。

2. 为什么还需要另一个命令行实用工具?

解析和分类命令行有许多解决方案。我都不喜欢。它们都需要大量数据来定义预期的参数。问题在于,这些大部分是字符串数据,需要在两个地方使用:在生成命令行文档时,以及在检查命令行数据时。这会带来支持挑战:如果必须修改命令行描述,它可能会先在功能部分被修改,当参数被检查时。文档很容易被遗忘。

更重要的是,使用字符串数据描述的命令行参数的一致性不允许在编译时进行验证。

不久前,我意识到用于定义命令行功能的数据应该是元数据。枚举类型是我的首选候选对象,因为它们立即为每个成员提供字符串表示,同时保证了每个成员的唯一性,这会在运行时进行验证。

而且,我的解决方案看起来非常轻量级且易于使用,尤其从应用程序开发者的角度来看。要欣赏这一点,请看下一节。

3. 基本用法

我将根据我在我 系列第二篇文章 中使用的两个枚举类型来演示用法,并进行一些更改。

using DescriptionAttribute = SA.Universal.Enumerations.DescriptionAttribute;
using AbbreviationAttribute = SA.Universal.Enumerations.AbbreviationAttribute;

//...

[Description(typeof(EnumerationDeclaration.Descriptions))]
enum StringOption {
    [Abbreviation(1)] InputDirectory,
    InputFileMask,
    [Abbreviation(1)] OutputDirectory,
    [Abbreviation(1)] ForceOutputFormat,
    [Abbreviation(1)] ConfigurationFile,
    [Abbreviation(3)] LogFile,
} //enum StringOption

[Description(typeof(EnumerationDeclaration.Descriptions))]
enum BitsetOption {
    [Abbreviation(1)] Default,
    [Abbreviation(1)] Recursive,
    [Abbreviation(1)] CreateOutputDirectory,
    [Abbreviation(1)] Quite,
} //BitsetOption

区别如下:我们不需要 `DisplayName` 属性;我们也不需要为枚举成员或 `System.Flags` 属性指定整数值——位集功能将由 `CommandLine` 类功能取代。因此,类型 `BitsetOptions` 被重命名为 `BitsetOption`,以遵循微软的命名建议。取而代之的是,我们使用了新引入的 `Abbreviation` 属性,用于以两种形式指定每个或所有命令选项:完整形式和缩写形式。

现在我们可以用两个泛型参数实例化命令行了。

using CommandLine = SA.Universal.Utilities.CommandLine<BitsetOption, StringOption>; 

//...

CommandLine CommandLine = new CommandLine(CommandLineParsingOptions.DefaultMicrosoft);

这是一个命令行示例。

-Log:log.txt /ForceOutputFormat:mp3 -C+ /R input1.wav
input2.wav /q- /q /some_invalid_parameter
-another_invalid:blah-blah

从这个例子可以看出:枚举成员用于指定某种命令行关键字,前面带有斜杠或破折号;一些命令行参数没有前缀字符(并且它们是有效的);关键字可以根据 `Abbreviation` 属性中指定的字符数进行缩写。

这是在运行时访问命令行参数的方法。

string outputDirectory = CommandLine[StringOption.OutputDirectory];

if (CommandLine[BitsetOption.CreateOutputDirectory]
        && string.IsNullOrEmpty(outputDirectory))
    System.IO.Directory.CreateDirectory(outputDirectory);
bool recursive = CommandLine[BitsetOption.Recursive];

string[] files = CommandLine.Files; // will return ["input1.wav", "input2.wav"]

//will return ["/some_invalid_parameter", another_invalid:blah-blah"]:
string[] unrecognized = CommandLine.UnrecognizedOptions;

//will return "/q":
string[] repeatedSwitch = CommandLine.RepeatedSwitches;

4. 命令行架构

下面详细描述命令行使用的通用数据架构。命令行表示为一组参数,由空格或空格与引号(`""`)的组合分隔;我们将选项分为三种形式。

1)   Switch Parameter:
     /|-<keyword>[-|+]
2)   Value Parameter:
     /|-<keyword>:[<value>]
3)   File Parameter: any string not prefixed with keyword indicator (dash or slash)

在这里,方括号、竖线和尖括号是 巴科斯-诺尔范式 的元符号,`<keyword>` 是传递给 `CommandLine` 作为泛型类型参数的枚举类型中的一个枚举成员名称:第一个参数为开关参数形式提供元数据,第二个参数为值参数形式提供元数据。

文件参数不带关键字指示符,可以是任何任意字符串。这种参数之所以称为“文件”,是因为最常见的用途是指定一组通用目的的文件名。例如,编辑器通常会尝试打开该集合中的每个文件;媒体播放器会将文件名放入播放列表,等等。然而,文件可以是任何东西。

关键字可以以完整形式或缩写形式出现。如果应用了 `SA.Universal.Enumerations.Abbrevation` 属性,则可以使用缩写形式。

using AbbreviationLength = System.Byte;

//...
 
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public class AbbreviationAttribute : Attribute {
    public AbbreviationAttribute() {
        this.FAbbreviationLength = 1;
    } //AbbreviationAttribute
    public AbbreviationAttribute(AbbreviationLength length) {
        this.FAbbreviationLength = length;
    } //AbbreviationAttribute
    public AbbreviationLength AbbreviationLength {
        get { return FAbbreviationLength; }
    } //AbbreviationLength
    AbbreviationLength FAbbreviationLength;
} //class AbbreviationAttribute

关键字的完整形式是枚举成员的完整名称;缩写形式使用相同的名称截断到 `AbbreviationLength` 的长度;如果 `AbbreviationLength` 为零、等于或大于完整名称的实际长度,则完整名称和缩写名称是相同的字符串。

整个参数可以包含在引号中。在这种情况下,参数可以包含一个或多个空格字符。这是输入包含空格的字符串值 `<value>` 的一种方式。紧跟在“:”后面的前导空格字符也被解析为 `<value>` 字符串的一部分。

值参数的这种冒号分隔形式大大简化了命令行模型:参数的顺序是任意的。同时,类型 `string[]` 的 `CommandLine` 属性会按它们在命令行中出现的顺序返回字符串。报告未识别选项时也使用相同的自然顺序(参见 5.5)。这种顺序在有两个或多个关键字相同的选项相互矛盾的情况下尤其重要。在这种情况下,选项在命令行中出现的第一个实例(按自然顺序)被认为是有效的,所有其他实例都可以从 `CommandLine` 中访问并报告为重复项。

关键字和文件可以是可选的区分大小写或不区分大小写。此行为由传递给 `CommandLine` 构造函数的 `CommandLineParsingOptions` 参数控制(参见 5.1)。

5. CommandLine 类

所有命令行功能都由一个名为 `CommandLine` 的类提供。

public sealed class CommandLine<SWITCHES, VALUES> {

    public struct CommandLineSwitchStatusWrapper {
        public static implicit operator CommandLineSwitchStatus(
                 CommandLineSwitchStatusWrapper wrapper) {
             return wrapper.FStatus;
        } //operator CommandLineSwitchStatus
        public static implicit operator bool(CommandLineSwitchStatusWrapper wrapper) {
            return wrapper.FStatus ==
                CommandLineSwitchStatus.Plus ||
                wrapper.FStatus == CommandLineSwitchStatus.Present;
        } //operator bool
        internal CommandLineSwitchStatusWrapper(CommandLineSwitchStatus status) {
            this.FStatus = status;
        } //CommandLineSwitchStatusWrapper
        CommandLineSwitchStatus FStatus;
    } //struct CommandLineSwitchStatusWrapper

    // constructors:
    public CommandLine() { Construct(ExtractCommandLine()); }
    public CommandLine(CommandLineParsingOptions options) {
        Construct(ExtractCommandLine(), options);
    } //CommandLine
    public CommandLine(string[] commandLine) { Construct(commandLine); }
    public CommandLine(string[] commandLine, CommandLineParsingOptions options) {
        Construct(commandLine, options);
    } //CommandLine
    
    // accessing valid command line parameters:
    public string this[VALUES key] { get { return Values[key]; } }
    public CommandLineSwitchStatusWrapper this[SWITCHES index] {
        get {
            return new CommandLineSwitchStatusWrapper(Switches[index]);
        }
    } //this
    public string[] Files { get { return FFiles; } }

    // Enumeration instances:
    public Enumeration<SWITCHES> SwitchEnumeration { get { return FSwitchEnumeration; } }
    public Enumeration<VALUES> ValueEnumeration { get { return FValueEnumeration; } }

    // accessing command line errors:
    public string[] UnrecognizedOptions { get { return FUnrecognizedOptions; } }
    public string[] RepeatedFiles { get { return FRepeatedFiles; } }
    public string[] RepeatedSwitches { get { return FRepeatedSwitches; } }
    public string[] RepeatedValues { get { return FRepeatedValues; } }

    public CommandLineParsingOptions CommandLineParsingOptions
        { get { return Options; } }
    public static CommandLineParsingOptions DefaultCommandLineParsingOptions
        { get { return GetDefaultCommandLineParsingOptions(); } }

    #region implementation
    //...
    #endregion implementation

} //class CommandLine

原始源代码包含更详细的 XML 注释,为清晰起见已从文章中删除。类的详细用法将在下面解释。

5.1. 命令行解析选项

这是可选地传递给类构造函数的命令行解析选项声明。

[System.Flags]
public enum CommandLineParsingOptions {
    CaseSensitiveKeys = 1,
    CaseSensitiveAbbreviations = 2,
    CaseSensitiveFiles = 4,
    DefaultMicrosoft =
        CaseSensitiveKeys |
        CaseSensitiveAbbreviations,
    DefaulUnix =
        CaseSensitiveKeys |
        CaseSensitiveAbbreviations |
        CommandLineParsingOptions.CaseSensitiveFiles,
    CaseInsensitive = 0,
} //enum CommandLineParsingOptions

这些选项控制命令行的哪些部分被视为区分大小写,关键字(`CaseSensitiveKeys`)、关键字的缩写形式(`CaseSensitiveAbbreviations`)和文件(`CaseSensitiveFiles`)是分开的;这些选项可以使用按位运算组合。当 `CommandLine` 实例在没有 `CommandLineParsingOptions` 参数的情况下构建时,会根据 `PlatformID` 值(通过 `System.Environment.OSVersion.Platform` 获取)使用适当的默认值。

由于可能需要使用不同于默认值的 `CommandLineParsingOptions` 设置,因此获取初始默认值仍然很重要,因为它取决于操作系统。为此,可以使用 `static` 属性 `DefaultCommandLineParsingOptions`。此属性使应用程序开发者能够获取操作系统依赖的默认值,然后在创建 `CommandLine` 实例之前修改该值。建议修改关键字和/或缩写关键字的大小写敏感性,但不要修改文件。

请注意,使用此默认值对 `Files` 很重要(如果这些命令行选项用于其预期目的,即指定文件或目录名)。只有在选项不重复时(参见 5.5)才会被命令行接受,因此,对于文件,区分大小写或不区分大小写的字符串比较应与平台文件系统的行为匹配。至于关键字或其缩写形式,这是一个便利性问题:区分大小写的方法可以使相同长度的命令行打包更多选项,而不区分大小写对用户更宽松。

5.2. 访问开关选项状态

开关选项(`Switch` Option)可以以三种形式出现在命令行中:末尾带有加号或减号(开启和关闭指示符),或者没有任何这些符号。此外,选项可以被简单地从命令行中省略。

这使得通过 `CommandLineSwitchStatus` 枚举类型描述了三种情况。

[System.Flags]
public enum CommandLineSwitchStatus : byte {
    Absent,
    Present,
    Minus,
    Plus,
} //enum CommandLineSwitchStatus

索引属性 `CommandLine.this[VALUES]` 允许双重使用。

[System.Flags]
CommandLine commandLine = new CommandLine();
bool isRecursive = commandLine[BitsetOption.Recursive];
CommandLineSwitchStatus recursiveStatus =
    commandLine[BitsetOption.Recursive];

此用法的一个布尔变体返回 `True`,如果选项的状态是 `CommandLineSwitchStatus.Present` 或 `CommandLineSwitchStatus.Plus`,并且可以被视为简化的用法,当不需要选项的所有详细状态时;这也是最典型的情况。

这项功能非常有趣:同一个索引属性这种明显双重类型的行为是通过其类型(参见 5)以及其到 `bool` 和 `CommandLineSwitchStatus` 两种类型的隐式转换运算符实现的。我在我名为“Wish You Were Here… Only Once”的作品中,在可能更具挑战性的需求情况下,使用了类似的技巧。

5.3. 访问值选项:值和状态

值选项(Value Option)通过字符串索引属性 `CommandLine.this[VALUES key]` 访问。由于这种形式的选项也可以从命令行中省略,因此应该测试此条件。通常,这是执行此检查的方法。

bool useConfigurationFile =
    string.IsNullOrEmpty(
        commandLine[StringOption.ConfigurationFile]);

如果有人想知道,它应该是 `null` 还是空字符串,答案是:如果选项缺失,相应的属性值将返回 `null`。但是,有一个有效的方法可以通过命令行显式地指定一个值为空字符串的选项。

-ConfigurationFile:

这是值选项的一个有效形式。在某些应用程序中,如果需要考虑这两种情况之间这种“微妙”的差异(不太典型的情况),则可以通过这种方式实现。

string configurationFile =
    commandLine[StringOption.ConfigurationFile]);
bool noConfiguration =
    configurationFile == null;
bool emptyConfiguration =
    configurationFile == string.Empty;
if (!(noConfiguration || emptyConfiguration)) {
    System.IO.StreamReader reader =
        new StreamReader(configurationFile, true);
} //if

//...

5.4. 访问文件选项

文件选项(File options)通过属性 `CommandLine.Files` 或类型 `string[]` 访问。此类型选项的目的和顺序在 4 中解释。

使用示例:

foreach (string file in CommandLine.Files)
    OpenFile(file);

5.5. 报告命令行错误

运行时解析的命令行字符串可能包含各种错误。在发生错误时,不会抛出异常。所有错误都由 `CommandLine` 检测,分为几类,并通过 `string[]` 类型的 `UnrecognizedOptions`、`RepeatedFiles`、`RepeatedSwitches` 和 `RepeatedValues` 属性访问。使用 `CommandLine` 的应用程序负责进一步处理这些错误:它们可以被忽略、报告给用户,或者被视为关键错误,因此应用程序可以停止进一步处理并要求用户使用更好的命令行重新启动应用程序。允许后一种选项,但不推荐:基本上,只有缺失的关键参数才应该导致应用程序停止处理。

首先,参数可能采用不符合为参数设计的任何三种模式的形式(参见 4)。另一种可能性是参数匹配开关参数或值参数的形式,但其关键字与相应枚举类型中的任何枚举成员都不匹配。这里的匹配被认为考虑了 `CommandLine` 构造函数中使用的 `CommandLineParsingOptions` 参数的值(参见 5.1)。

在上述所有情况下,参数都被视为未识别。所有这些参数都按自然顺序(从左到右)收集,并通过 `UnrecognizedOptions` 属性访问。

所有格式正确(已识别)的开关(`switch`)或值参数形式的选项可能会出现一次以上,并且可能在状态或值上相互矛盾。怎么处理这些?选项按自然顺序(从左到右)解析,并且每个选项的左侧第一个实例被分配给通过相应索引属性(`this[]`)可访问的状态或值。所有其他实例都被收集为重复项,分别针对开关(`switch`)和值形式。重复的选项可通过 `RepeatedSwitches` 和 `RepeatedValues` 属性访问。

所有文件形式的参数(没有选项指示符或关键字)都被认为是唯一的,因此只有每个唯一值的左侧第一个实例才被认为是有效的,并被收集以通过 `Files` 属性访问,所有其他实例都被视为重复项,并通过 `RepeatedFiles` 属性访问。不用说,文件名是否唯一是根据 `CommandLineParsingOptions` 参数的值来考虑的,该参数在 `CommandLine` 的构造函数中使用(另请参见 4)。

5.6. 用于迭代选项的枚举实例

`CommandLine` 类公开了泛型类 `Enumeration` 的两个实例:`SwitchEnumeration` 和 `ValueEnumeration`,一个用于开关(`switch`)选项,另一个用于值选项。关于这个泛型类的详细描述以及对枚举类型迭代支持的介绍,请参阅我系列的第一篇文章。自该文章发布以来,泛型类型已升级以携带枚举成员名称的缩写形式(参见 345.8)。迭代的一个应用是生成命令行文档(参见 5.8),其他用途在命令行测试应用程序中有所演示,请参见 6 和源代码。

5.7. 命令行元数据会不一致吗?

是的,当枚举类型规范中创建了名称冲突时,会发生这种情况。首先,两个泛型参数之间永远不会发生冲突,即使在 `CommandLine` 中用作泛型参数的两个不同类型中的相同枚举名称也可以找到。

此代码不会引起任何问题。

using CommandLine =
    SA.Universal.Utilities.CommandLine<CommandLineSwitches, CommandLineStringValues>;

enum CommandLineSwitches { A, B, C, }
enum CommandLineStringValues { A, B, D, E, }

//...

CommandLine CommandLine = new CommandLine();

这是因为在运行时,命令行选项将通过其格式进行识别,因此,例如,在命令行“`/A /A:value`”中,第一个参数将被识别为开关(`switch`)选项,并使用 `CommandLineSwitches` 类型进行解析,第二个参数将使用 `CommandLineStringValues` 类型进行解析;这两个参数都将被视为有效。

同时,缩写可能会在同一枚举类型中造成歧义。

enum StringOption {
    [Abbreviation(5)] InputDirectory,
    [Abbreviation(5)] InputFileMask,
    [Abbreviation(1)] OutputDirectory,
    [Abbreviation(1)] ForceOutputFormat,
    [Abbreviation(1)] ConfigurationFile,
    [Abbreviation(3)] LogFile,
} //enum StringOption

在此示例中,如果命令行指定“`-Input:c:\data\input -Input:*.mp3`”,则无法区分 `InputDirectory` 和 `InputFileMask`。

为了解决这种情况,使用了早期运行时检测。`CommandLine.ValidateEnumeration` 方法执行必要的分析,并使用 `System.Diagnostics.Debug.Assert` 在运行时指示问题;该方法在类构造期间被调用。显然,在编译时无法进行检查。该方法定义及其调用是在 `Debug` 编译符号已定义的情况下,在预编译器指令下编译的,因此此检查不会进入发布版本。如果代码至少在每次指定新的命令行元数据时都在 `Debug` 配置下运行,这已经足够了。

5.8. 命令行文档

有关使用人类可读元数据以及枚举类型文档示例的原则,请参阅我系列中的 第二篇文章

命令行用法可以基于 `Description` 属性自动生成文档;并且此文档可以以全球化的方式进行,也就是说,文档可以本地化到任何区域设置,而无需修改现有代码和使用 卫星程序集 的资源。

`CommandLine` 类公开了我在本系列 第一篇文章 中描述的泛型类 `Enumeration` 的两个实例。该类通过实现 `IEnumerable` 泛型接口提供对枚举功能的[支持]。另请参阅 5.6

泛型类 `EnumerationItem` 已得到扩展,除了先前创建的缓存元数据外,还包括枚举成员的缩写名称(5.6),另请参阅我系列中的 第一篇文章 以获取更多详细信息。这对于生成文档很有用。对于控制台应用程序,文档的呈现可能如下所示:

using SA.Universal.Utilities;
using SA.Universal.Enumerations;

//...

static string GenerateDoubleName<ENUM>(EnumerationItem<ENUM> item) {
    string name = item.Name;
    string abbreviation = item.AbbreviatedName;
    if (name == abbreviation)
        return name;
    else
        return string.Format("{0} (-{1})", name, abbreviation);
} //GenerateDoubleName

static void ShowDocumentation<SWITCHES, VALUES>
	(CommandLine<SWITCHES, VALUES> commandLine) {
    Console.WriteLine("Command line parameters:");
    Console.WriteLine("     /|-<switch>[-|+]");
    Console.WriteLine("     /|-<option>:[<value>]");
    Console.WriteLine();
    Console.WriteLine("SWITCHES:");
    foreach (EnumerationItem<SWITCHES> item in commandLine.SwitchEnumeration)
        Console.WriteLine("      -{0}: {1}", 
	GenerateDoubleName(item), item.Description);
    Console.WriteLine();
    Console.WriteLine("OPTIONS:");
    foreach (EnumerationItem<VALUES> item in commandLine.ValueEnumeration) {
        Console.WriteLine("      -{0}: {1}", 
	GenerateDoubleName(item), item.Description);
 } //ShowDocumentation

此示例的代码是通用的。此示例假定 `SA.Universal.Enumerations.DescriptionAttribute` 属性已应用于枚举类型,如 3 所示。此属性反过来假定已创建适当的资源。我系列中的 第二篇文章 解释了创建此类资源和使用 `DescriptionAttribute` 所需的所有步骤。此技术使用了 全球化 的文档方法:之后可以通过添加 卫星程序集本地化 到所需的区域设置,而无需修改已创建的程序集。

对于非控制台应用程序,可以通过使用 `System.Text.StringBuilder` 来创建类似的文档。首先应创建一个 `StringBuilder` 实例;并将 `Console.WriteLine` 的调用替换为 `string.Format` 的调用;获取的字符串应(带适当的换行符)追加到此 `StringBuilder` 实例。整个文档字符串可以通过 `StringBuilder.ToString()` 获取。

当然,这个示例是简化的。在真正构建良好的应用程序中,所有直接字符串常量都应该放入资源中。

6. CommandLine 测试

`CommandLine` 泛型类已经或多或少地得到了彻底测试(更多多于少),所以本节中描述的内容对于理解该类如何使用或其实现方式无关紧要。

值得注意的是,创建测试工具以实现更简单、更快速的测试、测试本身和调试,与 `CommandLine` 类本身的相对简单的实现相比,似乎付出了巨大的努力。这项活动确实揭示了一些错误并帮助快速修复了它们。测试困难的原因是:测试许多不同的本机命令行场景需要为每次新的测试用例修改重新启动测试应用程序。这将花费太多时间。

为了克服这个麻烦,我决定从文本编辑框中输入的原始形式模拟命令行行为。文本框的初始内容是从应用程序启动时提供的真实命令行填充的;但这两种任务似乎并非微不足道。问题在于 .NET 如何将原始命令行分割成字符串数组的命令行参数,考虑到使用引号字符允许参数中包含空格,这并不简单。测试应用程序也模拟了这种行为。

实用工具 `static` 类 `SA.Universal.Utilities.CommandLineSimulationUtility` 有助于实现命令行模拟。它只暴露两个成员:`UnparsedCommandLine` 属性,它返回一个 `string` 原始命令行,以及 `SimulateCommandLineParsingIntoArray` 方法,它以 .NET 在将命令行参数传递给应用程序入口点(“`main`”)时进行令牌化的方式返回 `string` 数组,同时考虑了引号字符。`UnparsedCommandLine` 的一个小困难是它应该删除代表应用程序入口程序集的 `main` 模块的第一个参数。这对于不同的应用程序托管方式有不同的处理方式;Visual Studio 主机和独立运行之间的区别是一个众所周知的例子。

本文提供的源代码中,实用工具类包含在主库 `Enumerations.dll` 中。需要注意的是,此实用工具类不需要用于应用程序开发,并且可以删除。将此实用工具类包含在库中的唯一目的是代码重用以及更紧凑地打包源代码,并附带命令行测试应用程序:`CommandLineTestWPF.exe` 和 `CommandLineTest.exe`。该实用工具类被重用,因为我提供了 Command Class 实用工具的两个版本:WPF 版本和 `System.Windows.Forms` 版本。历史上,WPF 版本是第一个创建的;大部分测试和错误修复都是使用 WPF 版本完成的。`System.Windows.Forms` 版本是为了支持我的 .NET Framework v. 2.0 和 Mono 支持主张(参见 7)而专门为本文创建的。

命令行测试应用程序的这两个版本都显示了 `CommandLine`、`Enumeration` 和相关 `Attribute` 类的用法——请参阅源代码。

7. 代码构建与兼容性

代码提供给 Microsoft .NET Framework 2.0 到 4.0 版本,并在 Mono 上使用 Ubuntu Linux v. 8.04 进行测试。可以使用 Visual Studio 2005、2008 或 2010 进行构建,或使用批处理构建来构建任何命名的 Framework 版本,而无需 Visual Studio。有关更多详细信息,请参阅本系列 第一篇文章构建部分

有关更多详细信息,请参阅本 文章.NETMono 兼容性部分。

唯一不兼容上述所有平台的项目是命令行测试应用程序 `CommandLineTestWPF.exe` 的 WPF 版本。自然,它针对 Microsoft .NET 版本 3.5 及更高版本(我未在 v. 3.0 上进行测试),并且在编写本文时,它不支持 Mono。可以改用基于 `System.Windows.Forms` 的应用程序 `CommandLineTest.exe`(参见 6)。

8. 替代方案

我不能说我全面搜索了所有已创建的有意义的替代方案。几乎所有作品都依赖于一些硬编码的 `string` 数据;所以我认为这些方法不会促进或帮助创建易于支持的代码。

据我所知,我只喜欢一个作品:“Powerful and simple command line parsing in C#”,作者是 Peter Palotas,日期是 2007 年 8 月 15 日。该工具非常强大,超出了我所有的预期,尽管它的使用可能不像我的那样简单。

这种方法的强大之处在于真正使用元数据来指定命令行规则。但是,我不知道这种元数据是否可以用全球化的方式定义,并且是否可以本地化。命令行解析由应用程序开发者创建的某个 `Option` 类驱动;有丰富的属性集用于将命令行选项解释为该 `Option` 的属性,以及每个成员的验证规则。这个验证规则集是另一个强大的特性。此外,它还支持丰富多样的命令行样式。

尽管如此,我可以看到我方法中的某些好处,并且可以看出缺乏参数验证的一些原因。首先,无法基于本地属性规则(即应用于每个单独属性的规则,而不是组合规则)执行完整的验证。无法设计一套通用的验证规则,适用于所有可想象的应用程序。这就是为什么我对验证规则的设想是应该有一个完全位于应用程序中的单独层。我的方法仅提供较低级别的功能,但它做得非常彻底。此外,更轻量级的解决方案和更简单、有些限制但更清晰的命令行模型的推广都具有显著的价值。

尽管我上面说了,Peter 的作品具有巨大的价值。对于需要复杂且高级命令行功能、并且符合验证规则集的复杂应用程序,该实用工具可能是必不可少的——我强烈推荐它。

9. 结论

所提议的命令行处理方法提供了功能、紧凑性、健壮性和易用性近乎最优的组合。

© . All rights reserved.