目录

介绍

概念化这个混乱

处理命令行参数

异常处理

用户界面

过期文件处理

编码此混乱

MSBuild支持


用于在C#中构建命令行工具应用程序的入门代码。该样板代码为应用程序提供异常处理和命令行参数解析。

  • 下载源4.8 KB

介绍

我编写了许多命令行实用程序——通常是代码生成器等。这些工具遵循命令行解析、使用报告和异常处理的基本总体模式。由于各个工具的基本结构相似,因此我从一些样板代码开始,然后对其进行修改以创建它们。我将在这里分享和解释该代码。多年来,在尝试了许多不同的技巧来构建这些工具之后,我决定了一个运行良好且从此代码开始的基本过程。

概念化这个混乱

我们在这里需要解决几个问题。这些问题实际上对于任何命令行工具应用程序都是普遍的:我们需要提供用户界面、处理命令行参数和报告错误。

处理命令行参数

使用我们的技术,开关采用以下形式 /<switch> {<arguments>}

使用列出的代码,在进行任何切换之前,它还需要可变数量的未切换参数。可以在代码中更改。

处理命令行参数几乎是可以生成或概括的。事实是,可以,但是增加的复杂性成本通常是不值得的。即使我们有一种概括参数处理的方法,它仍然认为添加、删除或更改参数将需要更改应用程序本身的逻辑,因此您并没有真正删除太多内容(如果有的话)。在大多数情况下,这是我的经验。另一个问题是,虽然很容易归纳80%的论点案例,但其他20%并非无关紧要。它不如编写专门的参数处理代码合理。

我们要做的是在Main()中获取C#提供给我们的部分处理过的string[] args数组,并对其进行简单循环,驱动一个switch/case来处理我们的标志。好处是,它易于维护和修改,并且可以处理引用的文件名等。缺点是它是基本的,并且也会奇怪地接受诸如"/switch"这样的东西,并在switch的周围加上引号。没关系,这不是很理想,但也不应造成任何伤害。当您在进行任何切换之前接受任意数量的未命名参数时,会有一点问题。我们在提供的样板代码中对此进行处理。

与此相关的是,经验告诉我,尽管启用了管道,但接受来自STDIN的输入并不是一个好主意,因为控制台会“处理”输入,这会破坏输入,从而导致难以跟踪错误。同时,可能希望将数据发送给STDOUT以显示或打印目的,或者直接发送给文件,以获取“未经处理的”副本。出于这个原因,我的工具要求您至少指定一个输入文件(如果它们完全接受输入),但是它们不需要指定输出文件。这只是您从经验中学到的东西之一。我的一些早期项目是为管道设计的,它可能会产生问题,尤其是对于Unicode流。

还要注意,因为我们不需要输出文件,所以我们将任何消息发送到Console.Error,而不是Console/Console.Out。这是因为如果要将输出发送到STDOUT,则需要带外信息到STDERR中。这对于干净的命令行界面很重要。

异常处理

这个想法是让我们的可执行文件报告任何错误,然后在成功时返回0作为退出代码,在失败时返回其他值。我们通过将整个混乱包装在一个try/catch/finally块中,然后使用该catch部分返回从抛出的异常中区分出来的错误代码来处理此问题。这里的一个小问题是,在调试时我们不需要这种全局异常处理,因此,如果我们使用DEBUG编译时间常数进行编译,我们将修改该catch块,并仅引发异常。这极大地简化了应用程序的调试。

用户界面

用户界面是我们内置的“帮助”功能。它提供了有关应用程序、版本、命令行用法以及开关含义的信息的基本描述。每当发生错误时,我们都会报告该错误,这仅仅是因为我们假设输入参数一定存在问题。如果不希望的话,您可以更改此行为。我们收集很多的从组件信息中指定属性AssemblyInfo.cs

过期文件处理

之所以包含了它,是因为我在几乎所有的命令行生成器工具中都使用了它。这对于可能需要很长时间才能使用的工具(例如DFA词法分析器和解析器生成器)至关重要,但是对于许多项目而言,我认为该功能的用例远比针对它的用例要多。如果您的工具生成输出文件并且可能需要花费大量时间来执行,则提供仅在输出早于输入时才重新运行的功能可能会有所帮助。这样一来,该工具仅在输入文件已更改的情况下才能执行工作。我们允许通过/ifstale开关。如果您的工具不生成文件,则没有必要,但这是在此处提供的,因为工具通常会生成文件。请注意,我们还会检查可执行文件本身是否比输出更新。这使得在相关项目中更容易用作预构建步骤,同时还可以进行工具本身的开发。基本上,如果可执行文件已更改,它将重新运行生成过程。

编码此混乱

这几乎是完整的样板代码。我唯一省略的是周围的名称空间和using声明。否则,我们将从上至下覆盖代码:

