需要应用程序以标准格式(在1.5.3中介绍)接受一个或多个命令行参数。你需要访问和解析传递给应用程序的完整的命令行。
在例1-5中,结合使用以下类来帮助解析命令行参数:Argument\ArgumentDefinition和ArgumentSemanticAnalyzer。
例1-5:Argument类:
using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Text;
public sealed class Argument
{
public string Original { get; }
public string Switch { get; private set; }
public ReadOnlyCollection SubArguments { get; }
private List subArguments;
public Argument(string original)
{
Original = original; Switch = string.Empty;
subArguments = new List();
SubArguments = new ReadOnlyCollection(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(';'));
}
// 一组谓词,提供关于参数的有用信息
// 使用lambda表达式实现
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 Verifier { get; }
public ArgumentDefinition(string argumentSwitch, string syntax, string description, Func verifier)
{
ArgumentSwitch = argumentSwitch.ToUpper();
Syntax = syntax;
Description = description;
Verifier = verifier;
}
public bool Verify(Argument arg) => Verifier(arg);
}
public sealed class ArgumentSemanticAnalyzer
{
private List argumentDefinitions = new List();
private Dictionary> argumentActions = new Dictionary>();
public ReadOnlyCollection UnrecognizedArguments { get; private set; }
public ReadOnlyCollection MalformedArguments { get; private set; }
public ReadOnlyCollection RepeatedArguments { get; private set; }
public ReadOnlyCollection ArgumentDefinitions =>
new ReadOnlyCollection(argumentDefinitions);
public IEnumerable 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 action) => argumentActions.Add(argumentSwitch, action);
public void RemoveArgumentAction(string argumentSwitch)
{
if (argumentActions.Keys.Contains(argumentSwitch))
argumentActions.Remove(argumentSwitch);
}
public bool VerifyArguments(IEnumerable arguments)
{
// 没有任何参数进行验证,失败
if (!argumentDefinitions.Any())
return false;
// 确认是否存在任一未定义的参数
this.UnrecognizedArguments = (from argumentItem in arguments
where !DefinedSwitches.Contains(argumentItem.Switch.ToUpper())
select argumentItem).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;
}
public void EvaluateArguments(IEnumerable arguments)
{
//此时只需要应用每个动作:
foreach (Argument argument in arguments)
argumentActions[argument.Switch.ToUpper()](argument);
}
public string InvalidArgumentsDisplay()
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat($"Invalid arguments: {Environment.NewLine}"); // 的
FormatInvalidArguments(builder, this.UnrecognizedArguments, "Unrecognized argument: {0}{1}");
// 添加格式不正式的参数
FormatInvalidArguments(builder, this.MalformedArguments, "Malformed argument: {0}{1}");
// 对于重复的参数,我们想要将其分组以用于显示,
// 因此通过开关分组并且将其添加到正在构建的字符串
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 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 debugOutput = null;
List literals = new List();
//我们相对每一个解析出的参数应用一个动作,因此将他们添加到分析器
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语言编译器的命令行格式。使用的格式定义如下所示。
幸运的是,在应用程序接受各个解析出的参数之前,运行时命令行解析器可以处理其中大部分任务。
运行时命令行解析器把一个包含每个解析过的参数的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将返回一个字符串数组,其中包含解析过的命令行参数。
1.5.2节介绍的三个类涉及命令行参数的各个阶段:
• 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;
这段代码由多出在LINQ查询的结果上调用了ToArray或ToList方法。这是由于查询结果是延迟执行的。这不仅意味着将以迟缓方式来计算结果,而且意味着每次访问结果是都需要从新计算他们。使用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();
ArgumentFinition还允许为程序编写一个使用说明方法。
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
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);
如果在验证参数时使用LINQ来查询未识别的、格式错误的或者重复的实参(argument),其中任何一项都会导致形参(parameter)无效。
public bool VerifyArguments(IEnumerable
{
// 没有任何参数进行验证,失败
if (!argumentDefinitions.Any())
return false;
// 确认是否存在任一未定义的参数
this.UnrecognizedArguments = (from argumentItem in arguments
where !DefinedSwitches.Contains(argumentItem.Switch.ToUpper())
select argumentItem).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的代码更加易于理解每一个验证阶段。每个查询都用问题领域的语言简介的指出了他在尝试执行什么任务。
MSDN文档中的”Main“和”命令行参数“主题。