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 }