class Program
{static readonly string _CodeBase = Assembly.GetEntryAssembly().GetModules()[0].FullyQualifiedName;static readonly string _File = Path.GetFileName(_CodeBase);static readonly Version _Version = Assembly.GetEntryAssembly().GetName().Version;static readonly string _Name = _GetName();static readonly string _Description = _GetDescription();static int Main(string[] args){int result=0; // the exit code// command line argsList<string> inputFiles = new List<string>(args.Length);string outputFile = null;bool ifStale = false;// holds the output writerTextWriter output = null;try{// no args prints the usage screenif (0 == args.Length){_PrintUsage();result = -1;}else if (args[0].StartsWith("/")){throw new ArgumentException("Missing input files.");}else{int start = 0;// process the command line args:// process input file args. keep going until we find a switchfor (start = 0; start < args.Length; ++start){var a = args[start];if (a.StartsWith("/"))break;inputFiles.Add(a);}// process the switchesfor (var i = start; i < args.Length; ++i){switch (args[i].ToLowerInvariant()){case "/output":if (args.Length - 1 == i) // check if we're at the endthrow new ArgumentException(string.Format("The parameter \"{0}\" is missing an argument", args[i].Substring(1)));++i; // advance outputFile = args[i];break;case "/ifstale":ifStale = true;break;default:throw new ArgumentException(string.Format("Unknown switch {0}", args[i]));}}// now that the switches are parsed// would be a good time to validate them// now let's check if our output is stalevar stale = true;if (ifStale && null != outputFile){stale = false;foreach (var f in inputFiles){if (_IsStale(f, outputFile) || _IsStale(_CodeBase, outputFile)){stale = true;break;}}}if (!stale){Console.Error.WriteLine("{0} skipped generation of {1} because it was not stale.", _Name, outputFile);}else{// DO WORK HERE!// TextWriter output will be cleaned up automatically on exit, // so set it to your output source when ready to generate.// It's a good idea not to open the output until everything // else has been done so that errors in the input will not// cause an existing file to be overwritten.}}}
#if !DEBUG// error reporting (Release only)catch (Exception ex){result = _ReportError(ex);}
#endiffinally{// clean upif (null != outputFile && null != output){output.Close();output = null;}}return result;}static string _GetName(){foreach (var attr in Assembly.GetEntryAssembly().CustomAttributes){if (typeof(AssemblyTitleAttribute) == attr.AttributeType){return attr.ConstructorArguments[0].Value as string;}}return Path.GetFileNameWithoutExtension(_File);}static string _GetDescription(){foreach (var attr in Assembly.GetEntryAssembly().CustomAttributes){if (typeof(AssemblyDescriptionAttribute) == attr.AttributeType){return attr.ConstructorArguments[0].Value as string;}}return "";}
#if !DEBUG// do our error handling here (release builds)static int _ReportError(Exception ex){_PrintUsage();Console.Error.WriteLine("Error: {0}", ex.Message);return -1;}
#endifstatic bool _IsStale(string inputfile, string outputfile){var result = true;// File.Exists doesn't always work righttry{if (File.GetLastWriteTimeUtc(outputfile) >= File.GetLastWriteTimeUtc(inputfile))result = false;}catch { }return result;}static void _PrintUsage(){var t = Console.Error;// write the name of our app. this actually uses the // name of the executable so it will always be correct// even if the executable file was renamed.t.WriteLine("{0} Version {1}", _Name,_Version);t.WriteLine(_Description);t.WriteLine();t.Write(_File);t.WriteLine(" <inputfile1> { <inputfileN> } [/output <outputfile>] [/ifstale]");t.WriteLine();t.WriteLine("   <inputfile>     An input file to use.");t.WriteLine("   <outputfile>    The output file to use - default stdout.");t.WriteLine("   <ifstale>       Do not generate unless <outputfile> is older than <inputfile>.");t.WriteLine();t.WriteLine("Any other switch displays this screen and exits.");t.WriteLine();}
}

您会注意到的第一件事是几个static readonly字段。这些内容包含有关我们的可执行文件的基本信息,主要用于用户界面,但是它不太可能会在您的代码的其他地方使用,因此在此处提供它们以方便访问。

在那之后,有Main()例程。请注意,我们在此处返回一个int。这样我们就可以根据需要对返回值进行尽可能多的控制,这对于在批处理文件或构建步骤中使用此工具至关重要。但是,在大多数情况下,我们将像往常一样通过抛出异常来处理错误,并让样板逻辑将其转换为退出代码。

下一个兴趣点是命令行arg变量列表。我喜欢为每个参数提出一个。每当我们添加或删除命令行参数时,都应在此处声明或删除其对应的变量。这使得将它们全部声明为一处变得更加清晰。每当我修改这些代码时,我要做的下一件事就是相应地修改_PrintUsage()例程,这样我就不会忘记。

现在我们有了output参数。应该将其设置为Console.Out,如果/output未指定,或通过StreamWriter指定outputFile或诸如此类。程序退出时,它将自动关闭。重要的是仅在可能的最后时刻进行设置,这样,如果在此之前发生任何错误,就不会擦除输出文件的先前内容。显然,如果您没有输出文件,则应删除所有此相应代码。

现在,我们可能需要为以后需要关闭的所有资源保留变量。例如,如果您访问数据库,则可能要挂起一个连接,然后再关闭它。如果是这样,请在此处为其声明一个变量并将其设置为null。稍后填充。在finally块中,我们将在下面将其关闭。

最后,我们从try/catch/finally代码块的开头开始,围绕着大多数代码。在这里,我们开始做真正的工作。

之后,我们进行一些预参数验证,从打印使用情况界面开始,如果未指定任何参数,则退出。

接下来,我们循环直到找到一个前导/指示开关。直到那时出现的每个参数都会在inputFiles列表中结束。如果您的应用程序不使用多个输入文件,则需要修改此代码,以仅将第一个参数读入inputFile变量,而不是循环并读入inputFiles。显然,如果您根本不使用输入文件,则应删除所有关联的代码。

现在我们进行开关处理。基本上,我们旋转一个循环,并且在每次迭代中,我们都会看到所处的开关。如果开关接受参数,则需要检查以确保我们不在最后一个参数上,然后,我们需要在存储结果之后再增加一次i,如下所示:

case "/output":if (args.Length - 1 == i) // check if we're at the endthrow new ArgumentException(string.Format("The parameter \"{0}\" is missing an argument", args[i].Substring(1)));++i; // advance outputFile = args[i];break;

在这里,因为/output需要一个参数,所以我们检查以确保我们不在最后,如果存在则抛出。否则,我们将i加1,然后设置适当的命令arg变量。可以将其复制并粘贴以制作新的接受单个参数的开关,如下所示:

case "/name":if (args.Length - 1 == i) // check if we're at the endthrow new ArgumentException(string.Format("The parameter \"{0}\" is missing an argument", args[i].Substring(1)));++i; // advance name = args[i];break;

我用粗体突出显示了这两个更改,以说明/name采用单个参数的开关。该代码已被复制和粘贴。

布尔开关也是如此:

case "/ifstale":ifStale = true;break;

需要进行与上述相同的两个基本更改以添加更多内容。

如果您需要创建一个带有可变数量参数的开关,,您可以在新的开关下创建代码,它的工作方式非常类似于inputFiles收集代码,不同之处在于它将使用i而不是将start用作其工作变量。

default case抛出,因为这表示一个无法识别的开关。

如果还不清楚的话,最主要的想法是switch/case设置先前声明的命令行变量。

有时,您会在其他命令行参数旁边指定非法的命令行参数。例如,您可能有一个无法使用/optimize选项指定的/debug选项。切换循环完成后,您将需要对命令行变量进行任何后期验证,以处理这些情况,并根据需要抛出异常。这里没有代码,因为样板代码中没有这样的场景。

现在我们继续/ifstale功能。和以前一样,除非输入比输出新,或者可执行文件本身比输出新,否则它将跳过输出的生成。处理此问题的代码位于上面的post-validation之后的部分。您可能需要更改的唯一一件事是,如果仅使用单个输入文件,您必须删除陈旧检查代码块中的循环,并使其在inputFile上工作,而不是在inputFiles上工作。

在所有这些之后,我们在这里,在else块中进行注释,这是我们工作的地方。此处的步骤是收集数据,处理数据,然后最后打开output流并生成输出。您可以在此处委派一个例程来完成工作,这可能是个好主意,但我不想混淆流程。这里委派的唯一问题是您可能需要传递很多变量——即您已声明的大多数命令行参数变量。在实践中,我确定是否以及如何执行此过程在很大程度上取决于应用程序,但是在实践中,我发现只需在此处完成很多工作即可,这些工作本身会委托给其他事情,例如代码生成器类,这会更容易。

在随后的finally代码块中,您将要释放output之外的所有资源,例如是否从先前的假设中声明了数据库连接。记住要检查是否为空。

除了_PrintUsage()之外,您不需要在您的应用程序中进行任何修改,因为所有这些都是收集程序集属性并比较文件日期的支持代码。请注意,当我们比较文件时,我们不依赖File.Exists(),因为它不适合UNC网络路径。

MSBuild支持

让你的应用程序MSBuild“友好”地与Visual Studio这样的东西进行沟通,当运行作为一个预构建步骤时,这涉及到按照MSBuild喜欢的方式来组织你的控制台消息。您必须修改错误报告,并且还必须小心如何构造状态消息,但这超出了本文的范围。即使您的工具没有执行此操作,它仍然可以在Visual Studio中使用。它只是没有多余的装饰,例如获取错误和带有行号的警告详细信息,以显示在构建错误列表中。

C#中的样板命令行工具应用程序相关推荐

