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.
- Named/unnamed values (eg. /foo=bar, "foo")
- Any prefix(es) you specify (eg. -foo, /foo, \foo, ...)
- Any assignment symbol(s) (eg. /foo=bar, /foo:bar, ...)
- Any additional or overriding pattern you provide
- Automatically sets field/property values according to arguments provided using custom attributes.
- All value types are supported, including enumerations.
- 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
{
private static System.Resources.ResourceManager m_resMan;
[AutoSetMember("batch", "file", "batchFile", "smurf")]
private static String m_batchFile;
[AutoSetMember("yc", SwitchMeansFalse=true)]
private static bool m_confirmMove = true;
[AutoSetMember("yc", SwitchMeansFalse=true)]
private static bool m_confirmOverwrite = true;
[AutoSetMember("r")]
private static bool m_recursive = false;
private static FindFile.SearchAttributes m_searchAttributes;
[STAThread]
static void Main(string[] args)
{
FlagCollection flags = new FlagCollection();
flags.Add("a", "acdehnorstFA");
ArgumentParser parser = new ArgumentParser(ArgumentFormats.All,
false, flags);
StringDictionary theArgs = parser.Parse(args);
parser.AutoSetMembers(typeof(XMove));
if (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;
.........
}
LoadResources();
AutoSetMemberAttribute.Resources = m_resMan;
Dummy dummy = new Dummy();
parser.AutoSetMembers(dummy);
}
private void LoadResources()
{
....
}
}
class Dummy
{
public enum MyEnum
{
a,
b,
c,
d
}
[AutoSetMember("foo")]
private static int field1;
[AutoSetMember(ResID="0001")]
private Double field2;
[AutoSetMember(ResID="0002")]
protected String Property1
{
get {...}
set {...}
}
[AutoSetMember("buzz")]
public MyEnum Property2
{
get {...}
set {...}
}
}
}
Two main classes do all the work: ArgumentParser
and AutoSetMemberAttribute
. As usual, contructors let you directly set one or more properties, so I will skip them.
public char[] AllowedPrefixes [get, set]
The accepted prefix(es).
public ArgumentFormats ArgumentFormats [get, set]
The accepted argument format(s).
public char[] AssignSymbols [get, set]
The accepted assignation symbol(s).
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.
public bool UseOnlyCustomPattern [get, set]
Indicates if the custom pattern provided is overriding the internal pattern automatically generated.
public StringDictionary Parse(string[] args)
Parses the array of arguments and returns the dictionary of parsed arguments
UnhandledArgument
property is also updated accordingly.
public void AutoSetMembers(Assembly assembly)
public void AutoSetMembers(Type type)
public void AutoSetMembers(object instance)
public void AutoSetMembers(object classToProcess, MemberInfo member)
Automatically sets member(s) of the provided assembly, type, class instance. Also works for a single field/property.
public void Clear()
Clears all saved arguments (both handled and unhandled).
private void BuildPattern()
Builds the pattern to be used when parsing each argument.
private void SetMemberValue(object instance, MemberInfo memberInfo, object value)
Set the static or instance member to the specified value.
public static CultureInfo Culture [get, set]
The culture to be used for retrieving culture aware aliases.
public static ResourceManager Resources [get, set]
The resource manager to be used for retrieving culture aware aliases.
public ArrayList Aliases [get]
Command line argument's name or aliases if many names are possible.
public string Description [get, set]
The description of the command line argument.
public object ID [get, set]
An ID (can be anything you want).
public string ResID [get, set]
The resource ID to be used when retrieving culture aware aliases.
public bool SwitchMeansFalse [get, set]
Indicates if the meaning of a switch is false instead of true as usual.
How it Works
Here is a quick explanation of the regex that is constructed by ArgumentParser.BuildPattern()
private method. Variables are indicated by capitalized names and are to be replaced at runtime.
// The whole parsing string (with all possible argument formats) :
// ---------------------------------------------------------------
// (CUSTOM_PATTERN)
// |(^(?[PREFIXES])(?<FLAGNAME>)FLAG_NAMES)(?<FLAGS>[FlagsCaptureName]+)$)
// |(^(?[PREFIXES])(?<NAME>[^EQUAL_SYMBOLS]+)([EQUAL_SYMBOLS](?<VALUE>.+))?$)
// |(LITERAL_STRING_SYMBOL?(?<VALUE>.*))
//
// Again, but commented :
// ----------------------
// (CUSTOM_PATTERN)| # custom pattern, if any (it has priority over
// # standard pattern)
//
// foreach flag in FlagCollection :
//
// (^
// (?[PREFIXES]) # mandatory prefix
// (?<FLAGNAME>)FLAG_NAMES) # flag name
// (?<FLAGS>[FlagsCaptureName]+) # flag value
// $)|
//
// (^
// (?[PREFIXES]) # mandatory prefix
// (?<NAME>[^EQUAL_SYMBOLS]+) # argument name (which includes flag
// # name/values)
// ([EQUAL_SYMBOLS](?<VALUE>.+))? # argument value, if any
// $)
//
// |(
// LITERAL_STRING_SYMBOL? # optional @ caracter indicating literal string
// (?<VALUE>.*) # standalone value (will be given an index when
// # parsed in Parse() method)
// )
At design time, AutoSetMemberAttribute
custom attribute is used to specify which argument will be used to set the affected member. Other informations include description, resource ID and an ID that you might use at your convenience.
At runtime, you can set the AutoSetMemberAtttribute
static properties Resource
and Culture
. It's necessary to make those properties static because attributes are serialized at compile time, so they can only have constant values as instance properties.
When you execute ArgumentParser.SetAutoMembers(...)
, the following occurs :
AutoSetMember
attributes will be located using reflection.
- If a resource ID is specified and a resource manager and a culture are provided, the localized argument name will be added as an alias.
- The argument associated to the member (if any) will be converted to the member's type
- The member's value will be updated using
ArgumentParser.SetMemberValue(...)
.
- The argument will be removed from
ArgumentParser.UnhandledArguments
and added to ArgumentParser.HandledArguments
.
public void AutoSetMembers(Assembly assembly)
{
Type[] types = assembly.GetTypes();
foreach (Type type in types)
AutoSetMembers(type);
}
public void AutoSetMembers(Type type)
{
MemberInfo[] members = type.FindMembers(
AutoSetMemberAttribute.SupportedMemberTypes,
AutoSetMemberAttribute.SupportedBindingFlags, Type.FilterName, "*");
foreach (MemberInfo member in members)
AutoSetMembers(type, member);
}
public void AutoSetMembers(object instance)
{
MemberInfo[] members = instance.GetType().FindMembers(
AutoSetMemberAttribute.SupportedMemberTypes,
AutoSetMemberAttribute.SupportedBindingFlags, Type.FilterName, "*");
foreach (MemberInfo member in members)
AutoSetMembers(instance, member);
}
public void AutoSetMembers(object classToProcess, MemberInfo member)
{
AutoSetMemberAttribute attrib = Attribute.GetCustomAttribute(member,
typeof(AutoSetMemberAttribute)) as AutoSetMemberAttribute;
if (attrib != null)
{
if (attrib.ResID != null && AutoSetMemberAttribute.Resources != null)
attrib.Aliases.Add(AutoSetMemberAttribute.Resources.GetString(
attrib.ResID, AutoSetMemberAttribute.Culture));
String argValue = null;
bool found = false;
foreach (String alias in attrib.Aliases)
{
if (m_unhandled.ContainsKey(alias))
{
argValue = (String)m_unhandled[alias];
m_handled.Add(alias, argValue);
m_unhandled.Remove(alias);
found = true;
break;
}
else if (m_handled.ContainsKey(alias))
{
argValue = (String)m_handled[alias];
found = true;
break;
}
}
if (found)
{
Type memberType = null;
switch (member.MemberType)
{
case MemberTypes.Property:
memberType = ((PropertyInfo)member).PropertyType;
break;
case MemberTypes.Field:
memberType = ((FieldInfo)member).FieldType;
break;
}
if (memberType == typeof(bool))
{
if (argValue == "")
SetMemberValue(classToProcess, member,
!attrib.SwitchMeansFalse);
else if (argValue == Boolean.FalseString ||
argValue == Boolean.TrueString)
SetMemberValue(classToProcess, member,
Boolean.Parse(argValue));
else
SetMemberValue(classToProcess, member,
Int32.Parse(argValue) != 0);
}
else if (memberType == typeof(String))
SetMemberValue(classToProcess, member, argValue);
else if (memberType.IsEnum)
{
object value = Enum.Parse(memberType, argValue,
m_ignoreCase);
SetMemberValue(classToProcess, member, value);
}
else if (memberType.IsValueType)
SetMemberValue(classToProcess, member,
Convert.ChangeType(argValue, memberType));
}
}
}
private void SetMemberValue(object instance, MemberInfo memberInfo,
object value)
{
if (memberInfo is PropertyInfo)
{
PropertyInfo pi = (PropertyInfo) memberInfo;
if (pi.CanWrite)
{
MethodInfo methodInfo = pi.GetSetMethod(true);
BindingFlags bindingFlags = BindingFlags.SetProperty;
if (methodInfo.IsStatic)
bindingFlags |= BindingFlags.Static;
pi.SetValue(instance, value, bindingFlags, null, null, null);
}
}
else if (memberInfo is FieldInfo)
{
FieldInfo fi = (FieldInfo) memberInfo;
BindingFlags bindingFlags = BindingFlags.SetField;
if (fi.IsStatic)
bindingFlags |= BindingFlags.Static;
fi.SetValue(instance, value, bindingFlags, null, null);
}
}