Free cookie consent management tool by TermsFeed Policy Generator

source: branches/2924_DotNetCoreMigration/HeuristicLab.CommandLineInterface/CLIApplication.cs @ 16985

Last change on this file since 16985 was 16985, checked in by dpiringe, 5 years ago

#2924:

  • added CLI Framework HeuristicLab.CommandLineInterface
  • added definition language test project HeuristicLab.DefinitionLanguage
  • added test project HeuristicLab.DynamicAssemblyTestApp, for PluginInfrastructure testing
  • changed project HeuristicLab to .NET Core and used it to create a CLI-Tool with the new CLI Framework
  • added Docker support to HeuristicLab
  • added IRunnerHost.cs ... forgot last commit
  • changed DockerRunnerHost and NativeRunnerHost to HeuristicLab-3.3.exe, was a little test project before
  • added new solution file HeuristicLab 3.3 No Views.sln, where all view projects are unloaded at start
File size: 14.2 KB
Line 
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Globalization;
5using System.Linq;
6using System.Reflection;
7using HeuristicLab.CommandLineInterface.Data;
8using HeuristicLab.CommandLineInterface.Exceptions;
9using HeuristicLab.Common;
10
11namespace HeuristicLab.CommandLineInterface {
12  /// <summary>
13  /// The main <c>CLIApplication</c> class.
14  /// Contains all methods for parsing the main method arguments and map them to types.
15  /// </summary>
16  public static class CLIApplication {
17    /// <summary>
18    /// The current application name. Gets set with the ApplicationAttribute.
19    /// </summary>
20    public static string AppName { get; private set; }
21    /// <summary>
22    /// The current application version. Get set with the ApplicationAttribute.
23    /// </summary>
24    public static Version AppVersion { get; private set; }
25
26    #region Properties
27    private static IList<Exception> Errors { get; set; } = new List<Exception>();
28    private static IList<CommandData> ExecutionOrder { get; set; } = new List<CommandData>();
29    #endregion
30
31    /// <summary>
32    /// Method to parse the given argument. The type parameter needs to implement the
33    /// ICommand interface and has to be marked with the ApplicationAttribute.
34    /// </summary>
35    /// <typeparam name="T">
36    /// Needs to implement the ICommand interface and has to be
37    /// marked with the ApplicationAttribute.
38    /// </typeparam>
39    /// <param name="args">The arguments from the main method.</param>
40    public static void Parse<T>(string[] args) {
41      // extract information from attributes and set the root command
42      CommandData cmdData = Init<T>();
43      ExecutionOrder.Add(cmdData);
44      int valueIndex = 0;
45
46      for (int i = 0; i < args.Length; ++i) {
47        // check if help is request, -> when args[i] is '--help'
48        if (IsHelpRequest(args[i])) CLIConsole.PrintHelp(cmdData);
49        // else check if arg is valid option, then set option
50        else if (IsValidOption(args[i], cmdData, out OptionData option))
51          SetOption(cmdData, option, (i + 1 < args.Length) ? args[++i] : "");
52        // else check if arg is valid command, then jump to next command
53        else if (IsValidCommand(args[i], cmdData, out CommandData next)) {         
54          CheckRequirements(cmdData);
55          valueIndex = 0; // reset value index for new command
56          ExecutionOrder.Add(next);
57          cmdData = next;
58        }
59        // else check if arg is valid value, then set value in property
60        else if (IsValidValue(valueIndex, args[i], cmdData, out ValueData value)) {
61          valueIndex++;
62          SetValue(cmdData, value, args[i]);
63        }
64        // else add error to errorList
65        else Errors.Add(new ArgumentException($"Argument '{args[i]}' is not valid!"));
66      }
67
68      CheckRequirements(cmdData);
69      if (Errors.Count > 0) CLIConsole.PrintHelp(cmdData, new AggregateException(Errors));
70      CallExecutionOrder();
71    }
72
73
74    #region Parser steps and attribute extraction
75    /// <summary>
76    /// Extracts application specific data (from ApplicationAttribute) and calls
77    /// RegisterCommand for the application command (root command).
78    /// </summary>
79    /// <typeparam name="T">The Command which is marked with the ApplicatonAttribute.</typeparam>
80    private static CommandData Init<T>() {
81      Type rootCommand = typeof(T);
82      if (!IsInstancableCommand(rootCommand))
83        throw new ArgumentException("Root type has to derive from ICommand", "rootCommand");
84
85      ApplicationAttribute app = rootCommand.GetCustomAttribute<ApplicationAttribute>(true);
86      if (app == null)
87        throw new ArgumentException("Root type needs ApplicationAttribute", "rootCommand");
88
89      AppName = app.Identifier.ToLower();
90      AppVersion = new Version(app.Version);
91
92      return RegisterCommand(rootCommand);
93    }
94
95    /// <summary>
96    /// Registers a command and manages data extraction from attributes.
97    /// </summary>
98    /// <param name="cmdType">RuntimeType of the command to register.</param>
99    /// <returns>A CommandData instance, which contains all information for that command.</returns>
100    private static CommandData RegisterCommand(Type cmdType) {
101      CommandData cmdData = ExtractCommandData(cmdType);
102      foreach (var prop in cmdType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)) {
103        ExtractOptionData(cmdData, prop);
104        ExtractValueData(cmdData, prop);
105      }
106
107      ValidateValueIndexOrder(cmdData);
108      return cmdData;
109    }
110
111    private static void SetOption(CommandData cmdData, OptionData option, string nextArg) {
112      if (option.Property.PropertyType == typeof(bool))
113        SetPropertyValue(option.Property, cmdData, bool.TrueString);
114      else
115        SetPropertyValue(option.Property, cmdData, nextArg);
116      option.Required = false; // set to false, because it got already set
117    }
118
119    private static void SetValue(CommandData cmdData, ValueData valueData, string nextArg) {
120      valueData.Required = false; // set to false to check later, that every values was set
121      SetPropertyValue(valueData.Property, cmdData, nextArg);
122    }
123
124    /// <summary>
125    /// Checks if all required options and values are set.
126    /// </summary>
127    /// <param name="cmdData">Command to check.</param>
128    private static void CheckRequirements(CommandData cmdData) {
129      foreach (var x in cmdData.Options) {
130        // because it get set to false, it is possible to see if all required options was specified
131        if (x.Required) {
132          Errors.Add(new TargetParameterCountException("Not all required options has been specified!"));
133          break;
134        }
135      }
136      foreach (var x in cmdData.Values) {
137        // because it get set to false, it is possible to see if all required values was specified
138        if (x.Required) {
139          Errors.Add(new TargetParameterCountException("Not all required values has been specified!"));
140          break;
141        }
142      }
143      if (Errors.Count > 0) CLIConsole.PrintHelp(cmdData, new AggregateException(Errors));
144    }
145
146
147    /// <summary>
148    /// Calls all commands in top down order (this means, root command first then a first level child, second level child and so on..).
149    /// </summary>
150    private static void CallExecutionOrder() {
151      foreach (CommandData cmd in ExecutionOrder) {
152        try {
153          GetInstance(cmd).Execute();
154        } catch (Exception e) {
155          CLIConsole.PrintHelp(cmd, e);
156        }
157      }
158    }
159    #endregion
160
161    #region Argument Validation
162    /// <summary>
163    /// Returns true when the given argument can be identified as help option (--help).
164    /// </summary>
165    private static bool IsHelpRequest(string arg) => arg.ToLower().Equals("--help");
166
167    /// <summary>
168    /// Returns true when the given argument is a valid option for the given command.
169    /// </summary>
170    private static bool IsValidOption(string arg, CommandData cmdData, out OptionData option) {
171      foreach (var opt in cmdData.Options) {
172        if ((OptionData.LongformPrefix + opt.Identifier).Equals(arg, StringComparison.InvariantCultureIgnoreCase) ||
173           (OptionData.ShortcutPrefix + opt.Shortcut).Equals(arg, StringComparison.InvariantCultureIgnoreCase)) {
174          option = opt;
175          return true;
176        }
177      }
178      option = null;
179      return false;
180    }
181
182    /// <summary>
183    /// Returns true when the given argument is a valid sub-command for the given command.
184    /// </summary>
185    private static bool IsValidCommand(string arg, CommandData cmdData, out CommandData next) {
186      foreach (var cmd in cmdData.Commands) {
187        if (arg.ToLower().Equals(cmd.Identifier.ToLower())) {
188          next = cmd;
189          return true;
190        }
191      }
192      next = null;
193      return false;
194    }
195
196    /// <summary>
197    /// Returns true when the given arguments is a valid value for the given command.
198    /// </summary>
199    private static bool IsValidValue(int index, string arg, CommandData cmdData, out ValueData value) {
200      bool b = index < cmdData.Values.Count;
201      value = null;
202      if (b) {
203        foreach (var x in cmdData.Values) if (x.Index == index) value = x;
204        b = value == null ? false : IsParsableString(arg, value.Property.PropertyType);
205      }
206      return b;
207    }
208    #endregion
209
210    #region Helper
211    /// <summary>
212    /// Instanciates a command or returns a already existing instance.
213    /// </summary>
214    private static ICommand GetInstance(CommandData cmdData) {
215      if (cmdData.Instance == null)
216        cmdData.Instance = (ICommand)Activator.CreateInstance(cmdData.CommandType);
217      return cmdData.Instance;
218    }
219
220    private static void SetPropertyValue(PropertyInfo property, CommandData cmdData, string nextArg) {
221      try {
222        if (IsValidIEnumerableType(property.PropertyType)) {
223          // get the generic type of the IEnumerable<T>, or type of string if its a IEnumerable
224          Type targetType = property.PropertyType.IsGenericType ? property.PropertyType.GenericTypeArguments[0] : typeof(string);
225          // build a generic List with the target type
226          IList list = (IList)CreateGenericType(typeof(List<>), targetType);
227          string[] elements = nextArg.Split(',');
228          foreach(var elem in elements) list.Add(ParseString(elem, targetType));
229          property.SetValue(GetInstance(cmdData), list);
230        } else property.SetValue(GetInstance(cmdData), ParseString(nextArg, property.PropertyType));
231      } catch (FormatException) {
232        Errors.Add(new FormatException($"Arguments '{nextArg}' does not match type {property.PropertyType.GetPrettyName()}"));
233      }
234    }
235
236    /// <summary>
237    /// Returns true if the specified type is a valid enumerable type (IEnumerable, IEnumerable<>, ICollection<>, IList<>).
238    /// </summary>
239    private static bool IsValidIEnumerableType(Type t) =>
240      t == typeof(IEnumerable) || (
241        t.IsGenericType && (
242            t == typeof(IEnumerable<>).MakeGenericType(t.GenericTypeArguments) ||
243            t == typeof(ICollection<>).MakeGenericType(t.GenericTypeArguments) ||
244            t == typeof(IList<>).MakeGenericType(t.GenericTypeArguments)
245        )
246      ) && t != typeof(string);
247
248    private static object CreateGenericType(Type baseType, params Type[] genericTypeArguments) =>
249      Activator.CreateInstance(baseType.MakeGenericType(genericTypeArguments));
250
251    private static object ParseString(string str, Type targetType) {
252
253      if (targetType == typeof(bool)) return bool.Parse(str);
254      else if (targetType == typeof(string)) return str;
255      else if (targetType == typeof(short)) return short.Parse(str);
256      else if (targetType == typeof(int)) return int.Parse(str);
257      else if (targetType == typeof(long)) return long.Parse(str);
258      else if (targetType == typeof(float)) return float.Parse(str, NumberStyles.Any, CultureInfo.InvariantCulture);
259      else if (targetType == typeof(double)) return double.Parse(str, NumberStyles.Any, CultureInfo.InvariantCulture);
260      else if (targetType.IsEnum) return Enum.Parse(targetType, str, true);
261      else throw new NotSupportedException($"Type {targetType.Name} is not supported!");
262    }
263
264    private static bool IsParsableString(string str, Type targetType) {
265      try {
266        ParseString(str, targetType);
267        return true;
268      } catch (Exception) { return IsValidIEnumerableType(targetType); }
269    }
270
271    private static CommandData ExtractCommandData(Type cmdType) {
272      if (!IsInstancableCommand(cmdType))
273        throw new ArgumentException($"Type {cmdType.Name} is not instantiable! Make sure that a parameterless constructor exists.");
274      CommandData cmdData = new CommandData();
275      CommandAttribute cmdAttribute = cmdType.GetCustomAttribute<CommandAttribute>(true);
276      if (cmdAttribute != null) {
277        cmdData.Identifier = (string.IsNullOrEmpty(cmdAttribute.Identifier) ?
278          cmdType.Name.Replace("Command", "") :
279          cmdAttribute.Identifier).ToLower();
280        cmdData.Description = cmdAttribute.Description;
281        cmdData.CommandType = cmdType;
282        foreach (Type t in cmdAttribute.SubCommands) {
283          CommandData child = RegisterCommand(t);
284          child.Parent = cmdData;
285          cmdData.Commands.Add(child);
286        }
287      }
288      return cmdData;
289    }
290
291    private static void ExtractOptionData(CommandData cmdData, PropertyInfo property) {
292      OptionAttribute optAttr = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), true).FirstOrDefault();
293      if (optAttr != null) {
294        cmdData.Options.Add(new OptionData() {
295          Identifier = property.Name.ToLower(),
296          Description = optAttr.Description,
297          Shortcut = optAttr.Shortcut,
298          Required = optAttr.Required,
299          Hidden = !property.GetMethod.IsPublic && !property.SetMethod.IsPublic,
300          Property = property
301        });
302      }
303    }
304
305    private static void ExtractValueData(CommandData cmdData, PropertyInfo property) {
306      ValueAttribute valAttr = (ValueAttribute)property.GetCustomAttributes(typeof(ValueAttribute), true).FirstOrDefault();
307      if (valAttr != null) {
308        cmdData.Values.Add(new ValueData() {
309          Description = valAttr.Description,
310          Property = property,
311          Index = valAttr.Index
312        });
313      }
314    }
315
316    /// <summary>
317    /// Returns true when a type is a valid and instancable command.
318    /// Valid = it implements ICommand and has a parameterless constructor.
319    /// </summary>
320    private static bool IsInstancableCommand(Type type) =>
321      type.GetInterface(nameof(ICommand)) != null &&
322      !type.IsAbstract &&
323      !type.IsInterface &&
324      type.GetConstructors().Any(info => info.GetParameters().Length == 0);
325
326    private static void ValidateValueIndexOrder(CommandData cmdData) {
327      for (int i = 0; i < cmdData.Values.Count; ++i) {
328        bool b = false;
329        foreach (var val in cmdData.Values)
330          b = b || i == val.Index;
331        if (!b) throw new InvalidValueIndexException("Invalid order of value indexes.");
332      }
333    }
334    #endregion
335  }
336}
Note: See TracBrowser for help on using the repository browser.