  1. GitHub日收12000星,微软新命令行工具引爆程序员圈!

    作者 | 伍杏玲 出品 | CSDN(ID:CSDNnews) 继上次微软开源计算器刷爆GitHub后,今天凌晨在微软Build大会登场的Windows Terminal,如旋风般目前已登顶GitHu ...

  2. GitHub 日收 12,000 星,微软新命令行工具引爆程序员圈!

    作者 | 伍杏玲 出品 | CSDN(ID:CSDNnews) 继上次微软开源计算器刷爆GitHub后,今天凌晨在微软Build大会登场的Windows Terminal,如旋风般目前已登顶GitHu ...

  3. php 开启命令模式,如何启用PhpStorm中的命令行工具

    本篇文章主要给大家介绍如何使用phpstorm中的命令行工具. PhpStorm下载地址: PhpStorm使用命令行工具,我们可以直接从IDE调用命令!在我们使用任何命令行工具之前,我们必须在设置中 ...

  4. 在vc6控制台程序中如何调用运行ImageMagick命令行工具

    在http://www.imagemagick.org/script/index.php网站下载相应的执行文件,这里以下载ImageMagick-6.6.5-10-Q16-windows-static ...

  5. Nmcli 网络管理命令行工具基础

    介绍 在本教程中,我们会在CentOS / RHEL 7中讨论网络管理命令行工具NetworkManager command line tool,也叫nmcli.那些使用ifconfig的用户应该在C ...

