//****************************************************************************************************** // Arguments.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 09/12/2004 - J. Ritchie Carroll // Generated original version of source code. // 03/28/2006 - Pinal C. Patel // Migrated 2.0 version of source code from 1.1 source (GSF.Console). // 10/09/2007 - J. Ritchie Carroll / Pinal C. Patel // Fixed stand-alone argument issue at end of line and changed to use generic Dictionary class. // 09/15/2008 - J. Ritchie Carroll // Converted to C#. // 09/26/2008 - Pinal C. Patel // Entered code comments. // 08/05/2009 - Josh L. Patterson // Edited Comments. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 04/2/2012 - J. Ritchie Carroll // Added new TryGetValue() method. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Security; using System.Text; using System.Text.RegularExpressions; using GSF.Collections; namespace GSF.Console { /// /// Represents an ordered set of arguments along with optional arguments parsed from a command-line. /// /// /// This example shows how to parse a command-line command that does not contain an executable name: /// /// using System; /// using GSF; /// using GSF.Console; /// /// class Program /// { /// static void Main() /// { /// Arguments args = new Arguments("Sample.txt -wrap=true"); /// string file = args["OrderedArg1"]; /// bool wrapText = args["wrap"].ParseBoolean(); /// /// Console.WriteLine(string.Format("File: {0}", file)); /// Console.WriteLine(string.Format("Wrap text: {0}", wrapText)); /// Console.ReadLine(); /// } /// } /// /// This example shows how to parse a command-line command that contains an executable name in it: /// /// using System; /// using GSF.Console; /// /// class Program /// { /// static void Main() /// { /// // Environment.CommandLine = @"""c:\program files\tva\theme application\app.exe"" Document1.dcx -theme=default" /// Arguments args = new Arguments(Environment.CommandLine, true); /// string doc = args["OrderedArg1"]; /// string theme = args["theme"]; /// /// Console.WriteLine(string.Format("Document: {0}", doc)); /// Console.WriteLine(string.Format("Application theme: {0}", theme)); /// Console.ReadLine(); /// } /// } /// /// [Serializable] public class Arguments : IEnumerable>, ISerializable { #region [ Members ] // Constants /// /// Regular expression pattern for tokenizing a list of arguments. /// public const string TokenRegex = @"(?:(?"")(?:[^\\""]*(?:\\.)*)*(?"")(?(Open)(?!))|\S)+"; /// /// Default value for . /// public const string DefaultOrderedArgID = "OrderedArg"; // Fields private readonly string m_commandLine; private readonly string m_orderedArgID; private readonly int m_orderedArgCount; private readonly Dictionary m_arguments; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// /// The command-line command to be parsed. public Arguments(string commandLine) : this(commandLine, false) { } /// /// Initializes a new instance of the class. /// /// The command-line command to be parsed. /// true if the first argument in the command-line command is to be skipped from being processed; otherwise false. public Arguments(string commandLine, bool skipFirstArgument) : this(commandLine, "OrderedArg", skipFirstArgument) { } /// /// Initializes a new instance of the class. /// /// The command-line command to be parsed. /// The prefix to be used in the identifier of ordered arguments. public Arguments(string commandLine, string orderedArgID) : this(commandLine, orderedArgID, false) { } /// /// Initializes a new instance of the class. /// /// The command-line command to be parsed. /// The prefix to be used in the identifier of ordered arguments. /// true if the first argument in the command-line command is to be skipped from being processed; otherwise false. public Arguments(string commandLine, string orderedArgID, bool skipFirstArgument) { Regex spliter = new Regex("^-{1,2}|^/|=|:", RegexOptions.IgnoreCase | RegexOptions.Compiled); Regex remover = new Regex("^[\'\"]?(.*?)[\'\"]?$", RegexOptions.IgnoreCase | RegexOptions.Compiled); string parameter = null; string argument; string[] parts; // We create a case-insensitive dictionary for parameters m_arguments = new Dictionary(StringComparer.CurrentCultureIgnoreCase); m_orderedArgCount = 0; m_commandLine = commandLine; m_orderedArgID = orderedArgID; // Valid parameters forms: // {-,/,--}param{=,:}((",')value(",')) // Examples: // -param1=value1 --param2 /param3:"Test-:-work" // /param4=happy -param5 '--=nice=--' string[] args = ParseCommand(m_commandLine); if (skipFirstArgument) { if (args.Length > 1) args = args.Copy(1, args.Length - 1); else args = new string[0]; } foreach (string arg in args) { // Found just a parameter in last pass... // The last parameter is still waiting, with no value, set it to nothing. if ((object)parameter != null) { if (!m_arguments.ContainsKey(parameter)) m_arguments.Add(parameter, null); } parameter = null; if (!string.IsNullOrEmpty(arg)) { // If this argument begins with a quote, we treat it as a stand-alone argument if (arg[0] == '\"' || arg[0] == '\'') { // Handle stand alone ordered arguments m_orderedArgCount++; parameter = orderedArgID + m_orderedArgCount; // Remove possible enclosing characters (",') if (!m_arguments.ContainsKey(parameter)) { argument = remover.Replace(arg, "$1"); m_arguments.Add(parameter, argument); } parameter = null; } else { // Look for new parameters (-,/ or --) and a // possible enclosed value (=,:) parts = spliter.Split(arg, 3); switch (parts.Length) { case 1: // Handle stand alone ordered arguments m_orderedArgCount++; parameter = orderedArgID + m_orderedArgCount; // Remove possible enclosing characters (",') if (!m_arguments.ContainsKey(parameter)) { argument = remover.Replace(arg, "$1"); m_arguments.Add(parameter, argument); } parameter = null; break; case 2: // Found just a parameter parameter = parts[1]; break; case 3: // Parameter with enclosed value parameter = parts[1]; // Remove possible enclosing characters (",') if (!m_arguments.ContainsKey(parameter)) { parts[2] = remover.Replace(parts[2], "$1"); m_arguments.Add(parameter, parts[2]); } parameter = null; break; } } } } // In case a parameter is still waiting if ((object)parameter != null && !m_arguments.ContainsKey(parameter)) m_arguments.Add(parameter, null); } /// /// Creates a new from serialization parameters. /// /// The with populated with data. /// The source for this deserialization. protected Arguments(SerializationInfo info, StreamingContext context) { // Deserialize argument fields m_commandLine = info.GetOrDefault("commandLine", ""); m_orderedArgID = info.GetOrDefault("orderedArgID", DefaultOrderedArgID); m_orderedArgCount = info.GetOrDefault("orderedArgCount", 0); m_arguments = new Dictionary(StringComparer.CurrentCultureIgnoreCase); int argumentCount = info.GetOrDefault("argumentCount", 0); for (int i = 0; i < argumentCount; i++) { string key = info.GetOrDefault("argumentKey" + i, null as string); if (!string.IsNullOrEmpty(key)) m_arguments.Add(key, info.GetOrDefault("argumentValue" + i, null as string)); } } #endregion #region [ Properties ] /// /// Gets the value for the specified argument from the command-line command. /// /// The argument whose value is to retrieved. /// Value for the specified argument if found; otherwise null. public virtual string this[string argument] { get { string value; m_arguments.TryGetValue(argument, out value); return value; } } /// /// Gets the total number of arguments (ordered and optional) present in the command-line command. /// public virtual int Count { get { return m_arguments.Count; } } /// /// Gets the prefix text in the identifier of ordered arguments present in the command-line command. /// public virtual string OrderedArgID { get { return m_orderedArgID; } } /// /// Gets the total number of ordered arguments in the command-line command. /// public virtual int OrderedArgCount { get { return m_orderedArgCount; } } /// /// Gets the ordered arguments as an array of strings. /// public virtual string[] OrderedArgs { get { return Enumerable.Range(1, OrderedArgCount) .Select(arg => this[OrderedArgID + arg]) .ToArray(); } } /// /// Gets a boolean value that indicates whether the command-line command contains request for displaying help. /// public virtual bool ContainsHelpRequest { get { return (m_arguments.ContainsKey("?") || m_arguments.ContainsKey("Help")); } } /// /// Gets the dictionary containing all of the arguments present in the command-line command. /// protected Dictionary InternalDictionary { get { return m_arguments; } } #endregion #region [ Methods ] /// /// Gets the value associated with the specified argument in the command-line command. /// /// The argument whose value is to be retrieved. /// Value associated with the specified argument if it exists in the command-line command, otherwise null. /// true if argument existed and was returned; otherwise false. public virtual bool TryGetValue(string argument, out string value) { return m_arguments.TryGetValue(argument, out value); } /// /// Gets a boolean value that indicates whether the specified argument is present in the command-line command. /// /// The argument to be checked. /// true if the argument exists in the command-line command; otherwise false. public virtual bool Exists(string argument) { return m_arguments.ContainsKey(argument); } /// /// Gets a string representation of the object. /// /// A string representation of the object. public override string ToString() { return m_commandLine; } /// /// Returns an for iterating through all the command-line command arguments. /// /// An for the command-line command arguments. public IEnumerator> GetEnumerator() { return m_arguments.GetEnumerator(); } /// /// Returns an for iterating through all the command-line command arguments. /// /// An for the command-line command arguments. IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)m_arguments).GetEnumerator(); } /// /// Populates a with the data needed to serialize the target object. /// /// The to populate with data. /// The destination (see ) for this serialization. /// The caller does not have the required permission. public void GetObjectData(SerializationInfo info, StreamingContext context) { // Serialize argument fields info.AddValue("commandLine", m_commandLine); info.AddValue("orderedArgID", m_orderedArgID); info.AddValue("orderedArgCount", m_orderedArgCount); info.AddValue("argumentCount", m_arguments.Count); int i = 0; foreach (KeyValuePair argument in m_arguments) { info.AddValue("argumentKey" + i, argument.Key); info.AddValue("argumentValue" + i, argument.Value); i++; } } #endregion #region [ Static ] // Static Fields private static readonly Regex CompiledTokenRegex = new Regex(TokenRegex, RegexOptions.Compiled); // Static Methods /// /// This function can be used to parse a single parameterized string and turn it into an array of parameters. /// /// String of parameters. /// Array of parameters. public static string[] ParseCommand(string command) { lock (CompiledTokenRegex) return CompiledTokenRegex.Matches(command) .Cast() .Select(match => match.Value) .ToArray(); } /// /// Provides an array of arguments parsed from the given command /// using parsing rules similar to those used in POSIX environments. /// /// The command to be parsed. /// An array of arguments. /// /// /// This function processes a set of command line arguments using a simple set of rules based on /// those used in popular POSIX command shells. It is important to note that these rules do not apply /// to instances of the class or the function. /// /// /// /// The motivation for this function was to provide a more user-friendly set of rules by which to /// specify command-line arguments. This is of course helpful for the user who has to type the /// command-line arguments by hand, but it also ends up being useful for systems that have to /// programmatically escape arguments to be consumed by other command-line applications (see /// the function). /// /// /// /// Also note that Windows has historically not provided a standard method for handling /// command-line arguments and that some of the command-line tools in Windows have rolled their /// own (potentially faulty) methods for doing so. More recently, Microsoft has provided the /// function for .NET applications to retrieve their /// own command line arguments using a standard format, but this function lacks the appropriate /// supporting functions for programmatically handling arbitrary command strings within an /// application. Furthermore, the rules for escaping spaces, quotes, and backslashes are very /// inconsistent, making it difficult to write a robust method for escaping individual arguments, /// let alone fully reproducing the same command parsing rules. For this reason, some programmers /// may be interested in using this method in conjunction with /// for parsing their own command line arguments. /// /// public static string[] ToArgs(string command) { string[] tokenCharacterPatterns = { // Backslash followed by another character @"\\.", // Substring wrapped in double quotes @"""(?:\\.|[^\\""])*""", // Substring wrapped in single quotes @"'[^']*'", // Mismatched double quote @"""(?:\\.|[^\\""])*\\?$", // Mismatched single quote @"'[^']*$", // A single character that is not a // single quote, double quote, or whitespace @"[^'""\s]" }; // Define the regular expression pattern to match tokens string pattern = $"(?<=^|\\s)({string.Join("|", tokenCharacterPatterns)})+(?=\\s|$)"; // This function converts an escape // sequence into the corresponding character Func unescape = escapeSequence => { if (escapeSequence == @"\n") return "\n"; if (escapeSequence == @"\r") return "\r"; if (escapeSequence == @"\t") return "\t"; return escapeSequence.Substring(1); }; // This function unquotes the given string by // simply removing the first and last characters Func unquote = str => str.Substring(1, str.Length - 2); // This function converts a token character into // its corresponding output in the args array Func interpretCharacter = tokenChar => { string value = tokenChar.Value; switch (value[0]) { // Backslash indicates an escape sequence case '\\': if (value.Length == 1) throw new FormatException("Malformed expression - dangling escape sequence."); return unescape(value); // Expressions wrapped in double quotes must be stripped of the surrounding // double quotes, and processed internally for escape sequences case '"': if (!value.EndsWith("\"", StringComparison.Ordinal)) throw new FormatException($"Malformed expression - mismatched quote. arg: {value}"); // A simple regex can be used to find the escape sequences since // it's already known that this is a cohesive double-quoted token List innerTokens = Regex.Matches(unquote(value), @"\\?.") .Cast() .Select(match => match.Value) .ToList(); // Mismatched quotes could also end in an escaped quote, // indicated here by an inner dangling escape sequence if (innerTokens.Any() && innerTokens.Last() == @"\") throw new FormatException($"Malformed expression - mismatched quote. arg: {value}"); return string.Concat(innerTokens.Select(token => token.Length > 1 ? unescape(token) : token)); // Expressions wrapped in single quotes must // be stripped of the surrounding single quotes case '\'': if (!value.EndsWith("'", StringComparison.Ordinal)) throw new FormatException($"Malformed expression - mismatched quote. arg: {value}"); return unquote(value); // Any other character produces itself default: return value; } }; // This function converts a token to its corresponding output in the args array Func interpretToken = token => string.Concat(token.Groups[1].Captures.Cast().Select(interpretCharacter)); // Apply the regular expression pattern and // convert the result into the array of arguments return Regex.Matches(command, pattern) .Cast() .Select(interpretToken) .ToArray(); } /// /// Escapes a string so that it will be parsed as a /// single argument by the method. /// /// The argument to be escaped. /// The escaped argument. public static string Escape(string arg) { StringBuilder escapedArgBuilder = new StringBuilder(); // Empty string is encoded as // two empty double quotes if (string.IsNullOrEmpty(arg)) return new string('"', 2); foreach (char c in arg) { switch (c) { case '\\': case '"': case '\'': // Quotes and backslashes need // to be escaped by a backslash escapedArgBuilder.Append('\\').Append(c); break; case '\n': // Newline (\n) escapedArgBuilder.Append(@"\n"); break; case '\r': // Carriage return (\r) escapedArgBuilder.Append(@"\r"); break; case '\t': // Tab (\t) escapedArgBuilder.Append(@"\t"); break; default: // White space needs to be // escaped by a backslash if (char.IsWhiteSpace(c)) escapedArgBuilder.Append('\\'); // All other characters do // not need to be escaped escapedArgBuilder.Append(c); break; } } return escapedArgBuilder.ToString(); } #endregion } }