Skip to content

Finish refactor of ArgumentDictionary for mixed short and long list arguments #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions examples/Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,8 @@ namespace Examples
/// <summary>
/// Provides an eval/print loop for command line argument strings.
/// </summary>
internal class Program
internal static class Program
{
#region Private Properties

/// <summary>
/// Gets or sets a value indicating whether show the help.
/// </summary>
[Argument('h', "help", "Gets or sets a value indicating whether show the help.")]
private static bool Help { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the Bool argument was supplied.
/// </summary>
Expand All @@ -30,6 +22,12 @@ internal class Program
[Argument('f', "float", "Gets or sets the value of the Double argument.")]
private static double Double { get; set; }

/// <summary>
/// Gets or sets a value indicating whether show the help.
/// </summary>
[Argument('h', "help", "Gets or sets a value indicating whether show the help.")]
private static bool Help { get; set; }

/// <summary>
/// Gets or sets the value of the Int argument.
/// </summary>
Expand All @@ -54,9 +52,16 @@ internal class Program
[Argument('s', "string")]
private static string String { get; set; }

#endregion Private Properties

#region Private Methods
/// <summary>
/// Returns a "pretty" string representation of the provided Type; specifically, corrects the naming of generic Types
/// and appends the type parameters for the type to the name as it appears in the code editor.
/// </summary>
/// <param name="type">The type for which the colloquial name should be created.</param>
/// <returns>A "pretty" string representation of the provided Type.</returns>
public static string ToColloquialString(this Type type)
{
return (!type.IsGenericType ? type.Name : type.Name.Split('`')[0] + "<" + String.Join(", ", type.GetGenericArguments().Select(a => a.ToColloquialString())) + ">");
}

/// <summary>
/// Application entry point
Expand Down Expand Up @@ -152,22 +157,22 @@ private static void Reset()
}

/// <summary>
/// Show help for arguments.
/// Show help for arguments.
/// </summary>
private static void ShowHelp()
{
var helpAttributes = Arguments.GetArgumentInfo(typeof(Program));

Console.WriteLine("Short\tLong\tFunction");
Console.WriteLine("-----\t----\t--------");
var maxLen = helpAttributes.Select(a => a.Property.PropertyType.ToColloquialString()).OrderByDescending(s => s.Length).FirstOrDefault().Length;

Console.WriteLine($"Short\tLong\t{"Type".PadRight(maxLen)}\tFunction");
Console.WriteLine($"-----\t----\t{"----".PadRight(maxLen)}\t--------");

foreach (var item in helpAttributes)
{
var result = item.ShortName + "\t" + item.LongName + "\t" + item.HelpText;
var result = item.ShortName + "\t" + item.LongName + "\t" + item.Property.PropertyType.ToColloquialString().PadRight(maxLen) + "\t" + item.HelpText;
Console.WriteLine(result);
}
}

#endregion Private Methods
}
}
204 changes: 109 additions & 95 deletions src/Utility.CommandLine.Arguments/Arguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,52 @@ public ArgumentAttribute(char shortName, string longName, string helpText = null
public char ShortName { get; set; }
}

/// <summary>
/// Encapsulates argument names and help text.
/// </summary>
public class ArgumentInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ArgumentInfo"/> class.
/// </summary>
/// <param name="shortName">The short name of the argument.</param>
/// <param name="longName">The long name of the argument.</param>
/// <param name="helpText">The help text for the argument.</param>
/// <param name="property">The property with which the argument is associated.</param>
public ArgumentInfo(char shortName, string longName, string helpText, PropertyInfo property)
{
ShortName = shortName;
LongName = longName;
HelpText = helpText;
Property = property;
}

/// <summary>
/// Gets the help text for the argument.
/// </summary>
public string HelpText { get; }

/// <summary>
/// Gets a value indicating whether the argument backing Type is a collection.
/// </summary>
public bool IsCollection => Property.PropertyType.IsArray || (Property.PropertyType.IsGenericType && Property.PropertyType.GetGenericTypeDefinition() == typeof(List<>));

/// <summary>
/// Gets the long name of the argument.
/// </summary>
public string LongName { get; }

