Contents

What? Yet another argument parsing class?! What for? Well, actually for a couple of reasons:

Supports many argument types:

Switches (eg. /foo, /foo=1, /foo=true [localized])

Named/unnamed flags (eg. -raFl, -aABCDEF). Flags are "type-safe" in the sense that you specify which values are accepted. For example, if the user writes -raFlW and 'W' is not an accepted flag, then an ArgumentFormatException is raised.

You can specify a single member, a class, a type (for static members) or an assembly.

One argument can set many members at once, even if located in different types.

Supports globalization through custom attributes (ie you provide a ResourceManager, a CultureInfo and a resource ID and the argument name/alias will be automatically updated according to resource file). This works for switches, named values and flags.

Keeps track of handled and unhandled arguments.

All that in less than 400 lines of code. :)

I want to thank Ray Hayes for his idea of automatically setting field/property by judiciously using custom attributes. This is a great example of that proverbial 1% of inspiration. :) Take a look at his article here.

This is an example quickly showing how you might use the class, more or less taken from the included demo, which by the way is a utility I posted some time ago on this site. You can read the article here.

using Common;
using Common.IO;
using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
namespace xmove
{
class XMove
{
privatestatic System.Resources.ResourceManager m_resMan;
// this argument can either be named "batch", "file", "batchFile"// or even "smurf" if you fancy it
[AutoSetMember("batch", "file", "batchFile", "smurf")]
privatestaticString m_batchFile;
[AutoSetMember("yc", SwitchMeansFalse=true)]
privatestaticbool m_confirmMove = true;
[AutoSetMember("yc", SwitchMeansFalse=true)]
privatestaticbool m_confirmOverwrite = true;
[AutoSetMember("r")]
privatestaticbool m_recursive = false;
privatestatic FindFile.SearchAttributes m_searchAttributes;
[STAThread]
staticvoid Main(string[] args)
{
// a FlagCollection contains all accepted flags
FlagCollection flags = new FlagCollection();
// here we add the flag named "a" with accepted values// a,c,d,h,...// the argument must then looks like "-a<VALUES>" on the command// line
flags.Add("a", "acdehnorstFA");
// create a new parser that accept all argument formats,// is case sensitive and accept provided flags
ArgumentParser parser = new ArgumentParser(ArgumentFormats.All,
false, flags);
// parse the arguments with the result stored in a// StringDictionary
StringDictionary theArgs = parser.Parse(args);
// automatically set all static members of the class XMove
parser.AutoSetMembers(typeof(XMove));
// now, the values of m_batchFile, m_confirmMove,// m_confirmOverwrite and m_recursive are all set !// the corresponding arguments have been moved to// parser.HandledArguments// the remaining arguments are in parser.UnhandledArguments// one argument remains which can't be set automaticallyif (theArgs.ContainsKey("a"))
{
String attribs = theArgs[m_resMan.GetString("app0019")];
if (attribs.IndexOf("A") > -1)
m_searchAttributes |= FindFile.SearchAttributes.All;
if (attribs.IndexOf("F") > -1)
m_searchAttributes |= FindFile.SearchAttributes.AnyFile;
.........
}
// here we load the resources for the application
LoadResources();
// we then set the resource manager of the// AutoSetMemberAttribute accordingly// why this property is static is explained in the// "How it Works" section of this article
AutoSetMemberAttribute.Resources = m_resMan;
// create a new instance of Dummy class
Dummy dummy = new Dummy();
// then set members of this class, no matter if they are// static, instance, public, private, protected, internal// type conversion is done automatically !
parser.AutoSetMembers(dummy);
}
privatevoid LoadResources()
{
....
}
}
class Dummy
{
publicenum MyEnum
{
a,
b,
c,
d
}
[AutoSetMember("foo")]
privatestaticint field1;
// here we indicate that the argument name is to be retrieved using// the ResourceManager at runtime
[AutoSetMember(ResID="0001")]
privateDouble field2;
// same here
[AutoSetMember(ResID="0002")]
protectedString Property1
{
get {...}
set {...}
}
[AutoSetMember("buzz")]
public MyEnum Property2
{
get {...}
set {...}
}
}
}

public string CustomPattern [get, set]An additional or overriding pattern. In the pattern, use capture name constants made public by this class (ArgumentNameCaptureName, ArgumentValueCaptureName, FlagNameCaptureName, FlagsCaptureName and PrefixCaptureName).

public StringDictionary HandledArguments [get]The argument(s) that have been automatically set by AutoSetMembers method.

public StringDictionary UnhandledArguments [get]The argument(s) that have not been automatically set by AutoSetMembers method.

Comments and Discussions

If I run a command like "mycommand somearg" with no option switches, then I get zero handled or unhandled arguments, yet if I add an option switch, then the "somearg" becomes one of the unhandled arguments. How am I supposed to fetch non option arguments?

For instance if were to run "mycommand /sw1 /sw1 arg1 arg2 /sw3=value", how should I fetch arg1 and arg2 that are not option switches and not values of an option switch?

Thanks Sebastian. I had thought that calling "parser.Parse" was optional and only needed if I wanted a StringDictionary and that if I called "parser.AutoSetMembers" that was sufficient.

Also, I found that the "parser = new ArgumentParser(ArgumentFormats.All, true)" form of the constructor failed with a null reference to flags and I had to use the form that included a flags argument even though it was an empty collection. Is that a minor bug?

Well, I have not done it before, but my guess would that you can assign the argument name "0", "1", etc. to a member and so AutoSetMembers will work as expected.

Seems like the code version on CP is older than the one I got in my depot. That article is quite old (by internet standards ), so I will check if I should update it or not. But yes, seems like the version currently on CP has a bug

Another feedback is that running "myprogram /sw1 arg1" where /sw1 is not valid, leaves both sw1 and arg1 in the UnHandledArguments collection as well as in the dictionary returned by parser.Parse(args).

Ideally all parsed arguments and switches would be unique amongst HandledArguments, UnhandledArguments and the theArgs StringDictionary. That would make it easier to test for valid switches, invalid switches and the proper number of plain arguments I think.

Is there proper way to test for invalid switches and the proper number of plain arguemnts that are not switches that I missed?

If expecting one plain argument arg1, I would like would like to test theArgs.Count==1 and to check if there were any invlaid switches, I would like to check UnHnadledArguments.Count > 0. Since sw1 and arg1 appear both in the theArgs dictionary and in UnHandledArguments I cannot see a way to do either check currently. Hope that made sense.

Ok, I took a longer look at the code I got here. Actually, I have the version on CP and another newer one. I read my notes about the latter, and it appears it was a work in progress which I decided was leading nowhere, so I will correct the CP version instead.

I get your point about validating arguments, but I am not sure how I could figure out if "/sw1 arg1" is a switch with a parameter or it is a standalone switch and a plain argument. I think putting them in the wrong place is worse than putting them at two places. Let me know if you have any idea how to differentiate both cases.

I will wait for your comments before I post a new version of the code.

Thanks Sebastian. Since you only support : and = as assignment operators, then I should think it would not be a problem to determine if a switch or regular argument. Of course, one could no longer use a blank as an assignment operator as you suggested to one user, but that is just as well in my opinion, since it could only work for one blank and not more than one blank. So /sw1 arg1 would be a switch and a plain argument and /sw1=arg1 or /sw1:arg1 would be a switch with a value and no plain argument. Dave