Ultimate command line arguments parsing: query with Linq

Command line parsing in C# is (static void Main(args []string) is very limited.

That is why I created a ParameterParser class that results a list of Parameter objects that can be queried with Linq. An example:


      -country=Sweden -IsNiceCountry   -Country="The Netherlands"  
      -"This = difficult"="Contains a "" and a -sign + double quotes: """"" 
      /empty= /space=" "
      

This results in a list of Parameter objects. A Parameter object provides properties:


      var parameters = new ParametersParser();
      foreach (var parameter in parameters)
      {
          Console.WriteLine("Index   : " + parameter.Index);
          Console.WriteLine("Bruto   : " + parameter.Bruto);
          Console.WriteLine("Netto   : [" + parameter.Netto + "]");
          Console.WriteLine("Key     : " + parameter.Key);
          Console.WriteLine("Value   : [" + (parameter.Value == null ? "<null>" : parameter.Value) + "]");
          Console.WriteLine("HasValue: " + parameter.HasValue);
          Console.WriteLine("");
      }
      

This code results in this list:


      Index   : 0
      Bruto   : -country=Sweden
      Netto   : [-country=Sweden]
      Key     : -country
      Value   : [Sweden]
      HasValue: True
       
      Index   : 1
      Bruto   : -IsNiceCountry
      Netto   : [-IsNiceCountry]
      Key     : -IsNiceCountry
      Value   : [<null>]
      HasValue: False
       
      Index   : 2
      Bruto   : -Country="The Netherlands"
      Netto   : [-Country=The Netherlands]
      Key     : -Country
      Value   : [The Netherlands]
      HasValue: True
       
      Index   : 3
      Bruto   : -"This = difficult"="Contains a "" and a -sign + double quotes: """""
      Netto   : [-This = difficult=Contains a " and a -sign + double quotes: ""]
      Key     : -This = difficult
      Value   : [Contains a " and a -sign + double quotes: ""]
      HasValue: True
       
      Index   : 4
      Bruto   : /empty=
      Netto   : [/empty=]
      Key     : /empty
      Value   : []
      HasValue: True
       
      Index   : 5
      Bruto   : /space=" "
      Netto   : [/space= ]
      Key     : /space
      Value   : [ ]
      HasValue: True
      

Now, it is easy to use Linq to query the parameters. For example: Get the parameters that has no value:


      var noValues = parameters.Where(p => !p.HasValue); 
      foreach (var noValue in noValues)
      {
          Console.WriteLine("No value: " + noValue);
      }
      

Result:


      No value: -IsNiceCountry
      

Or you could use methods that does the Linq stuff for you:


      // By default case insensitive 
      var countryParameters = parameters.GetParameters("-country");
      foreach (var parameter in countryParameters)
      {
          Console.WriteLine(parameter.Key + ": " + parameter.Value);
      }
      Console.WriteLine("");
       
      foreach (var key in parameters.DistinctKeys)
      {
          Console.WriteLine("Key     : " + key);
      }
       
      Console.WriteLine("");
      Console.WriteLine("Index 2 : " + parameters[2].Value);
       
      Console.WriteLine("");
      Console.WriteLine("Index of: " + parameters.GetParameters("/space").First().Index);
      

Result:


      -country: Sweden
      -Country: The Netherlands
       
      Key     : -country
      Key     : -IsNiceCountry
      Key     : -This = difficult
      Key     : /empty
      Key     : /space
       
      Index 2 : The Netherlands
       
      Index of: 5
      

Some other examples:


      Console.WriteLine(parameters.HasKey("/space")); // true 
      Console.WriteLine(parameters.GetFirstValue("/Space")); // " "
      Console.WriteLine(parameters.HasKeyAndValue("/Empty")); // true
      Console.WriteLine(parameters.HasKeyAndNoValue("-IsNiceCountry")); // true
      

Last but not least: The code of the classes I created.

The public ParametersParser class:


      public class ParametersParser : IEnumerable<Parameter>
      {
          private readonly bool _caseSensitive;
          private readonly List<Parameter> _parameters;
          public string ParametersText { get; private set; }
       
          public ParametersParser(
              string parametersText = null, 
              bool caseSensitive = false, 
              char keyValuesplitter = '=')
          {
              _caseSensitive = caseSensitive;
              ParametersText = parametersText != null ? parametersText : GetAllParametersText();
              _parameters = new BareParametersParser(ParametersText, keyValuesplitter)
                                   .Parameters.ToList();
          }
       
          public ParametersParser(bool caseSensitive)
              : this(null, caseSensitive)
          {
          }
       
          public IEnumerable<Parameter> GetParameters(string key)
          {
              return _parameters.Where(p => p.Key.Equals(key, ThisStringComparison));
          }
       
          public IEnumerable<string> GetValues(string key)
          {
              return GetParameters(key).Where(p => p.HasValue).Select(p => p.Value);
          }
       
          public string GetFirstValue(string key)
          {
              return GetFirstParameter(key).Value;
          }
       
          public Parameter GetFirstParameterOrDefault(string key)
          {
              return ParametersWithDistinctKeys.FirstOrDefault(KeyEqualsPredicate(key));
          }
       
          public Parameter GetFirstParameter(string key)
          {
              return ParametersWithDistinctKeys.First(KeyEqualsPredicate(key));
          }
       
          private Func<Parameter, bool> KeyEqualsPredicate(string key)
          {
              return p => p.Key.Equals(key, ThisStringComparison);
          }
       
          public IEnumerable<string> Keys 
          {
              get
              {
                  return _parameters.Select(p => p.Key);
              }
          }
       
          public IEnumerable<string> DistinctKeys 
          {
              get
              {
                  return ParametersWithDistinctKeys.Select(p => p.Key);
              }
          }
       
          public IEnumerable<Parameter> ParametersWithDistinctKeys
          {
              get
              {
                  return _parameters.GroupBy(k => k.Key, ThisEqualityComparer).Select(k => k.First());
              }
          }
       
          public bool HasKey(string key)
          {
              return GetParameters(key).Any();
          }
       
          public bool HasKeyAndValue(string key)
          {
              var parameter = GetFirstParameterOrDefault(key);
              return parameter != null && parameter.HasValue;
          }
       
          public bool HasKeyAndNoValue(string key) 
          {
              var parameter = GetFirstParameterOrDefault(key);
              return parameter != null && !parameter.HasValue;
          }
       
          private IEqualityComparer<string> ThisEqualityComparer
          {
              get
              {
                  return _caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
              }
          }
       
          private StringComparison ThisStringComparison
          {
              get
              {
                  return _caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
              }
          }
       
          public bool HasHelpKey
          {
              get
              {   
                  return HelpParameters.Any(h => 
                      _parameters.Any(p=> p.Key.Equals(h, StringComparison.OrdinalIgnoreCase)));
              }
          }
       
          public static IEnumerable<string> HelpParameters
          {
              get
              {
                  return new[] { "?", "help", "-?", "/?", "-help", "/help" };
              }
          }
       
          private static string GetAllParametersText()
          {
              var everything = Environment.CommandLine;
              var executablePath = Environment.GetCommandLineArgs()[0];
       
              var result = everything.StartsWith("\"") ?
                  everything.Substring(executablePath.Length + 2) :
                  everything.Substring(executablePath.Length);
              result = result.TrimStart(' ');
              return result;
          }
       
          public IEnumerator<Parameter> GetEnumerator()
          {
              return _parameters.GetEnumerator();
          }
       
          IEnumerator IEnumerable.GetEnumerator()
          {
              return GetEnumerator();
          }
       
          public Parameter this[int index]
          {
              get
              {
                  return _parameters[index];
              }
          }
       
          public int Count
          {
              get
              {
                  return _parameters.Count;
              }
          }
      }
      

The Parameter class that provides you the properties of a parameter:


      public class Parameter
      {
          public int Index { get; private set; }
          private readonly IEnumerable<CharContext> _charContexts;
               
          internal Parameter(IEnumerable<CharContext> charContexts, int index)
          {
              Index = index;
              _charContexts = charContexts;
          }
       
          public override string ToString()
          {
              return Bruto;
          }
       
          // Including quotes
          public string Bruto
          {
              get
              {
                  var charInfos = _charContexts.Select(c => c.Value);
                  return new string(charInfos.ToArray());
              }
          }
       
          // Excluding quotes
          public string Netto
          {
              get
              {
                  var charInfos = _charContexts.Where(c => c.IsNetto).Select(c => c.Value);
                  return new string(charInfos.ToArray());
              }
          }
       
          public string Key
          {
              get
              {
                  if (!HasValue)
                  {
                      return Netto;
                  }
                  var valueChars = _charContexts.Take(IndexOfKeyValueSplitter)
                      .Where(c => c.IsNetto)
                      .Select(v => v.Value);
                  var result = new string(valueChars.ToArray());
                  return result;
              }
          }
       
          public bool HasValue 
          {
              get
              {
                  return IndexOfKeyValueSplitter > -1;
              }
          }
       
          public string Value
          {
              get
              {
                  if (! HasValue)
                  {
                      return null;
                  }
                  var valueChars = _charContexts.Skip(IndexOfKeyValueSplitter + 1)
                      .Where(c => c.IsNetto)
                      .Select(v => v.Value);
                  var result = new string(valueChars.ToArray());
                  return result;
              }
          }
       
          private int IndexOfKeyValueSplitter
          {
              get
              {
                  for (var index = 0; index < _charContexts.Count(); index++)
                  {
                      var charContext = _charContexts.ElementAt(index);
                      if (charContext.IsKeyValueSplitter)
                      {
                          return index;
                      }
                  }
                  return -1;
              }
          }
       
      }
      

The BareParametersParser class that does the parsing:


      internal class BareParametersParser
      {
          private readonly char _keyValuesplitter;
          private readonly string _text;
       
          public BareParametersParser(string text, char keyValuesplitter = '=')
          {
              _keyValuesplitter = keyValuesplitter;
              _text = text.Trim();
          }
       
          private IEnumerable<CharContext> CharContexts
          {
              get
              {
                  var enumerator = _text.GetEnumerator();
       
                  // go to the first char
                  if (!enumerator.MoveNext())
                      yield break;
       
                  CharContext previous = null;
                  char value = enumerator.Current;
       
                  //  Continue with the second char
                  while (enumerator.MoveNext())
                  {
                      var next = new CharContext(enumerator.Current, _keyValuesplitter);
                      var context = new CharContext(value, _keyValuesplitter)
                                      {
                                          Previous = previous,
                                          Next = next
                                      };
                      yield return context;
       
                      previous = context;
                      value = next.Value;
                  }
       
                  // Return the last char
                  var last = new CharContext(value, _keyValuesplitter)
                                  {
                                      Previous = previous,
                                      Next = null
                                  };
                  yield return last;
              }
          }
       
          public IEnumerable<Parameter> Parameters
          {
              get
              {
                  var parameterChars = new List<CharContext>();
                  var index = 0;
                  foreach (var charContext in CharContexts)
                  {
                      if (!charContext.IsBetweenParameters)
                      {
                          parameterChars.Add(charContext);
                      }
                      if (charContext.IsFirstBetweenParameters && parameterChars.Any())
                      {
                          yield return new Parameter(parameterChars, index);
                          parameterChars = new List<CharContext>();
                          index++;
                      }
       
                  }
                  if (parameterChars.Any())
                  {
                      yield return new Parameter(parameterChars, index);
                  }
              }
          }
      }
      

and a class that helps to provide extra (context) information about the chars used in the parameters text:


      internal class CharContext
      {
          private readonly char _keyValuesplitter;
       
          public CharContext(char value, char keyValuesplitter = '=')
          {
              _keyValuesplitter = keyValuesplitter;
              Value = value;
              _isBetweenQuotes = new Lazy<bool>(GetIsBetweenQuotes);
          }
       
          public CharContext Previous { get; set; }
          public CharContext Next { get; set; }
          public char Value { get; private set; }
       
          private readonly Lazy<bool> _isBetweenQuotes;
          private bool IsBetweenQuotes
          {
              get
              {
                  return _isBetweenQuotes.Value;
              }
          }
       
          private bool GetIsBetweenQuotes()
          {
              if (Previous == null) return false;
              if (Value != '"') return Previous.IsBetweenQuotes;
              if (IsToEscape || IsEscapedQuote) return Previous.IsBetweenQuotes;
              return !Previous.IsBetweenQuotes;
          }
       
          private bool UnEscapedQuote
          {
              get
              {
                  if (Value != '"') return false;
                  if (Previous == null) return true;
                  return !Previous.IsToEscape;
              }
          }
       
          private bool IsToEscape
          {
              get
              {
                  if (Previous == null ||
                      Next == null ||
                      Value != '"' ||
                      Next.Value != '"') return false;
                  return !Previous.IsToEscape;
              }
          }
       
          private bool IsEscapedQuote
          {
              get
              {
                  if (Previous == null ||
                      Value != '"') return false;
                  return Previous.IsToEscape;
              }
          }
       
          public bool IsNetto
          {
              get
              {
                  return !(IsToEscape || IsBetweenParameters || UnEscapedQuote);
              }
          }
       
          public bool IsBetweenParameters
          {
              get
              {
                  return Value == ' ' && !IsBetweenQuotes;
              }
          }
       
          public bool IsFirstBetweenParameters
          {
              get
              {
                  return IsBetweenParameters && !Previous.IsBetweenParameters;
              }
          }
       
          public bool IsKeyValueSplitter
          {
              get
              {
                  return Value == _keyValuesplitter && !IsBetweenQuotes;
              }
          }
       
          public override string ToString() // Makes debugging easier
          {
              return Value.ToString();
          }
      }
      

Happy parsing ;-)

Leave a Comment

Comment

Comments

C# CSharp Blog Comment

Chris 03-12-2020 / Reply

Nice code... I do have a question as to why you didn't strip the '-' character off the Key... ex. parameters.GetFirstValue("test") fails but parameters.GetFirstValue("-test") works.