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 }