1. 程式人生 > 其它 >C#.NET 程式解析命令列啟動引數

C#.NET 程式解析命令列啟動引數

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 旨在幫助解決那些必須排序、查詢、分組、篩選和投影資料的問題。請使用它!

參考