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 ;-)
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.