using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using HeuristicLab.CommandLineInterface.Data;
using HeuristicLab.CommandLineInterface.Exceptions;
using HeuristicLab.Common;
namespace HeuristicLab.CommandLineInterface {
///
/// The main CLIApplication class.
/// Contains all methods for parsing the main method arguments and map them to types.
///
public static class CLIApplication {
///
/// The current application name. Gets set with the ApplicationAttribute.
///
public static string AppName { get; private set; }
///
/// The current application version. Get set with the ApplicationAttribute.
///
public static Version AppVersion { get; private set; }
#region Properties
private static IList Errors { get; set; } = new List();
private static IList ExecutionOrder { get; set; } = new List();
#endregion
///
/// Method to parse the given argument. The type parameter needs to implement the
/// ICommand interface and has to be marked with the ApplicationAttribute.
///
///
/// Needs to implement the ICommand interface and has to be
/// marked with the ApplicationAttribute.
///
/// The arguments from the main method.
public static void Parse(string[] args) {
// extract information from attributes and set the root command
CommandData cmdData = Init();
ExecutionOrder.Add(cmdData);
int valueIndex = 0;
for (int i = 0; i < args.Length; ++i) {
// check if help is request, -> when args[i] is '--help'
if (IsHelpRequest(args[i])) CLIConsole.PrintHelp(cmdData);
// else check if arg is valid option, then set option
else if (IsValidOption(args[i], cmdData, out OptionData option))
SetOption(cmdData, option, (i + 1 < args.Length) ? args[++i] : "");
// else check if arg is valid command, then jump to next command
else if (IsValidCommand(args[i], cmdData, out CommandData next)) {
CheckRequirements(cmdData);
valueIndex = 0; // reset value index for new command
ExecutionOrder.Add(next);
cmdData = next;
}
// else check if arg is valid value, then set value in property
else if (IsValidValue(valueIndex, args[i], cmdData, out ValueData value)) {
valueIndex++;
SetValue(cmdData, value, args[i]);
}
// else add error to errorList
else Errors.Add(new ArgumentException($"Argument '{args[i]}' is not valid!"));
}
CheckRequirements(cmdData);
if (Errors.Count > 0) CLIConsole.PrintHelp(cmdData, new AggregateException(Errors));
CallExecutionOrder();
}
#region Parser steps and attribute extraction
///
/// Extracts application specific data (from ApplicationAttribute) and calls
/// RegisterCommand for the application command (root command).
///
/// The Command which is marked with the ApplicatonAttribute.
private static CommandData Init() {
Type rootCommand = typeof(T);
if (!IsInstancableCommand(rootCommand))
throw new ArgumentException("Root type has to derive from ICommand", "rootCommand");
ApplicationAttribute app = rootCommand.GetCustomAttribute(true);
if (app == null)
throw new ArgumentException("Root type needs ApplicationAttribute", "rootCommand");
AppName = app.Identifier.ToLower();
AppVersion = new Version(app.Version);
return RegisterCommand(rootCommand);
}
///
/// Registers a command and manages data extraction from attributes.
///
/// RuntimeType of the command to register.
/// A CommandData instance, which contains all information for that command.
private static CommandData RegisterCommand(Type cmdType) {
CommandData cmdData = ExtractCommandData(cmdType);
foreach (var prop in cmdType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)) {
ExtractOptionData(cmdData, prop);
ExtractValueData(cmdData, prop);
}
ValidateValueIndexOrder(cmdData);
return cmdData;
}
private static void SetOption(CommandData cmdData, OptionData option, string nextArg) {
if (option.Property.PropertyType == typeof(bool))
SetPropertyValue(option.Property, cmdData, bool.TrueString);
else
SetPropertyValue(option.Property, cmdData, nextArg);
option.Required = false; // set to false, because it got already set
}
private static void SetValue(CommandData cmdData, ValueData valueData, string nextArg) {
valueData.Required = false; // set to false to check later, that every values was set
SetPropertyValue(valueData.Property, cmdData, nextArg);
}
///
/// Checks if all required options and values are set.
///
/// Command to check.
private static void CheckRequirements(CommandData cmdData) {
foreach (var x in cmdData.Options) {
// because it get set to false, it is possible to see if all required options was specified
if (x.Required) {
Errors.Add(new TargetParameterCountException("Not all required options has been specified!"));
break;
}
}
foreach (var x in cmdData.Values) {
// because it get set to false, it is possible to see if all required values was specified
if (x.Required) {
Errors.Add(new TargetParameterCountException("Not all required values has been specified!"));
break;
}
}
if (Errors.Count > 0) CLIConsole.PrintHelp(cmdData, new AggregateException(Errors));
}
///
/// Calls all commands in top down order (this means, root command first then a first level child, second level child and so on..).
///
private static void CallExecutionOrder() {
foreach (CommandData cmd in ExecutionOrder) {
try {
GetInstance(cmd).Execute();
} catch (Exception e) {
CLIConsole.PrintHelp(cmd, e);
}
}
}
#endregion
#region Argument Validation
///
/// Returns true when the given argument can be identified as help option (--help).
///
private static bool IsHelpRequest(string arg) => arg.Equals("--help", StringComparison.InvariantCultureIgnoreCase);
///
/// Returns true when the given argument is a valid option for the given command.
///
private static bool IsValidOption(string arg, CommandData cmdData, out OptionData option) {
foreach (var opt in cmdData.Options) {
if ((OptionData.LongformPrefix + opt.Identifier).Equals(arg, StringComparison.InvariantCultureIgnoreCase) ||
(OptionData.ShortcutPrefix + opt.Shortcut).Equals(arg, StringComparison.InvariantCultureIgnoreCase)) {
option = opt;
return true;
}
}
option = null;
return false;
}
///
/// Returns true when the given argument is a valid sub-command for the given command.
///
private static bool IsValidCommand(string arg, CommandData cmdData, out CommandData next) {
foreach (var cmd in cmdData.Commands) {
if (cmd.Identifier.Equals(arg, StringComparison.InvariantCultureIgnoreCase)) {
next = cmd;
return true;
}
}
next = null;
return false;
}
///
/// Returns true when the given arguments is a valid value for the given command.
///
private static bool IsValidValue(int index, string arg, CommandData cmdData, out ValueData value) {
bool b = index < cmdData.Values.Count;
value = null;
if (b) {
foreach (var x in cmdData.Values) if (x.Index == index) value = x;
b = value == null ? false : IsParsableString(arg, value.Property.PropertyType);
}
return b;
}
#endregion
#region Helper
///
/// Instanciates a command or returns a already existing instance.
///
private static ICommand GetInstance(CommandData cmdData) {
if (cmdData.Instance == null)
cmdData.Instance = (ICommand)Activator.CreateInstance(cmdData.CommandType);
return cmdData.Instance;
}
private static void SetPropertyValue(PropertyInfo property, CommandData cmdData, string nextArg) {
try {
if (IsValidIEnumerableType(property.PropertyType)) {
// get the generic type of the IEnumerable, or type of string if its a IEnumerable
Type targetType = property.PropertyType.IsGenericType ? property.PropertyType.GenericTypeArguments[0] : typeof(string);
// build a generic List with the target type
IList list = (IList)CreateGenericType(typeof(List<>), targetType);
string[] elements = nextArg.Split(',');
foreach (var elem in elements) list.Add(ParseString(elem, targetType));
property.SetValue(GetInstance(cmdData), list);
} else property.SetValue(GetInstance(cmdData), ParseString(nextArg, property.PropertyType));
} catch (FormatException) {
Errors.Add(new FormatException($"Arguments '{nextArg}' does not match type {property.PropertyType.GetPrettyName()}"));
}
}
///
/// Returns true if the specified type is a valid enumerable type (IEnumerable, IEnumerable<>, ICollection<>, IList<>).
///
private static bool IsValidIEnumerableType(Type t) =>
t == typeof(IEnumerable) || (
t.IsGenericType && (
t == typeof(IEnumerable<>).MakeGenericType(t.GenericTypeArguments) ||
t == typeof(ICollection<>).MakeGenericType(t.GenericTypeArguments) ||
t == typeof(IList<>).MakeGenericType(t.GenericTypeArguments)
)
) && t != typeof(string);
private static object CreateGenericType(Type baseType, params Type[] genericTypeArguments) =>
Activator.CreateInstance(baseType.MakeGenericType(genericTypeArguments));
private static object ParseString(string str, Type targetType) {
if (targetType == typeof(bool)) return bool.Parse(str);
else if (targetType == typeof(string)) return str;
else if (targetType == typeof(short)) return short.Parse(str);
else if (targetType == typeof(int)) return int.Parse(str);
else if (targetType == typeof(long)) return long.Parse(str);
else if (targetType == typeof(float)) return float.Parse(str, NumberStyles.Any, CultureInfo.InvariantCulture);
else if (targetType == typeof(double)) return double.Parse(str, NumberStyles.Any, CultureInfo.InvariantCulture);
else if (targetType.IsEnum) return Enum.Parse(targetType, str, true);
else throw new NotSupportedException($"Type {targetType.Name} is not supported!");
}
private static bool IsParsableString(string str, Type targetType) {
try {
ParseString(str, targetType);
return true;
} catch (Exception) { return IsValidIEnumerableType(targetType); }
}
private static CommandData ExtractCommandData(Type cmdType) {
if (!IsInstancableCommand(cmdType))
throw new ArgumentException($"Type {cmdType.Name} is not instantiable! Make sure that a parameterless constructor exists.");
CommandData cmdData = new CommandData();
CommandAttribute cmdAttribute = cmdType.GetCustomAttribute(true);
if (cmdAttribute != null) {
cmdData.Identifier = (string.IsNullOrEmpty(cmdAttribute.Identifier) ?
cmdType.Name.Replace("Command", "") :
cmdAttribute.Identifier).ToLower();
cmdData.Description = cmdAttribute.Description;
cmdData.CommandType = cmdType;
foreach (Type t in cmdAttribute.SubCommands) {
CommandData child = RegisterCommand(t);
child.Parent = cmdData;
cmdData.Commands.Add(child);
}
}
return cmdData;
}
private static void ExtractOptionData(CommandData cmdData, PropertyInfo property) {
OptionAttribute optAttr = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), true).FirstOrDefault();
if (optAttr != null) {
cmdData.Options.Add(new OptionData() {
Identifier = property.Name.ToLower(),
Description = optAttr.Description,
Shortcut = optAttr.Shortcut,
Required = optAttr.Required,
Hidden = !property.GetMethod.IsPublic && !property.SetMethod.IsPublic,
Property = property
});
}
}
private static void ExtractValueData(CommandData cmdData, PropertyInfo property) {
ValueAttribute valAttr = (ValueAttribute)property.GetCustomAttributes(typeof(ValueAttribute), true).FirstOrDefault();
if (valAttr != null) {
cmdData.Values.Add(new ValueData() {
Description = valAttr.Description,
Property = property,
Index = valAttr.Index
});
}
}
///
/// Returns true when a type is a valid and instancable command.
/// Valid = it implements ICommand and has a parameterless constructor.
///
private static bool IsInstancableCommand(Type type) =>
type.GetInterface(nameof(ICommand)) != null &&
!type.IsAbstract &&
!type.IsInterface &&
type.GetConstructors().Any(info => info.GetParameters().Length == 0);
private static void ValidateValueIndexOrder(CommandData cmdData) {
for (int i = 0; i < cmdData.Values.Count; ++i) {
bool b = false;
foreach (var val in cmdData.Values)
b = b || i == val.Index;
if (!b) throw new InvalidValueIndexException("Invalid order of value indexes.");
}
}
#endregion
}
}