C# Command Line Argument Parser

I know this has been done to death but nothing I found did the job for me so I started with the one that fitted my needs the most then edited from there.

Original source: http://www.codeproject.com/KB/recipes/command_line.aspx

I have a few specific requirements:

  • It must support lists, or the same argument specified multiple times. If an argument has comma’s in it then it will be treated as a list and split on the comma.
  • It must support paths with a trailing \ ie. –arg:”c:\Users\ginnivanj\My Path\”
  • Has support for flags.

Class Diagram

Example Usage

There are two ways you can start using this class, I have created a SplitCommandLine function which ignores escaped quotes, this is needed for path support, the trailing \ on the path causes the quote to be taken literally.

Using SplitCommandLine:

var commandLine = Environment.CommandLine;
var splitCommandLine = Arguments.SplitCommandLine(commandLine);

var arguments = new Arguments(splitCommandLine);

Letting windows do it:

static int Main(string[] args)
{
    _args = new Arguments(args);
}

Example Arguments

Argument: –flag
Usage: args.IsTrue("flag");
Result: true

Argument: –arg:MyValue
Usage: args.Single("arg");
Result: MyValue

Argument: –arg "My Value"
Usage: args.Single("arg");
Result: ‘My Value’

Argument: /arg=Value /arg=Value2
Usage: args["arg"]
Result: new string[] {"Value", "Value2"}

Argument: /arg="Value,Value2"
Usage: args["arg"]
Result: new string[] {"Value", "Value2"}

As you can see it is very flexible, it support [–/]arg[:=]value and with the list support it makes it really useful and adaptable.

I have tried to cover as many of the different options with unit tests to make sure it is robust.

Arguments Class

/// <summary>
/// Arguments class
/// </summary>
public class Arguments
{ 
    /// <summary>
    /// Splits the command line. When main(string[] args) is used escaped quotes (ie a path "c:\folder\")
    /// Will consume all the following command line arguments as the one argument. 
    /// This function ignores escaped quotes making handling paths much easier.
    /// </summary>
    /// <param name="commandLine">The command line.</param>
    /// <returns></returns>
    public static string[] SplitCommandLine(string commandLine)
    {
        var translatedArguments = new StringBuilder(commandLine);
        var escaped = false;
        for (var i = 0; i < translatedArguments.Length; i++)
        {
            if (translatedArguments[i] == '"')
            {
                escaped = !escaped;
            }
            if (translatedArguments[i] == ' ' && !escaped)
            {
                translatedArguments[i] = '\n';
            }
        }

        var toReturn = translatedArguments.ToString().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
        for (var i = 0; i < toReturn.Length; i++)
        {
            toReturn[i] = RemoveMatchingQuotes(toReturn[i]);
        }
        return toReturn;
    }

    public static string RemoveMatchingQuotes(string stringToTrim)
    {
        var firstQuoteIndex = stringToTrim.IndexOf('"');
        var lastQuoteIndex = stringToTrim.LastIndexOf('"');
        while (firstQuoteIndex != lastQuoteIndex)
        {
            stringToTrim = stringToTrim.Remove(firstQuoteIndex, 1);
            stringToTrim = stringToTrim.Remove(lastQuoteIndex - 1, 1); //-1 because we've shifted the indicies left by one
            firstQuoteIndex = stringToTrim.IndexOf('"');
            lastQuoteIndex = stringToTrim.LastIndexOf('"');
        }

        return stringToTrim;
    }

    private readonly Dictionary<string, Collection<string>> _parameters;
    private string _waitingParameter;