  6. 图解修改Windows启动菜单命令行工具BCDEdit

    一 BCDEdit命令操作实例 二 BCDEdit命令介绍 Bcdedit简介 在NT60系列操作系统(Windows 7/Vista/2008)中的一个命令行工具,用于建立和重新配置bootload ...

  7. 使用 Apache Commons CLI 开发命令行工具

    http://www.ibm.com/developerworks/cn/java/j-lo-commonscli/index.html 使用 Apache Commons CLI 开发命令行工具 杨 ...

  8. WinCE中命令行工具Viewbin简介(查看nk.bin中包含的文件)

    Viewbin是微软提供的一个命令行工具,在WinCE5.0和WinCE6.0中,可以在"/WINCE600/PUBLIC/COMMON/OAK/BIN/I386"找到他.View ...

  9. Cmder命令行工具在Windows系统中的配置

    一.Cmder简介 Cmder:一款用于Windows系统中,可增强传统cmd命令行工具的控制台模拟器(类似于Linux系统中的终端控制窗口) 特点: 无需安装,解压即用 可使用较多Linux命令,如 ...

最新文章

  1. VMware记录(一)- vCenter Server 服务安装提示无法解析此完全限定域名
  2. 电动力学每日一题 2021/10/11
  3. Cpp 对象模型探索 / 外部调用私有的虚函数的方法
  4. SQLServer中连接个数及超时问题
  5. 处理字符串中的单个字符CharAt()
  6. 详解Ubuntu Server下启动/停止/重启MySQL数据库的三种方式(ubuntu 16.04)
  7. 【HDU - 1281 】棋盘游戏 (经典的二分图匹配,匈牙利算法,枚举删除顶点,必须边,关建边)
  8. java中单例模式用法详解
  9. 零起点英语_【零起点英语】第100讲:The Cost of War 战争的代价
  10. Halcon——点胶机胶水路径应用(2)
  11. luogu P2678 跳石头 二分answer
  12. jsp中的session和上下文
  13. QQ帐户的申请与登陆 (25 分)(map映射)
  14. 利用NXlog采集Windows系统日志
  15. 电子邮件工作原理及主要协议
  16. OpenGL日常-GLAD
  17. 2021年N2观光车和观光列车司机考试内容及N2观光车和观光列车司机模拟考试题库
  18. UvaLive 4670 Dominating Patterns
  19. win10截图软件工具
  20. KCNScrew for Mac(mac序列号工具)

热门文章

  1. 云原生的本质_云原生2.0的逻辑之辩,如何让每个企业都成为新云原生企业
  2. 家族关系查询系统程序设计算法思路_七大查找算法(附C语言代码实现)
  3. 喜庆红色主题新年春节晚会年会背景素材(PSD分层格式)
  4. 什么?你的电商网页不够时尚?看这里
  5. 装扮圣诞海报气氛,你需要这些小元素!
  6. 搜索图片的干货网站?
  7. 中式国风地产创意海报素材模板
  8. go 调用其他文件函数_一篇文章让你了解Go语言中方法Methods的使用内幕
  9. memcpy 作用(C++)
  10. Linux开机启动过程(2):内核启动的第一步