Ultimate command line arguments parsing: query with Linq

by Alex Siepman 26. March 2014 14:05

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

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About the author

I am a software architect at Roxit and also a C# Developer. My main interests in the area of ​​C# are LINQ and generics

Visit my personal homepage (Dutch) for more info about me.

Month List

Page List