|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.CommandLine; |
| 4 | +using System.CommandLine.Invocation; |
| 5 | +using System.Linq; |
| 6 | +using System.Reflection; |
| 7 | +using System.Threading.Tasks; |
| 8 | + |
| 9 | +using ParseResult = System.CommandLine.Parsing.ParseResult; |
| 10 | + |
| 11 | +namespace kate.shared.CommandLine |
| 12 | +{ |
| 13 | + /// <summary> |
| 14 | + /// Helper Methods for generating commands. |
| 15 | + /// </summary> |
| 16 | + public static class CommandLineHelper |
| 17 | + { |
| 18 | + /// <summary> |
| 19 | + /// Generate an instance of <see cref="Command"/> |
| 20 | + /// </summary> |
| 21 | + /// <typeparam name="TOptions"> |
| 22 | + /// Type of the options to deserialize.</typeparam> |
| 23 | + /// <param name="commandName"> |
| 24 | + /// Name of the command.</param> |
| 25 | + /// <param name="commandHelpText"> |
| 26 | + /// Help text for the command</param> |
| 27 | + /// <param name="handler"> |
| 28 | + /// Handler to be called when the generated command was ran.</param> |
| 29 | + /// <returns>Instance of <see cref="Command"/></returns> |
| 30 | + public static Command GenerateCommand<TOptions>(string commandName, string commandHelpText, Func<TOptions, Task> handler) |
| 31 | + where TOptions : class, new() |
| 32 | + { |
| 33 | + return GenerateCommand(typeof(TOptions), commandName, commandHelpText, (obj) => |
| 34 | + { |
| 35 | + if (obj == null) |
| 36 | + { |
| 37 | + return handler(null); |
| 38 | + } |
| 39 | + if (!(obj is TOptions opts)) |
| 40 | + { |
| 41 | + throw new ArgumentException($"Not an instance of {typeof(TOptions)}", nameof(obj)); |
| 42 | + } |
| 43 | + return handler(opts); |
| 44 | + }); |
| 45 | + } |
| 46 | + |
| 47 | + /// <summary> |
| 48 | + /// Generate an instance of <see cref="Command"/> |
| 49 | + /// </summary> |
| 50 | + /// <typeparam name="TOptions"> |
| 51 | + /// Type of the options to deserialize.</typeparam> |
| 52 | + /// <param name="commandName"> |
| 53 | + /// Name of the command.</param> |
| 54 | + /// <param name="commandHelpText"> |
| 55 | + /// Help text for the command</param> |
| 56 | + /// <param name="handler"> |
| 57 | + /// Handler to be called when the generated command was ran.</param> |
| 58 | + /// <returns>Instance of <see cref="Command"/></returns> |
| 59 | + public static Command GenerateCommand<TOptions>(string commandName, string commandHelpText, Func<TOptions, InvocationContext, Task> handler) |
| 60 | + where TOptions : class, new() |
| 61 | + { |
| 62 | + return GenerateCommand(typeof(TOptions), commandName, commandHelpText, (obj, ctx) => |
| 63 | + { |
| 64 | + if (obj == null) |
| 65 | + { |
| 66 | + return handler(null, ctx); |
| 67 | + } |
| 68 | + else if (obj is TOptions opts) |
| 69 | + { |
| 70 | + return handler(opts, ctx); |
| 71 | + } |
| 72 | + throw new ArgumentException($"Not an instance of {typeof(TOptions)}", nameof(obj)); |
| 73 | + }); |
| 74 | + } |
| 75 | + |
| 76 | + /// <summary> |
| 77 | + /// Generate an instance of <see cref="Command"/> |
| 78 | + /// </summary> |
| 79 | + /// <param name="optionsType"> |
| 80 | + /// Type of the options to deserialize.</param> |
| 81 | + /// <param name="commandName"> |
| 82 | + /// Name of the command.</param> |
| 83 | + /// <param name="commandHelpText"> |
| 84 | + /// Help text for the command</param> |
| 85 | + /// <param name="handler"> |
| 86 | + /// Handler to be called when the generated command was ran.</param> |
| 87 | + /// <returns>Instance of <see cref="Command"/></returns> |
| 88 | + public static Command GenerateCommand( |
| 89 | + Type optionsType, |
| 90 | + string commandName, |
| 91 | + string commandHelpText, |
| 92 | + Func<object, InvocationContext, Task> handler) |
| 93 | + { |
| 94 | + var argumentPropertyMap = new Dictionary<string, (object, ActionParameterAttribute)>(); |
| 95 | + var optionsProps = optionsType.GetProperties(BindingFlags.Instance | BindingFlags.Public); |
| 96 | + foreach (var prop in optionsProps) |
| 97 | + { |
| 98 | + var actionParamAttr = prop.GetCustomAttribute<ActionParameterAttribute>(); |
| 99 | + if (actionParamAttr != null) |
| 100 | + { |
| 101 | + var genericArgumentType = typeof(Option<>).MakeGenericType(prop.PropertyType); |
| 102 | + var name = actionParamAttr.Name; |
| 103 | + if (name.StartsWith("--") == false) |
| 104 | + name = $"--{name}"; |
| 105 | + var argumentInstance = Activator.CreateInstance(genericArgumentType, new object[] { name, actionParamAttr.HelpText }); |
| 106 | + if (string.IsNullOrEmpty(actionParamAttr.ShortNameAlias) == false) |
| 107 | + { |
| 108 | + var shortName = actionParamAttr.ShortNameAlias; |
| 109 | + if (shortName.StartsWith("-") == false) |
| 110 | + { |
| 111 | + shortName = "-" + shortName; |
| 112 | + } |
| 113 | + argumentInstance = Activator.CreateInstance(genericArgumentType, new object[] { new string[] { name, shortName }, actionParamAttr.HelpText }); |
| 114 | + } |
| 115 | + typeof(Option<>).GetProperty(nameof(Option.IsRequired))?.SetValue(argumentInstance, actionParamAttr.IsRequired); |
| 116 | + argumentPropertyMap.Add(prop.Name, (argumentInstance, actionParamAttr)); |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + var cmd = new Command(commandName, commandHelpText); |
| 121 | + foreach (var argValue in argumentPropertyMap.Values.Select(e => e.Item1)) |
| 122 | + { |
| 123 | + var addArgumentMethod = typeof(Command).GetMethod("AddOption"); |
| 124 | + if (addArgumentMethod != null) |
| 125 | + { |
| 126 | + _ = addArgumentMethod.Invoke(cmd, new object[] { argValue }); |
| 127 | + } |
| 128 | + } |
| 129 | + cmd.SetHandler(async (ctx) => |
| 130 | + { |
| 131 | + var options = Activator.CreateInstance(optionsType); |
| 132 | + foreach (var kvPair in argumentPropertyMap) |
| 133 | + { |
| 134 | + var key = kvPair.Key; |
| 135 | + var argValue = kvPair.Value.Item1; |
| 136 | + |
| 137 | + var prop = optionsType.GetProperty(key); |
| 138 | + if (prop != null) |
| 139 | + { |
| 140 | + var getValueMethod = typeof(ParseResult) |
| 141 | + .GetMethods() |
| 142 | + .Where(v => v.Name =="GetValueForOption" && v.IsGenericMethod == true |
| 143 | + && (v.GetParameters().FirstOrDefault()?.ParameterType.ToString().StartsWith("System.CommandLine.Option") ?? false) |
| 144 | + && (v.GetParameters().FirstOrDefault()?.ParameterType.IsGenericType ?? false)) |
| 145 | + .FirstOrDefault(); |
| 146 | + if (getValueMethod == null) |
| 147 | + { |
| 148 | + var msg = string.Format("Failed to get method {0}.{1}.{2} on {3}", |
| 149 | + nameof(ctx), |
| 150 | + nameof(ctx.ParseResult), |
| 151 | + nameof(ctx.ParseResult.GetValueForOption), |
| 152 | + FormatTypeName(typeof(ParseResult))); |
| 153 | + throw new ApplicationException(msg); |
| 154 | + } |
| 155 | + var getValueMethodGeneric = getValueMethod.MakeGenericMethod(prop.PropertyType); |
| 156 | + if (getValueMethodGeneric == null) |
| 157 | + { |
| 158 | + var msg = string.Format("Could not make generic method for {0}.{1}.{2} where type is {3} on {4}", |
| 159 | + nameof(ctx), |
| 160 | + nameof(ctx.ParseResult), |
| 161 | + nameof(ctx.ParseResult.GetValueForOption), |
| 162 | + FormatTypeName(prop.PropertyType), |
| 163 | + FormatTypeName(typeof(ParseResult))); |
| 164 | + throw new ApplicationException(msg); |
| 165 | + } |
| 166 | + var argumentValue = getValueMethodGeneric.Invoke(ctx.ParseResult, new object[] { argValue }); |
| 167 | + prop.SetValue(options, argumentValue); |
| 168 | + } |
| 169 | + } |
| 170 | + await handler(options, ctx); |
| 171 | + }); |
| 172 | + return cmd; |
| 173 | + } |
| 174 | + |
| 175 | + /// <summary> |
| 176 | + /// Generate an instance of <see cref="Command"/> |
| 177 | + /// </summary> |
| 178 | + /// <param name="optionsType"> |
| 179 | + /// Type of the options to deserialize.</param> |
| 180 | + /// <param name="commandName"> |
| 181 | + /// Name of the command.</param> |
| 182 | + /// <param name="commandHelpText"> |
| 183 | + /// Help text for the command</param> |
| 184 | + /// <param name="handler"> |
| 185 | + /// Handler to be called when the generated command was ran.</param> |
| 186 | + /// <returns>Instance of <see cref="Command"/></returns> |
| 187 | + public static Command GenerateCommand(Type optionsType, string commandName, string commandHelpText, Func<object, Task> handler) |
| 188 | + { |
| 189 | + return GenerateCommand( |
| 190 | + optionsType, |
| 191 | + commandName, |
| 192 | + commandHelpText, |
| 193 | + (a, b) => handler(a)); |
| 194 | + } |
| 195 | + |
| 196 | + /// <summary> |
| 197 | + /// Generate an instance of <see cref="Command"/> dynamically with the options type, and the type of the |
| 198 | + /// class that should be created and called when the command has been invoked. |
| 199 | + /// </summary> |
| 200 | + /// <param name="optionsType"> |
| 201 | + /// Type of the options. Properties will only be included if they have <see cref="ActionParameterAttribute"/> on it. |
| 202 | + /// </param> |
| 203 | + /// <param name="actionType"> |
| 204 | + /// Type of the class that implements <see cref="IAction"/>. |
| 205 | + /// This type must also have <see cref="CommandActionAttribute"/> on it. |
| 206 | + /// </param> |
| 207 | + /// <returns>Generated instance of <see cref="Command"/></returns> |
| 208 | + /// |
| 209 | + /// <exception cref="ArgumentException"> |
| 210 | + /// Thrown when <paramref name="actionType"/> doesn't implement <see cref="IAction"/> |
| 211 | + /// </exception> |
| 212 | + public static Command GenerateCommand(Type optionsType, Type actionType) |
| 213 | + { |
| 214 | + if (typeof(IAction).IsAssignableFrom(actionType) == false) |
| 215 | + { |
| 216 | + throw new ArgumentException($"Class must implement {nameof(IAction)}", nameof(actionType)); |
| 217 | + } |
| 218 | + var cmdActionAttr = actionType.GetCustomAttribute<CommandActionAttribute>(); |
| 219 | + if (cmdActionAttr == null) |
| 220 | + { |
| 221 | + var msg = string.Format("Attribute {0} does not exist on type {1}", |
| 222 | + FormatTypeName(typeof(CommandActionAttribute)), |
| 223 | + actionType.ToString()); |
| 224 | + throw new ArgumentException(msg, nameof(actionType)); |
| 225 | + } |
| 226 | + |
| 227 | + return GenerateCommand( |
| 228 | + optionsType, |
| 229 | + cmdActionAttr.ActionName, |
| 230 | + cmdActionAttr.DisplayName, |
| 231 | + async (opts, ctx) => |
| 232 | + { |
| 233 | + var actionInstance = (IAction)Activator.CreateInstance(actionType); |
| 234 | + await actionInstance.RunAsync(opts); |
| 235 | + }); |
| 236 | + } |
| 237 | + |
| 238 | + /// <summary><inheritdoc cref="GenerateCommand(Type, Type)" path="/summary"/></summary> |
| 239 | + /// <typeparam name="TOptions"><inheritdoc cref="GenerateCommand(Type, Type)" path="/param[@name='optionsType']"/></typeparam> |
| 240 | + /// <typeparam name="TAction"><inheritdoc cref="GenerateCommand(Type, Type)" path="/param[@name='actionType']"/></typeparam> |
| 241 | + /// <returns><inheritdoc cref="GenerateCommand(Type, Type)" path="/returns"/></returns> |
| 242 | + public static Command GenerateCommand<TOptions, TAction>() |
| 243 | + where TOptions : class |
| 244 | + where TAction : class, IAction |
| 245 | + { |
| 246 | + return GenerateCommand(typeof(TOptions), typeof(TAction)); |
| 247 | + } |
| 248 | + |
| 249 | + private static string FormatTypeName(Type type) |
| 250 | + { |
| 251 | + return type.Namespace + '.' + type.Name; |
| 252 | + } |
| 253 | + } |
| 254 | +} |
0 commit comments