/// <summary>
/// Gets the property with which the argument is associated.
/// </summary>
public PropertyInfo Property { get; }

/// <summary>
/// Gets the short name of the argument.
/// </summary>
public char ShortName { get; }
}

/// <summary>
/// Provides static methods used to retrieve the command line arguments and operands with which the application was
/// started, as well as a Type to contain them.
Expand Down Expand Up @@ -162,21 +208,25 @@ private Arguments(string commandLineString, List<KeyValuePair<string, string>> a
}

/// <summary>
/// Gets the target Type, if applicable.
/// Gets a dictionary containing the arguments and values specified in the command line arguments with which the
/// application was started.
/// </summary>
public Type TargetType { get; }
/// <remarks>
/// This dictionary contains argument key/value pairs compiled from the <see cref="ArgumentList"/> and checked against
/// the <see cref="TargetType"/> to combine duplicated pairs into lists where the backing property is a collection, and
/// to overwrite where the backing property is not a collection.
/// </remarks>
public Dictionary<string, object> ArgumentDictionary { get; }

/// <summary>
/// Gets the list of arguments specified in the command line arguments with which the application was started.
/// </summary>
/// <remarks>
/// This list contains each argument key/value pair as supplied in the original string, preserving the original order
/// and any duplicated pairs.
/// </remarks>
public List<KeyValuePair<string, string>> ArgumentList { get; }

/// <summary>
/// Gets a dictionary containing the arguments and values specified in the command line arguments with which the
/// application was started.
/// </summary>
public Dictionary<string, object> ArgumentDictionary { get; }

/// <summary>
/// Gets the command line string from which the arguments were parsed.
/// </summary>
Expand All @@ -187,6 +237,11 @@ private Arguments(string commandLineString, List<KeyValuePair<string, string>> a
/// </summary>
public List<string> OperandList { get; private set; }

/// <summary>
/// Gets the target Type, if applicable.
/// </summary>
public Type TargetType { get; }

/// <summary>
/// Gets the argument value corresponding to the specified <paramref name="index"/>.
/// </summary>
Expand All @@ -201,7 +256,8 @@ public object this[int index]
}

/// <summary>
/// Gets the argument value corresponding to the specified <paramref name="key"/> from the <see cref="ArgumentDictionary"/> property.
/// Gets the argument value corresponding to the specified <paramref name="key"/> from the
/// <see cref="ArgumentDictionary"/> property.
/// </summary>
/// <param name="key">The key for which the value is to be retrieved.</param>
/// <returns>The argument value corresponding to the specified key.</returns>
Expand All @@ -213,43 +269,6 @@ public object this[string key]
}
}

private static Dictionary<string, object> GetArgumentDictionary(List<KeyValuePair<string, string>> argumentList, Type targetType = null)
{
var dict = new ConcurrentDictionary<string, object>();
var argumentInfo = targetType == null ? new List<ArgumentInfo>() : GetArgumentInfo(targetType);

foreach (var arg in argumentList)
{
var info = argumentInfo.Where(i => i.ShortName.ToString() == arg.Key || i.LongName == arg.Key).SingleOrDefault();

if (info != default(ArgumentInfo))
{
bool added = false;

foreach (var k in new[] { info.ShortName.ToString(), info.LongName })
{
if (dict.ContainsKey(k))
{
dict.AddOrUpdate(k, arg.Value, (key, existingValue) => info.IsCollection ? ((List<object>)existingValue).Concat(new[] { arg.Value }).ToList() : (object)arg.Value);
added = true;
break;
}
}

if (!added)
{
dict.TryAdd(arg.Key, info.IsCollection ? new List<object>(new[] { arg.Value }) : (object)arg.Value);
}
}
else
{
dict.AddOrUpdate(arg.Key, arg.Value, (key, existingValue) => arg.Value);
}
}

return dict.ToDictionary(a => a.Key, a => a.Value);
}

