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





5.00/5 (4投票s)
介绍一个 C# 部分 Program 类,为您的 CLI 项目添加核心功能,让您更快上手。
引言
更新:更好的默认值处理
更新 2:如果您使用 TextReader
或 TextWriter
作为参数类型(或在集合/数组中使用),现在可以加载或发出文件作为参数。示例已更新以显示其工作原理。现在,所有 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
类中标记了 CmdArgAttribute
的 static
字段定义。为使用屏幕提供尽可能多的信息是一个好主意。
说到这个,您可能还想指定程序集标题、版权、版本和描述等内容,这些内容都用于使用屏幕。
我现在要给您一些代码。
定义您的 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
属性可以获取可执行文件的 Filename
、CodeBase
(通常是可执行文件的完整路径)、友好的 Name
、Description
、Copyright
和 Version
。它还有 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()
函数可以接受 text
、width
、indent
和 startOffset
(用于第一行)。
text
是要换行的文本。width
是要换行的宽度,以字符为单位。indent
是用于缩进第一行之后的每一行的空格数。最后,startOffset
指示打印开始的第一行的起始位置。如果 width
为零,它会尝试近似控制台宽度。
陈旧文件检查
实用工具通常接受输入文件并生成输出文件。有时,如果实用工具处理时间很长,则可能希望在不需要重新生成输出时跳过处理。IsStale()
接受一个或多个输入文件和一个输出文件,或者接受一个或多个 TextReader
和一个 TextWriter
。它也可以接受 FileInfo
对象。如果输出文件不存在,或者输出文件比输入文件旧,它将返回 true。
自动文件管理
如果您使用 TextReader
或 TextWriter
作为参数类型(或它们的数组/集合),读取器将在退出或错误时自动打开和关闭。写入器将在首次写入时创建或打开文件,并在退出或错误时根据需要关闭。IsStale()
可用于比较输入和输出。参数解析器无法确定流应该是读取还是写入,因此如果您想进行二进制处理,很可能只想获取 FileInfo
参数并自行处理文件。
历史
- 2024 年 1 月 24 日 - 初始提交
- 2024 年 1 月 25 日 - 改进了默认值处理,以支持数组和列表
- 2024 年 1 月 25 日 - 支持
TextReader
和TextWriter
参数及列表 - 2024 年 1 月 25 日 - 错误修复和 wrap 应用程序示例
- 2024 年 1 月 25 日 - 错误修复和 DNF 支持,以及移除可空性警告
- 2024 年 1 月 26 日 - 重写。添加了更好的使用屏幕和参数选项。修复了文本换行
- 2024 年 1 月 29 日 - 添加了更多功能
- 2024 年 2 月 15 日 - 解析代码中的错误修复