    public Arguments(IEnumerable<string> arguments)
    {
        _parameters = new Dictionary<string, Collection<string>>();

        string[] parts;

        //Splits on beginning of arguments ( - and -- and / )
        //And on assignment operators ( = and : )
        var argumentSplitter = new Regex(@"^-{1,2}|^/|=|:",
            RegexOptions.IgnoreCase | RegexOptions.Compiled);

        foreach (var argument in arguments)
        {
            parts = argumentSplitter.Split(argument, 3);
            switch (parts.Length)
            {
                case 1:
                    AddValueToWaitingArgument(parts[0]);
                    break;
                case 2:
                    AddWaitingArgumentAsFlag();

                    //Because of the split index 0 will be a empty string
                    _waitingParameter = parts[1];
                    break;
                case 3:
                    AddWaitingArgumentAsFlag();

                    //Because of the split index 0 will be a empty string
                    string valuesWithoutQuotes = RemoveMatchingQuotes(parts[2]);

                    AddListValues(parts[1], valuesWithoutQuotes.Split(','));
                    break;
            }
        }

        AddWaitingArgumentAsFlag();
    }

    private void AddListValues(string argument, IEnumerable<string> values)
    {
        foreach (var listValue in values)
        {
            Add(argument, listValue);
        }
    }

    private void AddWaitingArgumentAsFlag()
    {
        if (_waitingParameter == null) return;

        AddSingle(_waitingParameter, "true");
        _waitingParameter = null;
    }

    private void AddValueToWaitingArgument(string value)
    {
        if (_waitingParameter == null) return;

        value = RemoveMatchingQuotes(value);

        Add(_waitingParameter, value);
        _waitingParameter = null;
    }

    /// <summary>
    /// Gets the count.
    /// </summary>
    /// <value>The count.</value>
    public int Count
    {
        get
        {
            return _parameters.Count;
        }
    }

    /// <summary>
    /// Adds the specified argument.
    /// </summary>
    /// <param name="argument">The argument.</param>
    /// <param name="value">The value.</param>
    public void Add(string argument, string value)
    {
        if (!_parameters.ContainsKey(argument))
            _parameters.Add(argument, new Collection<string>());

        _parameters[argument].Add(value);
    }

    public void AddSingle(string argument, string value)
    {
        if (!_parameters.ContainsKey(argument))
            _parameters.Add(argument, new Collection<string>());
        else
            throw new ArgumentException(string.Format("Argument {0} has already been defined", argument));

        _parameters[argument].Add(value);
    }

    public void Remove(string argument)
    {
        if (_parameters.ContainsKey(argument))
            _parameters.Remove(argument);
    }

    /// <summary>
    /// Determines whether the specified argument is true.
    /// </summary>
    /// <param name="argument">The argument.</param>
    /// <returns>
    ///     <c>true</c> if the specified argument is true; otherwise, <c>false</c>.
    /// </returns>
    public bool IsTrue(string argument)
    {
        AssertSingle(argument);

        var arg = this[argument];

        return arg != null && arg[0].Equals("true", StringComparison.OrdinalIgnoreCase);
    }

    private void AssertSingle(string argument)
    {
        if (this[argument] != null && this[argument].Count > 1)
            throw new ArgumentException(string.Format("{0} has been specified more than once, expecting single value", argument));
    }

    public string Single(string argument)
    {
        AssertSingle(argument);

        //only return value if its NOT true, there is only a single item for that argument
        //and the argument is defined
        if (this[argument] != null && !IsTrue(argument))
            return this[argument][0];

        return null;
    }

    public bool Exists(string argument)
    {
        return (this[argument] != null && this[argument].Count > 0);
    }

    /// <summary>
    /// Gets the <see cref="System.Collections.ObjectModel.Collection&lt;T&gt;"/> with the specified parameter.
    /// </summary>
    /// <value></value>
    public Collection<string> this[string parameter]
    {
        get
        {
            return _parameters.ContainsKey(parameter) ? _parameters[parameter] : null;
        }
    }
}

Unit Tests

Tests use xUnit as the unit testing framework

