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

Program.Base:为您的项目添加即插即用的命令行应用程序功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2024年1月24日

MIT

8分钟阅读

viewsIcon

8270

downloadIcon

138

介绍一个 C# 部分 Program 类,为您的 CLI 项目添加核心功能,让您更快上手。

引言

更新:更好的默认值处理

更新 2:如果您使用 TextReaderTextWriter 作为参数类型(或在集合/数组中使用),现在可以加载或发出文件作为参数。示例已更新以显示其工作原理。现在,所有 IDisposable 参数实例的可处置实例在 Run() 完成或出错后都会被调用 Dispose()

更新 3:修复了错误,并添加了一个“wrap”示例应用程序,该程序可以将文本文件按指定宽度换行。

更新 4:一些 bug 修复。已为 .NET Framework 分叉,并移除了可空性问题。

更新 5:已重写和清理。现在支持开头的序数参数,而不仅仅是一个。因此,文章已基本重写。

更新 6:添加了更多环境解析,添加了 FileSystemInfo/FileInfo/DirectoryInfo 支持。

更新 7:我在为最近的一个项目使用它时发现了一些解析错误。我很懊恼。这些错误已修复,这是一个重要的更新。

我编写了大量的 C# 命令行工具 - 经常是构建工具。有时,提供大多数命令行工具共有的基本功能(如参数解析和用法屏幕)所花费的时间和精力比实用工具本身的功能还要多。

再也不会了。我宁愿只包含一些代码,让一切都准备就绪,触手可及。为此,我最终决定编写我的“圣杯” CLI 样板代码,以处理我一遍又一遍必须做的所有事情,并且我将其提供给社区。

Using the Code

这不是一个微不足道的代码实现,但它的设计易于使用。它广泛使用 .NET 反射来获取程序集的信息,包括确定要解析哪些参数以及如何生成使用屏幕。整个过程都经过精心设计,尽可能自动化。

首先,将 Program.Base.cs 放入您的项目。由于其工作方式,它不能是库。您需要实现默认/空命名空间下的 Program 类,或者编辑 Program.Base.cs

基本上,它会劫持您的入口点,让您在 Program partial class 中实现一个 void Run() 方法。当它被调用时,一切都已被切片、切块并完美烹饪。您的命令行已被解析,程序集信息已加载,并且您拥有有用的实用方法。

您的命令行由 Program 类中标记了 CmdArgAttributestatic 字段定义。为使用屏幕提供尽可能多的信息是一个好主意。

说到这个,您可能还想指定程序集标题、版权、版本和描述等内容,这些内容都用于使用屏幕。

我现在要给您一些代码。

定义您的 Program 类和替代入口点

您会希望将 Program 类设置为 partial。我倾向于也将其设置为 static

static partial class Program { 
    void Run() {
    }
}

这将创建一个程序,除了可能用于显示使用屏幕的 /? 之外,不接受任何参数。如果您确实传递了 /?,结果将如下所示

wrap v1.0

   word wraps input

Usage: wrap

  /? Displays this screen and exits

以上假设您填写了程序集描述和版本。

定义您的参数

让我们为应用程序添加一些参数

[CmdArg(Ordinal=0, Required =true,Description ="The input files")]
public static TextReader[] Inputs = new TextReader[] { Console.In };
[CmdArg("output",Description = "The output file. Defaults to <stdout>")]
public static TextWriter Output = Console.Out;
[CmdArg("id", Description = "The guid id", ItemName = "guid")]
public static Guid Id = Guid.Empty;
[CmdArg("ips", Description = "The ip addesses", ItemName = "address")]
public static List<IPAddress> Ips = new List<IPAddress>() { IPAddress.Any };
[CmdArg("ifstale", Description = "Only regenerate if input has changed")]
public static bool IfStale = false;
[CmdArg("width", Description = "The width to wrap to", ItemName = "chars")]
public static int Width = Console.WindowWidth/2;
[CmdArg("enum", Description = "The binding flags", ItemName = "flag")]
public static List<BindingFlags> Enum = null;
[CmdArg("indices", Description = "The indices", ItemName = "index")]

在这里,我们定义了许多参数。第一个是 Inputs/#0 参数(请参阅 [CmdArg] 元数据)。

这是一个序数参数,通过将 Ordinal 设置为非负值来表示。序数参数必须放在参数列表的开头,并且没有与之关联的 / 开关。

总之,这个特定的参数接受一个 TextReader[] 数组,这表明它是一系列文件。它很容易就是某种 TextReader 集合 - 这取决于您。由于它是一个 TextReader 对象列表,因此 ItemName 默认为“infile”,这正是我们想要的,因为它在使用屏幕中引用列表中每个项目的名称。通常最好指定它以提高清晰度,但我们在这里不需要它。我们还指出了它是 Required 的,因此解析器将强制要求一个或多个这些条目。请注意,我们没有指定 ItemName,但它解析为 infile。这可能是因为元素类型是 TextReader。当然,这可以通过显式设置 ItemName 来覆盖。

第二个是一个简单的字符串 /output <outfile>,它只接受一个参数。与上面类似,我们没有将“outfile”指定为 ItemName。这是通过使用 TextWriter 作为元素类型来推断的。

Id/id 有点意思。它接受一个 Guid。这之所以有效,是因为 Guid 有一个关联的 TypeConverter。这意味着 .NET 有一个内置的、众所周知的从字符串转换它的机制。解析器在内部使用它。

