张赐荣——一位视障程序员。
赐荣小站: www.prc.cx

張賜榮

张赐荣的技术博客

博客园 首页 新随笔 联系 订阅 管理

C# 程序解析命令行启动参数

问题

您需要应用程序以标准格式接受一个或多个命令行参数。兵并且您需要访问和解析传递给应用程序的完整命令行。

解决方法

结合使用以下类来帮您解析命令行参数:Argument 、ArgumentDefinition 和 ArgumentSemanticAnalyzer 。
完整代码

public static void TestParser(string[] argumentStrings)
{
//Important point: why am I immediately converting the parsed arguments to an array?
//Because query results are CALCULATED LAZILY and RECALCULATED ON DEMAND.
//If we just did the transformation without forcing it to an array, then EVERY SINGLE TIME
//we iterated the collection it would reparse.  Remember, the query logic does not know that
//the argumentStrings collection isn抰 changing!  It is not an immutable object, so every time
//we iterate the collection, we run the query AGAIN, and that reparses everything.
//Since we only want to parse everything once, we iterate it once and store the results in an array.
//Now that we抳e got our parsed arguments, we抣l do an error checking pass:
var arguments = (from argument in argumentStrings
select new Argument(argument)).ToArray();
Console.Write("Command line: ");
foreach (Argument a in arguments)
{
Console.Write($"{a.Original} ");
}
Console.WriteLine("");
ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer();
analyzer.AddArgumentVerifier(
new ArgumentDefinition("output",
"/output:[path to output]",
"Specifies the location of the output file.",
x => x.IsCompoundSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("trialMode",
"/trialmode",
"If this is specified it places the product into trial mode",
x => x.IsSimpleSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("DEBUGOUTPUT",
"/debugoutput:[value1];[value2];[value3]",
"A listing of the files the debug output information will be written to",
x => x.IsComplexSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("",
"[literal value]",
"A literal value",
x => x.IsSimple));
if (!analyzer.VerifyArguments(arguments))
{
string invalidArguments = analyzer.InvalidArgumentsDisplay();
Console.WriteLine(invalidArguments);
ShowUsage(analyzer);
return;
}
//We抣l come back to that.  Assuming that our error checking pass gave the thumbs up,
//we抣l extract the information out of the parsed arguments that we need to run our program.
//Here抯 the information we need:
string output = string.Empty;
bool trialmode = false;
IEnumerable<string> debugOutput = null;
List<string> literals = new List<string>();
//For each parsed argument we want to apply an action,
// so add them to the analyzer .
analyzer.AddArgumentAction("OUTPUT", x => { output = x.SubArguments[0]; });
analyzer.AddArgumentAction("TRIALMODE", x => { trialmode = true; });
analyzer.AddArgumentAction("DEBUGOUTPUT", x => { debugOutput = x.SubArguments; });
analyzer.AddArgumentAction("", x => { literals.Add(x.Original); });
// check the arguments and run the actions
analyzer.EvaluateArguments(arguments);
// display the results
Console.WriteLine("");
Console.WriteLine($"OUTPUT: {output}");
Console.WriteLine($"TRIALMODE: {trialmode}");
if (debugOutput != null)
{
foreach (string item in debugOutput)
{
Console.WriteLine($"DEBUGOUTPUT: {item}");
}
}
foreach (string literal in literals)
{
Console.WriteLine($"LITERAL: {literal}");
}
//and we are ready to run our program:
//Program program = new Program(output, trialmode, debugOutput, literals);
//program.Run();
}
public static void ShowUsage(ArgumentSemanticAnalyzer analyzer)
{
Console.WriteLine("Program.exe allows the following arguments:");
foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions)
{
Console.WriteLine($"\t{definition.ArgumentSwitch}: ({definition.Description}){Environment.NewLine}\tSyntax: {definition.Syntax}");
}
}
public sealed class Argument
{
public string Original { get; }
public string Switch { get; private set; }
public ReadOnlyCollection<string> SubArguments { get; }
private List<string> subArguments;
public Argument(string original)
{
Original = original;
Switch = string.Empty;
subArguments = new List<string>();
SubArguments = new ReadOnlyCollection<string>(subArguments);
Parse();
}
private void Parse()
{
if (string.IsNullOrEmpty(Original))
{
return;
}
char[] switchChars = { '/', '-' };
if (!switchChars.Contains(Original[0]))
{
return;
}
string switchString = Original.Substring(1);
string subArgsString = string.Empty;
int colon = switchString.IndexOf(':');
if (colon >= 0)
{
subArgsString = switchString.Substring(colon + 1);
switchString = switchString.Substring(0, colon);
}
Switch = switchString;
if (!string.IsNullOrEmpty(subArgsString))
subArguments.AddRange(subArgsString.Split(';'));
}
// A set of predicates that provide useful information about itself
//   Implemented using lambdas
public bool IsSimple => SubArguments.Count == 0;
public bool IsSimpleSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 0;
public bool IsCompoundSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 1;
public bool IsComplexSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count > 0;
}
public sealed class ArgumentDefinition
{
public string ArgumentSwitch { get;  }
public string Syntax { get;  }
public string Description { get;  }
public Func<Argument, bool> Verifier { get;  }
public ArgumentDefinition(string argumentSwitch,
string syntax,
string description,
Func<Argument, bool> verifier)
{
ArgumentSwitch = argumentSwitch.ToUpper();
Syntax = syntax;
Description = description;
Verifier = verifier;
}
public bool Verify(Argument arg) => Verifier(arg);
}
public sealed class ArgumentSemanticAnalyzer
{
private List<ArgumentDefinition> argumentDefinitions =
new List<ArgumentDefinition>();
private Dictionary<string, Action<Argument>> argumentActions =
new Dictionary<string, Action<Argument>>();
public ReadOnlyCollection<Argument> UnrecognizedArguments { get; private set; }
public ReadOnlyCollection<Argument> MalformedArguments { get; private set; }
public ReadOnlyCollection<Argument> RepeatedArguments { get; private set; }
public ReadOnlyCollection<ArgumentDefinition> ArgumentDefinitions => new ReadOnlyCollection<ArgumentDefinition>(argumentDefinitions);
public IEnumerable<string> DefinedSwitches => from argumentDefinition in argumentDefinitions
select argumentDefinition.ArgumentSwitch;
public void AddArgumentVerifier(ArgumentDefinition verifier) => argumentDefinitions.Add(verifier);
public void RemoveArgumentVerifier(ArgumentDefinition verifier)
{
var verifiersToRemove = from v in argumentDefinitions
where v.ArgumentSwitch == verifier.ArgumentSwitch
select v;
foreach (var v in verifiersToRemove)
argumentDefinitions.Remove(v);
}
public void AddArgumentAction(string argumentSwitch, Action<Argument> action) => argumentActions.Add(argumentSwitch, action);
public void RemoveArgumentAction(string argumentSwitch)
{
if (argumentActions.Keys.Contains(argumentSwitch))
argumentActions.Remove(argumentSwitch);
}
public bool VerifyArguments(IEnumerable<Argument> arguments)
{
// no parameter to verify with, fail.
if (!argumentDefinitions.Any())
return false;
// Identify if any of the arguments are not defined
this.UnrecognizedArguments = (from argument in arguments
where !DefinedSwitches.Contains(argument.Switch.ToUpper())
select argument).ToList().AsReadOnly();
//Check for all the arguments where the switch matches a known switch,
//but our well-formedness predicate is false.
this.MalformedArguments = (from argument in arguments
join argumentDefinition in argumentDefinitions
on argument.Switch.ToUpper() equals
argumentDefinition.ArgumentSwitch
where !argumentDefinition.Verify(argument)
select argument).ToList().AsReadOnly();
//Sort the arguments into 揼roups?by their switch, count every group,
//and select any groups that contain more than one element,
//We then get a read only list of the items.
this.RepeatedArguments =
(from argumentGroup in
from argument in arguments
where !argument.IsSimple
group argument by argument.Switch.ToUpper()
where argumentGroup.Count() > 1
select argumentGroup).SelectMany(ag => ag).ToList().AsReadOnly();
if (this.UnrecognizedArguments.Any() ||
this.MalformedArguments.Any() ||
this.RepeatedArguments.Any())
return false;
return true;
}
public void EvaluateArguments(IEnumerable<Argument> arguments)
{
//Now we just apply each action:
foreach (Argument argument in arguments)
argumentActions[argument.Switch.ToUpper()](argument);
}
public string InvalidArgumentsDisplay()
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat($"Invalid arguments: {Environment.NewLine}");
// Add the unrecognized arguments
FormatInvalidArguments(builder, this.UnrecognizedArguments,
"Unrecognized argument: {0}{1}");
// Add the malformed arguments
FormatInvalidArguments(builder, this.MalformedArguments,
"Malformed argument: {0}{1}");
// For the repeated arguments, we want to group them for the display
// so group by switch and then add it to the string being built.
var argumentGroups = from argument in this.RepeatedArguments
group argument by argument.Switch.ToUpper() into ag
select new { Switch = ag.Key, Instances = ag };
foreach (var argumentGroup in argumentGroups)
{
builder.AppendFormat($"Repeated argument: {argumentGroup.Switch}{Environment.NewLine}");
FormatInvalidArguments(builder, argumentGroup.Instances.ToList(),
"\t{0}{1}");
}
return builder.ToString();
}
private void FormatInvalidArguments(StringBuilder builder,
IEnumerable<Argument> invalidArguments, string errorFormat)
{
if (invalidArguments != null)
{
foreach (Argument argument in invalidArguments)
{
builder.AppendFormat(errorFormat,
argument.Original, Environment.NewLine);
}
}
}
}

如何使用这些类为应用程序处理命令行?方法如下所示。

public static void Main(string[] argumentStrings) { var arguments = (from argument in argumentStrings select new Argument(argument)).ToArray(); Console.Write("Command line: "); foreach (Argument a in arguments) { Console.Write($"{a.Original} "); } Console.WriteLine(""); ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer(); analyzer.AddArgumentVerifier( new ArgumentDefinition("output", "/output:[path to output]", "Specifies the location of the output file.", x => x.IsCompoundSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("trialMode", "/trialmode", "If this is specified it places the product into trial mode", x => x.IsSimpleSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("DEBUGOUTPUT", "/debugoutput:[value1];[value2];[value3]", "A listing of the files the debug output " + "information will be written to", x => x.IsComplexSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("", "[literal value]", "A literal value", x => x.IsSimple)); if (!analyzer.VerifyArguments(arguments)) { string invalidArguments = analyzer.InvalidArgumentsDisplay(); Console.WriteLine(invalidArguments); ShowUsage(analyzer); return; } // 设置命令行解析结果的容器 string output = string.Empty; bool trialmode = false; IEnumerable<string> debugOutput = null; List<string> literals = new List<string>(); //我们想对每一个解析出的参数应用一个动作, //因此将它们添加到分析器 analyzer.AddArgumentAction("OUTPUT", x => { output = x.SubArguments[0]; }); analyzer.AddArgumentAction("TRIALMODE", x => { trialmode = true; }); analyzer.AddArgumentAction("DEBUGOUTPUT", x => { debugOutput = x.SubArguments; }); analyzer.AddArgumentAction("", x=>{literals.Add(x.Original);}); // 检查参数并运行动作 analyzer.EvaluateArguments(arguments); // 显示结果 Console.WriteLine(""); Console.WriteLine($"OUTPUT: {output}"); Console.WriteLine($"TRIALMODE: {trialmode}"); if (debugOutput != null) { foreach (string item in debugOutput) { Console.WriteLine($"DEBUGOUTPUT: {item}"); } } foreach (string literal in literals) { Console.WriteLine($"LITERAL: {literal}"); } } public static void ShowUsage(ArgumentSemanticAnalyzer analyzer) { Console.WriteLine("Program.exe allows the following arguments:"); foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions) { Console.WriteLine($"\t{definition.ArgumentSwitch}: ({definition.Description}){Environment.NewLine} \tSyntax: {definition.Syntax}"); } } 

讲解

在解析命令行参数之前,必须明确选用一种通用格式。本范例中使用的格式遵循用于
Visual C#.NET 语言编译器的命令行格式。使用的格式定义如下所示。

  • 通过一个或多个空白字符分隔命令行参数。
  • 每个参数可以以一个 - 或 / 字符开头,但不能同时以这两个字符开头。如果不以其中一个字符开头,就把参数视为一个字面量,比如文件名。
  • 以 - 或 / 字符开头的参数可被划分为:以一个选项开关开头,后接一个冒号,再接一个或多个用 ; 字符分隔的参数。命令行参数 -sw:arg1;arg2;arg3 可被划分为一个选项开关(sw )和三个参数(arg1 、arg2 和 arg3 )。注意,在完整的参数中不应该有任何空格,否则运行时命令行解析器将把参数分拆为两个或更多的参数。
  • 用双引号包裹住的字符串(如 "c:\test\file.log" )会去除双引号。这是操作系统解释传入应用程序中的参数时的一项功能。
  • 不会去除单引号。
  • 要保留双引号,可在双引号字符前放置 \ 转义序列字符。
  • 仅当 \ 字符后面接着双引号时,才将 \ 字符作为转义序列字符处理;在这种情况下,只会显示双引号。
  • ^ 字符被运行时解析器作为特殊字符处理。
    幸运的是,在应用程序接收各个解析出的参数之前,运行时命令行解析器可以处理其中大部分任务。
    运行时命令行解析器把一个包含每个解析过的参数的 string[] 传递给应用程序的入口点。入口点可以采用以下形式之一。
    public static void Main() public static int Main() public static void Main(string[] args) public static int Main(string[] args)
    前两种形式不接受参数,但是后两种形式接受解析过的命令行参数的数组。注意,静态属性 Environment.CommandLine 将返回一个字符串,其中包含完整的命令行;静态方法 Environment.GetCommandLineArgs 将返回一个字符串数组,其中包含解析过的命令行参数。
    前文介绍的三个类涉及命令行参数的各个阶段。
  • Argument
    封装一个命令行参数并负责解析该参数。
  • ArgumentDefinition
    定义一个对当行命令行有效的参数。
  • ArgumentSemanticAnalyzer
    基于设置的 ArgumentDefinition 进行参数的验证和获取。
    把以下命令行参数传入这个应用程序中:
    MyApp c:\input\infile.txt -output:d:\outfile.txt -trialmode
    将得到以下解析过的选项开关和参数。
    Command line: c:\input\infile.txt -output:d:\outfile.txt -trialmode OUTPUT: d:\outfile.txt TRIALMODE: True LITERAL: c:\input\infile.txt
    如果您没有正确地输入命令行参数,比如忘记了向 -output 选项开关添加参数,得到的输出将如下所示。
    Command line: c:\input\infile.txt -output: -trialmode Invalid arguments: Malformed argument: -output Program.exe allows the following arguments: OUTPUT: (Specifies the location of the output file.) Syntax: /output:[path to output] TRIALMODE: (If this is specified, it places the product into trial mode) Syntax: /trialmode DEBUGOUTPUT: (A listing of the files the debug output information will be written to) Syntax: /debugoutput:[value1];[value2];[value3] : (A literal value) Syntax: [literal value]
    在这段代码中有几个值得指出的地方。
    每个 Argument 实例都需要能确定它自身的某些事项。相应地,作为 Argument 的属性暴露了一组谓词,告诉我们这个 Argument 的一些有用信息。ArgumentSemanticAnalyzer 将使用这些属性来确定参数的特征。
    public bool IsSimple => SubArguments.Count == 0; public bool IsSimpleSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 0; public bool IsCompoundSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 1; public bool IsComplexSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count > 0;
     关于 lambda 表达式的更多信息请参考
    Lambda 表达式
    这段代码有多处在 LINQ 查询的结果上调用了 ToArray 或 ToList 方法。
    var arguments = (from argument in argumentStrings select new Argument(argument)).ToArray();
    这是由于查询结果是延迟执行的。这不仅意味着将以迟缓方式来计算结果,而且意味着每次访问结果时都要重新计算它们。使用 ToArray 或 ToList 方法会强制积极计算结果,生成一份不需要在每次使用时都重新计算的副本。查询逻辑并不知道正在操作的集合是否发生了变化,因此每次都必须重新计算结果,除非使用这些方法创建出一份“即时”副本。
    为了验证这些参数是否正确,必须创建 ArgumentDefinition ,并将每个可接受的参数类型与 ArgumentSemanticAnalyzer 相关联,代码如下所示。
    ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer(); analyzer.AddArgumentVerifier( new ArgumentDefinition("output", "/output:[path to output]", "Specifies the location of the output file.", x => x.IsCompoundSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("trialMode", "/trialmode", "If this is specified it places the product into trial mode", x => x.IsSimpleSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("DEBUGOUTPUT", "/debugoutput:[value1];[value2];[value3]", "A listing of the files the debug output " + "information will be written to", x => x.IsComplexSwitch)); analyzer.AddArgumentVerifier( new ArgumentDefinition("", "[literal value]", "A literal value", x => x.IsSimple));
    每个 ArgumentDefinition 都包含 4 个部分:参数选项开关、显示参数语法的字符串、参数说明以及用于验证参数的验证谓词。这些信息可以用于验证参数,如下所示。
    //检查开关与某个已知开关匹配但是检查格式是否正确 //的谓词为false的所有参数 this.MalformedArguments = ( from argument in arguments join argumentDefinition in argumentDefinitions on argument.Switch.ToUpper() equals argumentDefinition.ArgumentSwitch where !argumentDefinition.Verify(argument) select argument).ToList().AsReadOnly();
    ArgumentDefinition 还允许为程序编写一个使用说明方法。
    public static void ShowUsage(ArgumentSemanticAnalyzer analyzer) { Console.WriteLine("Program.exe allows the following arguments:"); foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions) { Console.WriteLine("\t{0}: ({1}){2}\tSyntax: {3}", definition.ArgumentSwitch, definition.Description, Environment.NewLine,definition.Syntax); } }
    为了获取参数的值以便使用它们,需要从解析过的参数中提取信息。对于解决方案示例,我们需要以下信息。
    // 设置命令行解析结果的容器 string output = string.Empty; bool trialmode = false; IEnumerable debugOutput = null; List literals = new List();
    如何填充这些值?对于每个参数,都需要一个与之关联的动作,以确定如何从 Argument 实例获得值。每个动作就是一个谓词,这使得这种方式非常强大,因为您在这里可以使用任何谓词。下面的代码说明如何定义这些 Argument 动作并将其与 ArgumentSemanticAnalyzer 相关联。
    //对于每一个解析出的参数,我们想要对其应用一个动作, //因此将它们添加到分析器 analyzer.AddArgumentAction("OUTPUT", x => { output = x.SubArguments[0]; }); analyzer.AddArgumentAction("TRIALMODE", x => { trialmode = true; }); analyzer.AddArgumentAction("DEBUGOUTPUT", x => { debugOutput = x.SubArguments;}); analyzer.AddArgumentAction("", x=>{literals.Add(x.Original);});
    现在已经建立了所有的动作,就可以对 ArgumentSemanticAnalyzer 应用 EvaluateArguments 方法来获取值,代码如下所示。
    // 检查参数并运行动作 analyzer.EvaluateArguments(arguments);
    现在通过执行动作填充了值,并且可以利用这些值来运行程序,代码如下所示。
    // 传入参数值并运行程序 Program program = new Program(output, trialmode, debugOutput, literals); program.Run();
    如果在验证参数时使用 LINQ 来查询未识别的、格式错误的或者重复的实参(argument),其中任何一项都会导致形参(parameter)无效。
public bool VerifyArguments(IEnumerable<Argument> arguments) { // 没有任何参数进行验证,失败 if (!argumentDefinitions.Any()) return false; // 确认是否存在任一未定义的参数 this.UnrecognizedArguments = ( from argument in arguments where !DefinedSwitches.Contains(argument.Switch.ToUpper()) select argument).ToList().AsReadOnly(); if (this.UnrecognizedArguments.Any()) return false; //检查开关与某个已知开关匹配但是检查格式是否正确 //的谓词为false的所有参数 this.MalformedArguments = ( from argument in arguments join argumentDefinition in argumentDefinitions on argument.Switch.ToUpper() equals argumentDefinition.ArgumentSwitch where !argumentDefinition.Verify(argument) select argument).ToList().AsReadOnly(); if (this.MalformedArguments.Any()) return false; //将所有参数按照开关进行分组,统计每个组的数量, //并选出包含超过一个元素的所有组, //然后我们获得一个包含这些数据项的只读列表 this.RepeatedArguments = (from argumentGroup in from argument in arguments where !argument.IsSimple group argument by argument.Switch.ToUpper() where argumentGroup.Count() > 1 select argumentGroup).SelectMany(ag => ag).ToList().AsReadOnly(); if (this.RepeatedArguments.Any()) return false; return true; }

与 LINQ 出现之前通过多重嵌套循环、switch 语句、IndexOf 方法及其他机制实现同样功能的代码相比,上述使用 LINQ 的代码更加易于理解每一个验证阶段。每个查询都用问题领域的语言简洁地指出了它在尝试执行什么任务。
 LINQ 旨在帮助解决那些必须排序、查找、分组、筛选和投影数据的问题。请使用它!

参考

posted on 2022-04-07 21:04  张赐荣  阅读(351)  评论(0编辑  收藏  举报

感谢访问张赐荣的技术分享博客!
博客地址:https://cnblogs.com/netlog/
知乎主页:https://www.zhihu.com/people/tzujung-chang
个人网站:https://prc.cx/