public class ArgumentsTests
{
    [Fact]
    public void ArgumentBooleanTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-testBool"
                                       };
        var target = new Arguments(args);
        Assert.True(target.IsTrue("testBool"));
    }

    [Fact]
    public void IsTrueDoesntExist()
    {
        IEnumerable<string> args = new string[]{};
        var target = new Arguments(args);
        Assert.False(target.IsTrue("doesntExist"));
    }

    [Fact]
    public void ArgumentDoubleDashesTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "--testArg=Value"
                                       };
        var target = new Arguments(args);
        Assert.Equal("Value", target.Single("testArg"));
    }

    [Fact]
    public void ArgumentSingleTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value"
                                       };
        var target = new Arguments(args);
        Assert.Equal(1, target["test"].Count);
        Assert.Equal("Value", target.Single("test"));
    }

    [Fact]
    public void ArgumentWithSpaceSeparatorTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-test Value");

        var target = new Arguments(args);
        Assert.Equal(1, target["test"].Count);
        Assert.Equal("Value", target.Single("test"));
    }

    [Fact]
    public void ArgumentWithSpaceSeparatorAndSpaceInValueTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-test \"Value With Space\"");

        var target = new Arguments(args);
        Assert.Equal(1, target["test"].Count);
        Assert.Equal("Value With Space", target.Single("test"));
    }

    [Fact]
    public void AddWaitingAsFlagTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-flag -test \"Value With Space\"");

        var target = new Arguments(args);
        Assert.Equal(2, target.Count);
        Assert.Equal(1, target["test"].Count);
        Assert.Equal("Value With Space", target.Single("test"));
        Assert.True(target.IsTrue("flag"));
    }

    [Fact]
    public void AddSingleTwiceTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-flag");

        var target = new Arguments(args);

        Assert.Throws<ArgumentException>(() => target.AddSingle("flag", "true"));
    }

    [Fact]
    public void FlagsTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-flag1 -flag2");

        var target = new Arguments(args);

        Assert.True(target.IsTrue("flag1"));
        Assert.True(target.IsTrue("flag2"));
    }

    [Fact]
    public void RemoveTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-flag1 -flag2");

        var target = new Arguments(args);

        Assert.True(target.IsTrue("flag1"));
        Assert.True(target.IsTrue("flag2"));
        target.Remove("flag1");
        Assert.False(target.Exists("flag1"));
        Assert.True(target.IsTrue("flag2"));
    }

    [Fact]
    public void SingleReturnsNullIfNotDefinedTest()
    {

        var target = new Arguments(new string[]{});

        Assert.False(target.Exists("notDefined"));
        Assert.Null(target.Single("notDefined"));
    }

    [Fact]
    public void ExistsTest()
    {
        IEnumerable<string> args = Arguments.SplitCommandLine("-flag1");

        var target = new Arguments(args);

        Assert.True(target.Exists("flag1"));
    }

    [Fact]
    public void ArgumentListTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           "-test:Value2"
                                       };
        var target = new Arguments(args);
        Assert.Equal(2, target["test"].Count);
        Assert.Equal("Value", target["test"][0]);
        Assert.Equal("Value2", target["test"][1]);
    }

    [Fact]
    public void ArgumentPathTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           @"-test:C:\Folder\"
                                       };
        var target = new Arguments(args);
        Assert.Equal(2, target["test"].Count);
        Assert.Equal("Value", target["test"][0]);
        Assert.Equal(@"C:\Folder\", target["test"][1]);
    }

    [Fact]
    public void ArgumentQuotedPathTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           "-test:\"C:\\Folder\\\""
                                       };
        var target = new Arguments(args);
        Assert.Equal(2, target["test"].Count);
        Assert.Equal("Value", target["test"][0]);
        Assert.Equal("C:\\Folder\\", target["test"][1]);
    }

    [Fact]
    public void ArgumentQuotedPathWithSpaceTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           "-test:\"C:\\Folder Name\\\""
                                       };
        var target = new Arguments(args);
        Assert.Equal(2, target["test"].Count);
        Assert.Equal("Value", target["test"][0]);
        Assert.Equal("C:\\Folder Name\\", target["test"][1]);
    }

    [Fact]
    public void ArgumentQuotedPathWithSpaceAndFollowingArgTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           "-test:\"C:\\Folder Name\\\"",
                                           "-testPath:\"C:\\Folder2\\\"",
                                           "-boolArg"
                                       };

        var target = new Arguments(args);
        Assert.Equal(2, target["test"].Count);
        Assert.Equal(@"C:\Folder2\", target.Single("testPath"));
        Assert.True(target.IsTrue("boolArg"));

        Assert.Equal("Value", target["test"][0]);
        Assert.Equal("C:\\Folder Name\\", target["test"][1]);
    }

    [Fact]
    public void ArgumentListRequestSingleThrowsExceptionTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-test:Value",
                                           "-test:Value2"
                                       };

        var target = new Arguments(args);
        //Should throw Argument exception because test is defined more than once
        Assert.Throws<ArgumentException>(() => target.Single("test"));
    }

    [Fact]
    public void ArgumentCommaListTest()
    {
        IEnumerable<string> args = new[]
                                       {
                                           "-testList:Value,Value2,Value3"
                                       };

        var target = new Arguments(args);
        Assert.Equal(3, target["testList"].Count);

        Assert.Equal("Value", target["testList"][0]);
        Assert.Equal("Value2", target["testList"][1]);
        Assert.Equal("Value3", target["testList"][2]);
    }

    [Fact]
    public void BlogExample()
    {
        const string commandLine = "-u -d -mdb=\"c:\\entries.mdb\" -xml=\"j:\\\"";

        var target = new Arguments(Arguments.SplitCommandLine(commandLine));

        Assert.Equal("c:\\entries.mdb", target.Single("mdb"));
        Assert.Equal("j:\\", target.Single("xml"));
    }
}
.net c# command-line
Posted by: Jake Ginnivan
Last revised: 19 Feb, 2011 06:29 AM