Ips/ips 更有趣,因为它包含一个 IPAddress 实例列表。其中每一个都使用其 Parse() 方法进行解析,因为它没有 TypeConverter

由于 IfStale/ifstale 是一个 bool,它只有一个开关 /ifstale 而没有参数。如果指定,此字段将设置为 true。

Count/count 接受单个整数。

Enum/enum 接受标志。请注意我们如何使用列表而不是进行按位或运算。事实上,解析器本身并不支持 enum,但通过 TypeConverter 的魔力支持它,所以它依赖于它。在这种情况下,适当的模式是获取一个标志列表,然后稍后合并它们。

Indices/indices - 最后一个参数 - 只是一个整数列表

未指定的任何可选参数都可以为其关联的字段或属性分配默认值。如果指定了值,则会替换默认值。这也适用于列表和数组。

如果我们不指定任何参数并在 Release* 中运行它,我们会得到以下使用屏幕。

Example v1.0

   An example of using Program.Base to handle core CLI functionality

Usage: Example {<infile1> [<infileN>]} [/enum {<flag1> <flagN>}] [/id <guid>] [/ifstale] [/indices {<index1> <indexN>}]
    [/ips {<address1> <addressN>}] [/output <outfile>] [/width <chars>]

  <infile>  The output files
  <flag>    The binding flags
  <guid>    The guid id
  <ifstale> Only regenerate if input has changed
  <index>   The indices
  <address> The ip addesses
  <outfile> The output file - defaults to <stdout>
  <chars>   The width to wrap to
- or -
  /help    Displays this screen and exits

Error: Missing required argument <infile>

您可以看到,基于您提供的信息,它为您做了很多工作。它还可以处理基本的验证等,但如果您需要执行进一步的逻辑来验证参数,则应在调用 PrintUsage() 后抛出异常以指示验证失败。

*在 Debug 构建中,应用程序会在出错时抛出异常。这是为了让您可以调试出现的任何异常。在 Release 版本中,错误会弹出使用屏幕和错误描述。

访问 Info 属性可以获取可执行文件的 FilenameCodeBase(通常是可执行文件的完整路径)、友好的 NameDescriptionCopyrightVersion。它还有 CommandLinePrefix,它会提供传递的参数之前的文字命令行。其中一些用于使用屏幕。这是通过反射您的程序集信息获得的。

进度报告

有两种方法可以向控制台报告进度。一种是当您没有明确的结束点,不知道需要多长时间时使用。另一种则提供带有百分比的进度条。使用它们很简单。

Console.Write("Progress test: ");
for (int i = 0; i < 10; ++i)
{
    WriteProgress(i, i > 0, Console.Out);
    Thread.Sleep(100);
}
Console.WriteLine();
Console.Write("Progress bar test: ");
for (int i = 0; i <= 100; ++i)
{
    WriteProgressBar(i, i > 0, Console.Out);
    Thread.Sleep(10);
}
Console.WriteLine();

进行一些动画后,您将看到最终屏幕

Progress test: \
Progress bar test: [■■■■■■■■■■] 100%

第一次调用每种方法时,必须将 false 作为第二个参数传递。之后,传递 true

请注意,这些需要退格符,而某些控制台仿真设施(如 Visual Studio 的输出窗口)不支持退格符。

文本换行

能够将文本换行到控制台宽度* 通常是可取的。

WordWrap() 函数可以接受 textwidthindentstartOffset(用于第一行)。

text 是要换行的文本。width 是要换行的宽度,以字符为单位。indent 是用于缩进第一行之后的每一行的空格数。最后,startOffset 指示打印开始的第一行的起始位置。如果 width 为零,它会尝试近似控制台宽度。

陈旧文件检查

实用工具通常接受输入文件并生成输出文件。有时,如果实用工具处理时间很长,则可能希望在不需要重新生成输出时跳过处理。IsStale() 接受一个或多个输入文件和一个输出文件,或者接受一个或多个 TextReader 和一个 TextWriter。它也可以接受 FileInfo 对象。如果输出文件不存在,或者输出文件比输入文件旧,它将返回 true。

自动文件管理

如果您使用 TextReaderTextWriter 作为参数类型(或它们的数组/集合),读取器将在退出或错误时自动打开和关闭。写入器将在首次写入时创建或打开文件,并在退出或错误时根据需要关闭。IsStale() 可用于比较输入和输出。参数解析器无法确定流应该是读取还是写入,因此如果您想进行二进制处理,很可能只想获取 FileInfo 参数并自行处理文件。

历史

  • 2024 年 1 月 24 日 - 初始提交
  • 2024 年 1 月 25 日 - 改进了默认值处理,以支持数组和列表
  • 2024 年 1 月 25 日 - 支持 TextReaderTextWriter 参数及列表
  • 2024 年 1 月 25 日 - 错误修复和 wrap 应用程序示例
  • 2024 年 1 月 25 日 - 错误修复和 DNF 支持,以及移除可空性警告
  • 2024 年 1 月 26 日 - 重写。添加了更好的使用屏幕和参数选项。修复了文本换行
  • 2024 年 1 月 29 日 - 添加了更多功能
  • 2024 年 2 月 15 日 - 解析代码中的错误修复
© . All rights reserved.