/// <summary>
/// Retrieves a collection of <see cref="ArgumentInfo"/> gathered from properties in the target <paramref name="type"/>
/// marked with the <see cref="ArgumentAttribute"/><see cref="Attribute"/> along with the short and long names and help text.
Expand All @@ -268,13 +287,11 @@ private static Dictionary<string, object> GetArgumentDictionary(List<KeyValuePai

if (attribute != default(CustomAttributeData))
{
retVal.Add(new ArgumentInfo()
{
ShortName = (char)attribute.ConstructorArguments[0].Value,
LongName = (string)attribute.ConstructorArguments[1].Value,
HelpText = (string)attribute.ConstructorArguments[2].Value,
Type = property.PropertyType,
});
retVal.Add(new ArgumentInfo(
shortName: (char)attribute.ConstructorArguments[0].Value,
longName: (string)attribute.ConstructorArguments[1].Value,
helpText: (string)attribute.ConstructorArguments[2].Value,
property: property));
}
}

Expand Down Expand Up @@ -413,16 +430,7 @@ public static void Populate(Type type, Arguments arguments, bool clearExistingVa
}
else if (propertyType.IsArray || (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)))
{
// if the property is an array or list, convert the value to an array or list of the matching type. start
// by converting atomic values to a list containing a single value, just to simplify processing.
if (valueIsList)
{
convertedValue = value;
}
else
{
convertedValue = new List<object>(new object[] { value });
}
convertedValue = value;

// next, create a list with the same type as the target property
Type valueType;
Expand Down Expand Up @@ -526,6 +534,43 @@ private static void ClearProperties(Dictionary<string, PropertyInfo> properties)
}
}

private static Dictionary<string, object> GetArgumentDictionary(List<KeyValuePair<string, string>> argumentList, Type targetType = null)
{
var dict = new ConcurrentDictionary<string, object>();
var argumentInfo = targetType == null ? new List<ArgumentInfo>() : GetArgumentInfo(targetType);

foreach (var arg in argumentList)
{
var info = argumentInfo.Where(i => i.ShortName.ToString() == arg.Key || i.LongName == arg.Key).SingleOrDefault();

if (info != default(ArgumentInfo))
{
bool added = false;

foreach (var k in new[] { info.ShortName.ToString(), info.LongName })
{
if (dict.ContainsKey(k))
{
dict.AddOrUpdate(k, arg.Value, (key, existingValue) => info.IsCollection ? ((List<object>)existingValue).Concat(new[] { arg.Value }).ToList() : (object)arg.Value);
added = true;
break;
}
}

if (!added)
{
dict.TryAdd(arg.Key, info.IsCollection ? new List<object>(new[] { arg.Value }) : (object)arg.Value);
}
}
else
{
dict.AddOrUpdate(arg.Key, arg.Value, (key, existingValue) => arg.Value);
}
}

return dict.ToDictionary(a => a.Key, a => a.Value);
}

private static List<KeyValuePair<string, string>> GetArgumentList(string commandLineString)
{
var argumentList = new List<KeyValuePair<string, string>>();
Expand Down Expand Up @@ -640,37 +685,6 @@ private static PropertyInfo GetOperandsProperty(Type type)
}
}

/// <summary>
/// Encapsulates argument names and help text.
/// </summary>
public class ArgumentInfo
{
/// <summary>
/// Gets or sets the help text for the argument.
/// </summary>
public string HelpText { get; set; }

/// <summary>
/// Gets or sets the long name of the argument.
/// </summary>
public string LongName { get; set; }

/// <summary>
/// Gets or sets the short name of the argument.
/// </summary>
public char ShortName { get; set; }

/// <summary>
/// Gets or sets the backing Type of the argument.
/// </summary>
public Type Type { get; set; }

/// <summary>
/// Gets a value indicating whether the argument backing Type is a collection.
/// </summary>
public bool IsCollection => Type.IsArray || (Type.IsGenericType && Type.GetGenericTypeDefinition() == typeof(List<>));
}

/// <summary>
/// Indicates that the property is to be used as the target for automatic population of command line operands when invoking
/// the <see cref="Arguments.Populate(string, bool, string)"/> method.
Expand All @@ -679,4 +693,4 @@ public class ArgumentInfo
public class OperandsAttribute : Attribute
{
}
}
}
Loading