Trackbacks

Comments

Oliver
Oliver
07 Apr, 2011 11:49 AM

Hi Jake

This C# command line parser is great stuff. Here some points:

  • Please offer a downloadable arguments.cs file as pasting the code from HTML to Visual Studio gives a One-Liner instead of structured code

  • Please add the namespace definitions necessary to make it work

using System; using System.Collections.Generic; using System.Collections.ObjectModel; // Yes, "Collection" has been moved... using System.Text.RegularExpressions; using System.Text;

Regards, Oliver

Dave
Dave
18 Apr, 2011 06:41 PM

Thanks very much for this. I couldn't find anything that worked for me, either. Until finding your example.

To further customize it, I made the following modifications:

I added a constructor overload to the Arguments class:

public Arguments(IEnumerable<string> arguments)
    : this(arguments, false)
{
}

And modified the original constructor to:

public Arguments(IEnumerable<string> arguments, bool ignoreCase)

And finally, modified the declaration for the Dictionary Class in the original constructor to:

if (ignoreCase)
    _parameters = new Dictionary<string, Collection<string>>(StringComparer.OrdinalIgnoreCase);
else
    _parameters = new Dictionary<string, Collection<string>>();

I'm new to C#, so I hope this is the proper way to implement this.

Thanks again.

Jos
Jos
12 Jul, 2011 07:47 AM

Great i used the same base class. I also had added the suggestion from Dave.

Added some extra code if (CaseSensitive == true) { Spliter = new Regex(@"^-{1}|^/", RegexOptions.Compiled); _parameters = new Dictionary>(StringComparer.Ordinal); } else { Spliter = new Regex(@"^-{1}|^/", RegexOptions.IgnoreCase | RegexOptions.Compiled); _parameters = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); }

MarkLTX
MarkLTX
10 Aug, 2011 03:02 PM

Oliver is right that copying the class text from Explorer into a file produces a giant one-line comment. I got around this by using "View Source" in the browser and copying the code from there. Then I had to replace "<" with '<' and so on for '<', '>', and '&'.

Mike B.
Mike B.
28 Sep, 2011 07:35 PM

One bug I encountered: if your argument includes a backslash, you cannot use argumentvariable. You have to use argument:variable. For example:

myprogram -f myfile.txt (doesn't work)
myprogram -f:myfile.txt (does work)

Your Comments

Used for your gravatar. Not required. Will not be public.
Posting code? Indent it by four spaces to make it look nice. Learn more about Markdown.

Preview