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.Console; 13 14 import hunt.console.command; 15 import hunt.console.error.InvalidArgumentException; 16 import hunt.console.error.LogicException; 17 import hunt.console.helper.HelperSet; 18 import hunt.console.helper.ProgressBar; 19 import hunt.console.helper.QuestionHelper; 20 import hunt.console.input; 21 import hunt.console.output; 22 import hunt.console.util.StringUtils; 23 import hunt.console.util.ThrowableUtils; 24 25 import hunt.stream.Common; 26 import hunt.collection.Map; 27 import hunt.collection.HashMap; 28 import hunt.collection.List; 29 import hunt.collection.ArrayList; 30 import hunt.collection.Set; 31 import hunt.collection.HashSet; 32 import hunt.Exceptions; 33 import hunt.logging.ConsoleLogger; 34 import hunt.Integer; 35 36 import core.stdc.stdlib; 37 import std.string; 38 import hunt.util.StringBuilder; 39 40 class Console 41 { 42 private Map!(string, Command) _commands; 43 private bool _wantHelps; 44 private string _name; 45 private string _version; 46 private InputDefinition _definition; 47 private bool _autoExit = true; 48 private string _defaultCommand; 49 private bool _catchExceptions = true; 50 private Command _runningCommand; 51 private Command[] _defaultCommands; 52 private int[] _terminalDimensions; 53 private HelperSet _helperSet; 54 55 this() 56 { 57 this("UNKNOWN", "UNKNOWN"); 58 } 59 60 this(string name, string ver) 61 { 62 _commands = new HashMap!(string, Command)(); 63 _name = name; 64 _version = ver; 65 _defaultCommand = "list"; 66 _helperSet = getDefaultHelperSet(); 67 _definition = getDefaultInputDefinition(); 68 69 foreach (Command command ; getDefaultCommands()) { 70 add(command); 71 } 72 } 73 74 public int run(string[] args) 75 { 76 args = args.length > 1 ? args[1..$] : [] ; 77 return run(new ArgvInput(args), new SystemOutput()); 78 } 79 80 public int run(Input input, Output output) 81 { 82 configureIO(input, output); 83 84 int exitCode; 85 86 try { 87 exitCode = doRun(input, output); 88 } catch (Exception e) { 89 warning(e.msg); 90 // if(e.next !is null) { 91 // warning(e.next); 92 // } 93 version(HUNT_DEBUG) warning(e); 94 95 if (!_catchExceptions) { 96 RuntimeException re = cast(RuntimeException)e; 97 if(re is null) 98 throw new RuntimeException(e); 99 else 100 throw e; 101 } 102 103 if (cast(ConsoleOutput)output !is null) { 104 renderException(e, (cast(ConsoleOutput) output).getErrorOutput()); 105 } else { 106 renderException(e, output); 107 } 108 109 exitCode = 1; 110 } 111 112 if (_autoExit) { 113 if (exitCode > 255) { 114 exitCode = 255; 115 } 116 117 /* System. */exit(exitCode); 118 } 119 120 return exitCode; 121 } 122 123 private void renderException(Throwable error, Output output) 124 { 125 string title = format("%s [%s] ", error.message(), typeid(error).name); 126 output.writeln(title); 127 output.writeln(""); 128 129 if (cast(int)(output.getVerbosity()) >= cast(int)(Verbosity.VERBOSE)) { 130 output.writeln("<comment>Exception trace:</comment>"); 131 output.writeln(ThrowableUtils.getThrowableAsString(error)); 132 } 133 } 134 135 protected int doRun(Input input, Output output) 136 { 137 if (input.hasParameterOption(["--version", "-V"], true)) { 138 output.writeln(getLongVersion()); 139 140 return 0; 141 } 142 143 string name = getCommandName(input); 144 if (input.hasParameterOption(["--help", "-h"], true)) { 145 if (name is null) { 146 name = "help"; 147 input = new ArrayInput("command", "help"); 148 } else { 149 _wantHelps = true; 150 } 151 } 152 153 if (true == input.hasParameterOption("--ansi")) { 154 output.setDecorated(true); 155 } else if (true == input.hasParameterOption("--no-ansi")) { 156 output.setDecorated(false); 157 } 158 159 if (true == input.hasParameterOption(["--no-interaction", "-n"], true)) { 160 input.setInteractive(false); 161 } 162 163 if (true == input.hasParameterOption(["--quiet", "-q"], true)) { 164 output.setVerbosity(Verbosity.QUIET); 165 } else if (true == input.hasParameterOption(["--verbose", "-v"], true)) { 166 output.setVerbosity(Verbosity.VERBOSE); 167 } 168 169 if (name is null) { 170 name = _defaultCommand; 171 input = new ArrayInput("command", _defaultCommand); 172 } 173 174 Command command = find(name); 175 176 _runningCommand = command; 177 int exitCode = doRunCommand(command, input, output); 178 _runningCommand = null; 179 180 return exitCode; 181 } 182 183 protected int doRunCommand(Command command, Input input, Output output) 184 { 185 int exitCode; 186 187 try { 188 exitCode = command.run(input, output); 189 } catch (Exception e) { 190 warning(e.msg); 191 // version(HUNT_DEBUG) warning(e); 192 // todo events 193 throw new RuntimeException(e); 194 } 195 196 return exitCode; 197 } 198 199 private void configureIO(Input input, Output output) 200 { 201 if (input.hasParameterOption("--ansi")) { 202 output.setDecorated(true); 203 } else if (input.hasParameterOption("--no-ansi")) { 204 output.setDecorated(false); 205 } 206 207 if (input.hasParameterOption(["--no-interaction", "-n"], true)) { 208 input.setInteractive(false); 209 } 210 // todo implement posix isatty support 211 212 if (input.hasParameterOption(["--quiet", "-q"], true)) { 213 output.setVerbosity(Verbosity.QUIET); 214 } else { 215 if (input.hasParameterOption("-vvv") || 216 input.hasParameterOption("--verbose=3", true) || 217 input.getParameterOption("--verbose", true) == "3") { 218 output.setVerbosity(Verbosity.DEBUG); 219 } else if (input.hasParameterOption("-vv", true) || 220 input.hasParameterOption("--verbose=2", true) || 221 input.getParameterOption("--verbose", true) == "2") { 222 output.setVerbosity(Verbosity.VERY_VERBOSE); 223 } else if (input.hasParameterOption("-v", true) || 224 input.hasParameterOption("--verbose=1", true) || 225 input.getParameterOption("--verbose", true) == "1") { 226 output.setVerbosity(Verbosity.VERBOSE); 227 } 228 } 229 } 230 231 public void setAutoExit(bool autoExit) 232 { 233 _autoExit = autoExit; 234 } 235 236 public void setCatchExceptions(bool catchExceptions) 237 { 238 _catchExceptions = catchExceptions; 239 } 240 241 public string getName() 242 { 243 return _name; 244 } 245 246 public void setName(string name) 247 { 248 _name = name; 249 } 250 251 public string getVersion() 252 { 253 return _version; 254 } 255 256 public void setVersion(string ver) 257 { 258 _version = ver; 259 } 260 261 public InputDefinition getDefinition() 262 { 263 return _definition; 264 } 265 266 public void setDefinition(InputDefinition definition) 267 { 268 _definition = definition; 269 } 270 271 public string getHelp() 272 { 273 string nl = "\r\n"/* System.getProperty("line.separator") */; 274 275 StringBuilder sb = new StringBuilder(); 276 sb 277 .append(getLongVersion()) 278 .append(nl) 279 .append(nl) 280 .append("<comment>Usage:</comment>") 281 .append(nl) 282 .append(" [options] command [arguments]") 283 .append(nl) 284 .append(nl) 285 .append("<comment>Options:</comment>") 286 .append(nl) 287 ; 288 289 foreach (InputOption option ; _definition.getOptions()) { 290 sb.append(format(" %-29s %s %s", 291 "<info>--" ~ option.getName() ~ "</info>", 292 option.getShortcut() is null ? " " : "<info>-" ~ option.getShortcut() ~ "</info>", 293 option.getDescription()) 294 ).append(nl); 295 } 296 297 return sb.toString(); 298 } 299 300 public string getLongVersion() 301 { 302 if (!(getName() == ("UNKNOWN")) && !(getVersion() == ("UNKNOWN"))) { 303 return format("<info>%s</info> version <comment>%s</comment>", getName(), getVersion()); 304 } 305 306 return "<info>Console Tool</info>"; 307 } 308 309 public Command register(string name) 310 { 311 return add(new Command(name)); 312 } 313 314 public void addCommands(Command[] commands) 315 { 316 foreach (Command command ; commands) { 317 add(command); 318 } 319 } 320 321 /** 322 * Adds a command object. 323 * 324 * If a command with the same name already exists, it will be overridden. 325 */ 326 public Command add(Command command) 327 { 328 command.setConsole(this); 329 330 if (!command.isEnabled()) 331 { 332 command.setConsole(null); 333 return null; 334 } 335 336 if (command.getDefinition() is null) 337 { 338 throw new LogicException(format("Command class '%s' is not correctly initialized. You probably forgot to call the super constructor.", typeid(command).name)); 339 } 340 341 _commands.put(command.getName(), command); 342 343 foreach (string a ; command.getAliases()) 344 { 345 _commands.put(a, command); 346 } 347 348 return command; 349 } 350 351 public Command find(string name) 352 { 353 return get(name); 354 } 355 356 public Map!(string, Command) all() 357 { 358 return _commands; 359 } 360 361 public Map!(string, Command) all(string namespace) 362 { 363 Map!(string, Command) commands = new HashMap!(string, Command)(); 364 365 foreach (Command command ; _commands.values()) { 366 if (namespace == extractNamespace(command.getName(), new Integer(StringUtils.count(namespace, ':') + 1))) { 367 commands.put(command.getName(), command); 368 } 369 } 370 371 return commands; 372 } 373 374 public string extractNamespace(string name) 375 { 376 return extractNamespace(name, null); 377 } 378 379 public string extractNamespace(string name, Integer limit) 380 { 381 List!(string) parts = new ArrayList!(string)(); 382 383 foreach(value; name.split(":")) 384 { 385 parts.add(value); 386 } 387 388 parts.removeAt(parts.size() - 1); 389 390 if (parts.size() == 0) 391 { 392 return null; 393 } 394 395 if (limit !is null && parts.size() > limit.intValue()) 396 { 397 // parts = parts.subList(0, limit); 398 List!(string) temp = new ArrayList!(string)(); 399 for(int i = 0 ; i < limit.intValue(); i++) 400 { 401 temp.add(parts.get(i)); 402 } 403 404 parts = temp; 405 } 406 407 string[] res; 408 foreach(value; parts) 409 { 410 res ~= value; 411 } 412 413 return StringUtils.join(res, ":"); 414 } 415 416 public Command get(string name) 417 { 418 if (!_commands.containsKey(name)) 419 { 420 throw new InvalidArgumentException(format("The command '%s' does not exist.", name)); 421 } 422 423 Command command = _commands.get(name); 424 425 if (_wantHelps) 426 { 427 _wantHelps = false; 428 429 HelpCommand helpCommand = cast(HelpCommand) get("help"); 430 helpCommand.setCommand(command); 431 432 return helpCommand; 433 } 434 435 return command; 436 } 437 438 public bool has(string name) 439 { 440 return _commands.containsKey(name); 441 } 442 443 public string[] getNamespaces() 444 { 445 Set!(string) namespaces = new HashSet!(string)(); 446 447 string namespace; 448 foreach (Command command ; _commands.values()) 449 { 450 namespace = extractNamespace(command.getName()); 451 452 if (namespace !is null) 453 { 454 namespaces.add(namespace); 455 } 456 457 foreach (string a ; command.getAliases()) 458 { 459 extractNamespace(a); 460 if (namespace !is null) { 461 namespaces.add(namespace); 462 } 463 } 464 } 465 466 string[] res; 467 foreach(value; namespaces) 468 { 469 res ~= value; 470 } 471 472 return res; 473 } 474 475 protected string getCommandName(Input input) 476 { 477 return input.getFirstArgument(); 478 } 479 480 protected static InputDefinition getDefaultInputDefinition() 481 { 482 InputDefinition definition = new InputDefinition(); 483 definition.addArgument(new InputArgument("command", InputArgument.REQUIRED, "The command to execute")); 484 definition.addOption(new InputOption("--help", "-h", InputOption.VALUE_NONE, "Display this help message.")); 485 definition.addOption(new InputOption("--quiet", "-q", InputOption.VALUE_NONE, "Do not output any message.")); 486 definition.addOption(new InputOption("--verbose", "-v|vv|vvv", InputOption.VALUE_NONE, "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug.")); 487 definition.addOption(new InputOption("--version", "-V", InputOption.VALUE_NONE, "Display this application version.")); 488 definition.addOption(new InputOption("--ansi", null, InputOption.VALUE_NONE, "Force ANSI output.")); 489 definition.addOption(new InputOption("--no-ansi", null, InputOption.VALUE_NONE, "Disable ANSI output.")); 490 definition.addOption(new InputOption("--no-interaction", "-n", InputOption.VALUE_NONE, "Do not ask any interactive question.")); 491 492 return definition; 493 } 494 495 public Command[] getDefaultCommands() 496 { 497 Command[] commands = new Command[2]; 498 commands[0] = new HelpCommand(); 499 commands[1] = new ListCommand(); 500 501 return commands; 502 } 503 504 public HelperSet getHelperSet() 505 { 506 return _helperSet; 507 } 508 509 public void setHelperSet(HelperSet helperSet) 510 { 511 _helperSet = helperSet; 512 } 513 514 protected HelperSet getDefaultHelperSet() 515 { 516 HelperSet helperSet = new HelperSet(); 517 helperSet.set(new QuestionHelper()); 518 519 return helperSet; 520 } 521 522 public int[] getTerminalDimensions() 523 { 524 if (_terminalDimensions !is null) { 525 return _terminalDimensions; 526 } 527 528 return [80, 120]; 529 } 530 531 public string getSttyColumns() 532 { 533 // todo make this work 534 implementationMissing(); 535 // string sttyColumns = null; 536 // try { 537 // ProcessBuilder builder = new ProcessBuilder("/bin/bash", "stty", "-a"); 538 // Process process = builder.start(); 539 // StringBuilder o = new StringBuilder(); 540 // BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); 541 // string line, previous = null; 542 // while ((line = br.readLine()) !is null) { 543 // if (!line == previous) { 544 // previous = line; 545 // o.append(line).append('\n'); 546 // } 547 // } 548 // sttyColumns = o.toString(); 549 // } catch (IOException e) { 550 // e.printStackTrace(); 551 // } 552 553 // return sttyColumns; 554 return null; 555 } 556 }