C#.NET 程式解析命令列啟動引數
阿新 • • 發佈:2022-04-07
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; IEnumerabledebugOutput = 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 旨在幫助解決那些必須排序、查詢、分組、篩選和投影資料的問題。請使用它!