C# 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.
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);
}
Argument: –flag
Usage: args.IsTrue(“flag”);
Result: trueArgument: –arg:MyValue
Usage: args.Single(“arg”);
Result: MyValueArgument: –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[:=<space>]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
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<T>”/> with the specified parameter.
/// </summary>
/// <value></value>
public Collection<string> this[string parameter]
{
get
{
return _parameters.ContainsKey(parameter) ? _parameters[parameter] : null;
}
}
}
Unit Tests
The best way to understand how this class works is to have a look at the unit tests, if I am missing any functionality or I have missed something let me know!
[TestClass]
public class ArgumentsTests
{
public TestContext TestContext { get; set; }
/// <summary>
///A test for Arguments Constructor
///</summary>
[TestMethod]
public void ArgumentBooleanTest()
{
IEnumerable<string> args = new[]
{
“-testBool”
};
var target = new Arguments(args);
Assert.IsTrue(target.IsTrue(“testBool”));
}
[TestMethod]
public void IsTrueDoesntExist()
{
IEnumerable<string> args = new string[]{};
var target = new Arguments(args);
Assert.IsFalse(target.IsTrue(“doesntExist”));
}
[TestMethod]
public void ArgumentDoubleDashesTest()
{
IEnumerable<string> args = new[]
{
“–testArg=Value”
};
var target = new Arguments(args);
Assert.AreEqual(“Value”, target.Single(“testArg”));
}
[TestMethod]
public void ArgumentSingleTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”
};
var target = new Arguments(args);
Assert.AreEqual(1, target["test"].Count);
Assert.AreEqual(“Value”, target.Single(“test”));
}
[TestMethod]
public void ArgumentWithSpaceSeparatorTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-test Value”);
var target = new Arguments(args);
Assert.AreEqual(1, target["test"].Count);
Assert.AreEqual(“Value”, target.Single(“test”));
}
[TestMethod]
public void ArgumentWithSpaceSeparatorAndSpaceInValueTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-test \”Value With Space\”");
var target = new Arguments(args);
Assert.AreEqual(1, target["test"].Count);
Assert.AreEqual(“Value With Space”, target.Single(“test”));
}
[TestMethod]
public void AddWaitingAsFlagTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-flag -test \”Value With Space\”");
var target = new Arguments(args);
Assert.AreEqual(2, target.Count);
Assert.AreEqual(1, target["test"].Count);
Assert.AreEqual(“Value With Space”, target.Single(“test”));
Assert.IsTrue(target.IsTrue(“flag”));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void AddSingleTwiceTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-flag”);
var target = new Arguments(args);
target.AddSingle(“flag”, “true”);
}
[TestMethod]
public void FlagsTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-flag1 -flag2″);
var target = new Arguments(args);
Assert.IsTrue(target.IsTrue(“flag1″));
Assert.IsTrue(target.IsTrue(“flag2″));
}
[TestMethod]
public void RemoveTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-flag1 -flag2″);
var target = new Arguments(args);
Assert.IsTrue(target.IsTrue(“flag1″));
Assert.IsTrue(target.IsTrue(“flag2″));
target.Remove(“flag1″);
Assert.IsFalse(target.Exists(“flag1″));
Assert.IsTrue(target.IsTrue(“flag2″));
}
[TestMethod]
public void SingleReturnsNullIfNotDefinedTest()
{
var target = new Arguments(new string[]{});
Assert.IsFalse(target.Exists(“notDefined”));
Assert.IsNull(target.Single(“notDefined”));
}
[TestMethod]
public void ExistsTest()
{
IEnumerable<string> args = Arguments.SplitCommandLine(“-flag1″);
var target = new Arguments(args);
Assert.IsTrue(target.Exists(“flag1″));
}
[TestMethod]
public void ArgumentListTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”,
“-test:Value2″
};
var target = new Arguments(args);
Assert.AreEqual(2, target["test"].Count);
Assert.AreEqual(“Value”, target["test"][0]);
Assert.AreEqual(“Value2″, target["test"][1]);
}
[TestMethod]
public void ArgumentPathTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”,
@”-test:C:\Folder\”
};
var target = new Arguments(args);
Assert.AreEqual(2, target["test"].Count);
Assert.AreEqual(“Value”, target["test"][0]);
Assert.AreEqual(@”C:\Folder\”, target["test"][1]);
}
[TestMethod]
public void ArgumentQuotedPathTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”,
“-test:\”C:\\Folder\\\”"
};
var target = new Arguments(args);
Assert.AreEqual(2, target["test"].Count);
Assert.AreEqual(“Value”, target["test"][0]);
Assert.AreEqual(“C:\\Folder\\”, target["test"][1]);
}
[TestMethod]
public void ArgumentQuotedPathWithSpaceTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”,
“-test:\”C:\\Folder Name\\\”"
};
var target = new Arguments(args);
Assert.AreEqual(2, target["test"].Count);
Assert.AreEqual(“Value”, target["test"][0]);
Assert.AreEqual(“C:\\Folder Name\\”, target["test"][1]);
}
[TestMethod]
public void ArgumentQuotedPathWithSpaceAndFollowingArgTest()
{
IEnumerable<string> args = new[]
{
“-test:Value”,
“-test:\”C:\\Folder Name\\\”",
“-testPath:\”C:\\Folder2\\\”",
“-boolArg”
};
var target = new Arguments(args);
Assert.AreEqual(2, target["test"].Count);
Assert.AreEqual(@”C:\Folder2\”, target.Single(“testPath”));
Assert.IsTrue(target.IsTrue(“boolArg”));
Assert.AreEqual(“Value”, target["test"][0]);
Assert.AreEqual(“C:\\Folder Name\\”, target["test"][1]);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
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
target.Single(“test”);
}
[TestMethod]
public void ArgumentCommaListTest()
{
IEnumerable<string> args = new[]
{
“-testList:Value,Value2,Value3″
};
var target = new Arguments(args);
Assert.AreEqual(3, target["testList"].Count);
Assert.AreEqual(“Value”, target["testList"][0]);
Assert.AreEqual(“Value2″, target["testList"][1]);
Assert.AreEqual(“Value3″, target["testList"][2]);
}
\
July 30, 2009 - 9:36 pm
This code refuses to compile on my .NET 1.1 system. What version does it require?
July 31, 2009 - 10:03 am
.NET 3.5 probably, it may compile on .NET 3.0 but I am unsure, will look into it bit later.
August 15, 2009 - 8:54 am
The text formatting widget you use above really mangled the code. The quotes all had to be replaced and a couple < + > as well.
Additionally I had to use the following ‘using’ statements:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions
August 15, 2009 - 11:19 am
Thanks for the feedback, will find a new formatting widget and add in the using statements.
Cheers,
Jake
August 15, 2009 - 7:48 pm
Nice code but I can’t make it work …
Here the args I us:
-u -d -mdb “c:\entries.mdb” -xml “j:\”
In this way the result is:
Arguments CommandLine = new Arguments(args);
if (CommandLine.IsTrue(”mdb”)) mdb = CommandLine.Single(”mdb”); // mdb is true, Single return null
-u -d -mdb=”c:\entries.mdb” -xml=”j:\”
In this way the result is:
Arguments CommandLine = new Arguments(args);
if (CommandLine.IsTrue(”mdb”)) mdb = CommandLine.Single(”mdb”); // mdb is false
Do I missunderstood the usage of?
Thanks,
Andre
August 16, 2009 - 12:28 am
…and where are my manners!? Thanks for making the code available Jake!
August 16, 2009 - 3:45 pm
Hey Andre,
Thanks for that, looks like there is a error with spaces, ie first example.
The second issue is that IsTrue only checks to see if a flag is declared. So CommandLine.IsTrue(”u”) would return true.
Use if (CommandLine.Exists(”mdb”)) or if (CommandLine.Single(”mdb”) != null)
Hope this helps, will write some tests to get the first example with ‘-arg value’ working. Cheers
August 16, 2009 - 5:05 pm
Jake,
that helps at all and works fine.
For sure to use a space would be fine but it was more importent to make it work.
Thanks for your support.
Cheers
September 23, 2009 - 6:37 am
Great piece of code. Thanks for sharing.
One thing I stumbled upon, though, is that this code chokes on command line inputs that have URL formatting. For instance, if you want to pass in the address of a web service via the command line (let’s say, http://localhost/test/myservice.svc), the “/” get interpreted as split characters.
The easy work around is to remove the “/” character as a parameter option in the RegEx:
var argumentSplitter = new Regex(@”^-{1,2}|=|:”,…
There are more complex workarounds that could be done to enable “/” parameters -and- URL format input, but each approach has special considerations.
Hope this “nuance” helps others that may run in to this case.
-Todd
September 23, 2009 - 6:42 am
Actually – amend that. URL formatted inputs are broken with the original RegEx, but not due to the “/”. Rather, it’s due to the “:” in the URL. The following RegEx should solve the problem:
@”^-{1,2}|^/|=”
-Todd
September 23, 2009 - 9:31 am
Ah, that makes sense. Another possible solution would be to use the -url:http://localhost/test/myservice.svc format for the arguments. I have not tested though. I will be posting a follow up post with another argument library I found which is more strongly typed than this one. But this is great for flexible argument parsing.
Will let you know if I sort it out.
December 1, 2009 - 6:31 pm
Thanks for your code. I highly suggest you add a download zip, since copy-pasting doesn’t work (due to special characters like ` (single quote with direction) etc.)