1 /* 2 * hunt-console eases the creation of beautiful and testable command line interfaces. 3 * 4 * Copyright (C) 2018-2019, HuntLabs 5 * 6 * Website: https://www.huntlabs.net 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.console.command.Command; 13 14 import std.string; 15 import std.regex; 16 17 import hunt.console.Console; 18 import hunt.console.error.InvalidArgumentException; 19 import hunt.console.error.LogicException; 20 import hunt.console.helper.Helper; 21 import hunt.console.helper.HelperSet; 22 import hunt.console.input.Input; 23 import hunt.console.input.InputArgument; 24 import hunt.console.input.InputDefinition; 25 import hunt.console.input.InputOption; 26 import hunt.console.output.Output; 27 28 import hunt.collection.Collection; 29 import hunt.collection.List; 30 import hunt.Exceptions; 31 import hunt.console.command.CommandExecutor; 32 import hunt.logging; 33 34 import std.range; 35 36 class Command 37 { 38 private Console application; 39 private string name; 40 private string[] aliases; 41 private InputDefinition definition; 42 private string help; 43 private string description; 44 private bool _ignoreValidationErrors = false; 45 private bool applicationDefinitionMerged = false; 46 private bool applicationDefinitionMergedWithArgs = false; 47 private string synopsis; 48 private HelperSet helperSet; 49 private CommandExecutor executor; 50 51 this() 52 { 53 this(null); 54 } 55 56 this(string name) 57 { 58 definition = new InputDefinition(); 59 60 if (name !is null) { 61 setName(name); 62 } 63 64 configure(); 65 66 if (this.name.empty) { 67 throw new LogicException(format("The command defined in '%s' cannot have an empty name.", typeid(this).name)); 68 } 69 } 70 71 void ignoreValidationErrors() 72 { 73 _ignoreValidationErrors = true; 74 } 75 76 void setConsole(Console application) 77 { 78 this.application = application; 79 if (application is null) { 80 setHelperSet(null); 81 } else { 82 setHelperSet(application.getHelperSet()); 83 } 84 } 85 86 Console getConsole() 87 { 88 return application; 89 } 90 91 HelperSet getHelperSet() 92 { 93 return helperSet; 94 } 95 96 void setHelperSet(HelperSet helperSet) 97 { 98 this.helperSet = helperSet; 99 } 100 101 Helper getHelper(string name) 102 { 103 return helperSet.get(name); 104 } 105 106 /** 107 * Checks whether the command is enabled or not in the current environment 108 * 109 * Override this to check for x or y and return false if the command can not 110 * run properly under the current conditions. 111 */ 112 bool isEnabled() 113 { 114 return true; 115 } 116 117 /** 118 * Configures the current command. 119 */ 120 protected void configure() 121 { 122 } 123 124 /** 125 * Executes the current command. 126 * 127 * This method is not abstract because you can use this class 128 * as a concrete class. In this case, instead of defining the 129 * execute() method, you set the code to execute by passing 130 * a CommandExecutor to the setExecutor() method. 131 */ 132 protected int execute(Input input, Output output) 133 { 134 throw new LogicException("You must override the execute() method in the concrete command class."); 135 } 136 137 /** 138 * Interacts with the user. 139 */ 140 protected void interact(Input input, Output output) 141 { 142 } 143 144 /** 145 * Initializes the command just after the input has been validated. 146 * 147 * This is mainly useful when a lot of commands extends one main command 148 * where some things need to be initialized based on the input arguments and options. 149 */ 150 protected void initialize(Input input, Output output) 151 { 152 } 153 154 /** 155 * Runs the command. 156 */ 157 int run(Input input, Output output) 158 { 159 // force the creation of the synopsis before the merge with the app definition 160 getSynopsis(); 161 162 mergeConsoleDefinition(); 163 164 try { 165 input.bind(definition); 166 } catch (RuntimeException e) { 167 if (!_ignoreValidationErrors) { 168 throw e; 169 } 170 } 171 172 initialize(input, output); 173 174 if (input.isInteractive()) { 175 interact(input, output); 176 } 177 178 input.validate(); 179 180 int statusCode; 181 if (executor !is null) { 182 statusCode = executor.execute(input, output); 183 } else { 184 statusCode = execute(input, output); 185 } 186 187 return statusCode; 188 } 189 190 Command setExecutor(CommandExecutor executor) 191 { 192 this.executor = executor; 193 194 return this; 195 } 196 197 void mergeConsoleDefinition() 198 { 199 mergeConsoleDefinition(true); 200 } 201 202 void mergeConsoleDefinition(bool mergeArgs) 203 { 204 if (application is null || (applicationDefinitionMerged && (applicationDefinitionMergedWithArgs || !mergeArgs))) { 205 return; 206 } 207 208 if (mergeArgs) { 209 Collection!(InputArgument) currentArguments = definition.getArguments(); 210 definition.setArguments(application.getDefinition().getArguments()); 211 definition.addArguments(currentArguments); 212 } 213 214 definition.addOptions(application.getDefinition().getOptions()); 215 216 applicationDefinitionMerged = true; 217 if (mergeArgs) { 218 applicationDefinitionMergedWithArgs = true; 219 } 220 } 221 222 Command setDefinition(InputDefinition definition) 223 { 224 this.definition = definition; 225 226 applicationDefinitionMerged = false; 227 228 return this; 229 } 230 231 InputDefinition getDefinition() 232 { 233 return definition; 234 } 235 236 InputDefinition getNativeDefinition() 237 { 238 return getDefinition(); 239 } 240 241 Command addArgument(string name) 242 { 243 definition.addArgument(new InputArgument(name)); 244 245 return this; 246 } 247 248 Command addArgument(string name, int mode) 249 { 250 definition.addArgument(new InputArgument(name, mode)); 251 252 return this; 253 } 254 255 Command addArgument(string name, int mode, string description) 256 { 257 definition.addArgument(new InputArgument(name, mode, description)); 258 259 return this; 260 } 261 262 Command addArgument(string name, int mode, string description, string defaultValue) 263 { 264 definition.addArgument(new InputArgument(name, mode, description, defaultValue)); 265 266 return this; 267 } 268 269 Command addOption(string name) 270 { 271 definition.addOption(new InputOption(name)); 272 273 return this; 274 } 275 276 Command addOption(string name, string shortcut) 277 { 278 definition.addOption(new InputOption(name, shortcut)); 279 280 return this; 281 } 282 283 Command addOption(string name, string shortcut, int mode) 284 { 285 definition.addOption(new InputOption(name, shortcut, mode)); 286 287 return this; 288 } 289 290 Command addOption(string name, string shortcut, int mode, string description) 291 { 292 definition.addOption(new InputOption(name, shortcut, mode, description)); 293 294 return this; 295 } 296 297 Command addOption(string name, string shortcut, int mode, string description, string defaultValue) 298 { 299 definition.addOption(new InputOption(name, shortcut, mode, description, defaultValue)); 300 301 return this; 302 } 303 304 Command setName(string name) 305 { 306 validateName(name); 307 308 this.name = name; 309 310 return this; 311 } 312 313 string getName() 314 { 315 return name; 316 } 317 318 Command setDescription(string description) 319 { 320 this.description = description; 321 322 return this; 323 } 324 325 string getDescription() 326 { 327 return description; 328 } 329 330 Command setHelp(string help) 331 { 332 this.help = help; 333 334 return this; 335 } 336 337 string getHelp() 338 { 339 return help; 340 } 341 342 string getProcessedHelp() 343 { 344 string help = getHelp(); 345 if (help is null) { 346 return ""; 347 } 348 349 help = help.replace("%command.name%", getName()); 350 351 return help; 352 } 353 354 Command setAliases(string[] aliases...) 355 { 356 foreach (a ; aliases) { 357 validateName(a); 358 } 359 360 this.aliases = aliases; 361 362 return this; 363 } 364 365 string[] getAliases() 366 { 367 return aliases; 368 } 369 370 string getSynopsis() 371 { 372 if (synopsis is null) { 373 synopsis = format("%s %s", name, definition.getSynopsis()).strip(); 374 } 375 376 return synopsis; 377 } 378 379 private void validateName(string name) 380 { 381 // logInfo("command name : ",name); 382 // if (!name.matchAll("^[^\\:]++(\\:[^\\:]++)*$").empty) { 383 // logError("command name : ",name); 384 // throw new InvalidArgumentException(format("Command name '%s' is invalid.", name)); 385 // } 386 } 387 }