控制台程序入口点框架






3.22/5 (5投票s)
快速、轻松地编写更好的控制台应用程序
谁应该阅读本文?
任何编写控制台应用程序的人 - 即使只写一次!
必备组件
必须具备 C#、方法、参数和特性的基本理解。虽然您不需要理解构成框架的代码 - 但您需要理解如何使用它。如果您还不理解这些,那么我将非常好奇您为什么会阅读这篇文章!
引言
在我多年的开发生涯中,我编写了许多实用工具 - 用来加速重复性任务的辅助工具。我编写这些工具的模式通常是编写一个程序集来封装工具逻辑,再编写另一个程序集来执行逻辑 - 通常是一个控制台应用程序。这样,我就可以从命令行使用我的工具,或者直接从编写该工具的产品内部使用,而无需弹出到另一个进程。
然而,很快就会有人厌倦编写控制台应用程序的重复步骤 - 解析命令行参数、开关、类型转换和信息输出……正如您可以想象的那样,非常乏味。更不用说,在设计和输出这些控制台应用程序时保持一致本身就是一项任务 - 很多时候会导致约定杂乱无章。因此,借用一句老话,是时候抽象抽象的过程了。事实证明这相当简单,由此诞生了一个我非常想与您分享的高度灵活的框架。
本文详述的实用程序围绕我个人对控制台应用程序的要求而设计。命令行调用的性质本质上是自由形式的,但通过观察我编写的众多工具的典型实现,出现了一种通用的使用约定。正是基于这种约定,我设计了这个框架 - 它可能不适合所有应用程序 - 但如果您在构思实用程序时考虑到这种约定,您可能会发现其好处远远大于它施加的任何限制。
所有控制台应用程序的共性是
- 应用程序有一个或多个“操作”,每个操作都有自己的一套自定义设置(参数),并且每个操作都是应用程序的一个互斥的入口点。
- 应用程序必须将字符串参数解析为 CLR 类型,以便在程序逻辑中使用。
- 在开始任何领域特定的逻辑之前,应用程序的输入验证必须生成有意义的错误消息,当使用无效参数时。
- 应用程序必须生成用法文档,通常是在程序调用时没有参数,参数无效,或使用了 ‘/?’ 开关时。
- 应用程序必须支持参数的默认状态,仅当所需值与默认值不同时才要求指定它们。
通常,单个控制台应用程序每次调用执行单个操作;此操作通过传递各种参数来配置。如果有人将此“程序模式”应用于其实用程序,则命令行约定与众所周知的“方法 + 参数”程序结构完全类比。
控制台应用程序的操作等同于程序入口点(方法),参数当然就是方法参数。通过使用自定义属性,可以标记程序结构,并使用运行时工具来执行这些方法,所有必需的参数都将从命令行参数中解析并进行强类型转换。
该框架可以从程序模式推断出所有必需的用法文档和错误消息。这促进了所有控制台程序之间标准化和一致的输出格式 - 因为它是由框架生成的,而不是每次都重写。当在大型开发团队中使用时,其价值不可否认,考虑到要确保所有开发人员都严格遵守构建命令行用法和诊断输出的约定有多么困难。
在编写一行程序逻辑之前,您的控制台应用程序将已经实现了专业的、一致的“用户界面”。
下图显示了 .NET Framework 的 ‘gacutil’ 的输出

下一个图显示了我们测试工具类似的输出 - 所有这些都是由框架自动生成的。

程序集元数据用于提取版权声明和来自标记属性的用法信息。
开始
标记属性位于 Adlib.Console.Markup
命名空间中,每个属性在 Localized
子命名空间中都有一个本地化等价物。下面的代码示例演示了本地化版本的用法 - 主要区别在于 Description
属性-属性指定的是资源名称,而不是实际的描述值。然后使用此名称从 ConsoleProgram
属性指定的资源类型中查找适当的字符串资源。
Program
类用 ‘ConsoleProgram
’ 属性标记,该属性提供了定义默认动词的机会,基本上是如果未指定命令行参数,则将调用的方法。
每个需要从命令行访问的方法都用Verb属性装饰,其中指定了 ‘switch
’ 及其描述。
[ConsoleProgram(typeof(Resources), DefaultVerb = "/?")]
class Program
{
static void Main(string[] args)
{
typeof(Program).Execute(args);
System.Console.ReadLine();
}
[Verb("/l", Description = "ListDescription")]
public static void List(
[Argument("/assembly_name", Description="ListAssemblyName")]
string assemblyName)
{
System.Console.WriteLine(string.Format
("Called the 'List' method with 'assembly_name' = {0}", assemblyName));
}
[Verb("/?", Description = "HelpDescription")]
public static void Help()
{
typeof(Program).PrintHelp();
}
扩展方法提供对“运行时”的访问。只需在 Program
类型上调用 ‘Execute’ 并传递参数数组,框架就会负责查找要调用的正确方法,并将所有参数从字符串解析为正确的参数类型。
默认值通过在 Argument
属性中指定它们来实现。以下示例对此进行了演示
[Verb("/i", Description = "InstallDescription")]
public static void Install(
[Argument("/assembly_path")]string assemblyPath,
[Argument("/f", Default=false)]bool force)
{
System.Console.WriteLine(
string.Format("Install: 'assembly_path' = {0},
'force' = {1}.", assemblyPath, force));
}
历史
- 2009.09.19:首次发布