1 /**
2     Handles program commands, which are arguments passed to a program that are not prefixed with `-` or `--`. So that
3     you handle programs that can be executed in this format:
4 
5     ---
6     $ ./program -v --global-option=7 command1 --parameter="yo" sub-command --another-parameter=42
7     ---
8 
9     Usage:
10 
11     The basic idea is to create a `ProgramCommands` structure and then define a number of `Command`s that it can handle and
12     optionally, a set of $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`) that it can handle. Each `Command` can in
13     turn have it's own `ProgramCommands`.
14 
15     Handlers:
16 
17     Handlers are provided to be able to eacily handle when a command is encountered. This will allow you to structure your
18     program with a module based architecture, where each "handler" can e.g. pass the commands to a module that is build
19     specifically for handling that command.
20 
21     E.g.:
22     ---
23     // install.d
24     void commandHandler(T)(T commands) {
25         writeln(commands.install); // prints true
26     }
27     // main.d
28     static import install, build;
29     void main(string[] args) {
30         auto commands = ProgramCommands!(
31             Command!"build".handler!(build.commandHandler)
32             Command!"install".handler!(install.commandHandler)
33         )();
34         commands.parse(args);
35         commands.executeHandlers();
36     }
37     // Run it
38     $ ./program install
39     $ >> true
40     ---
41 
42     Inner_ProgramOptions:
43 
44     The first argument to a `ProgramCommands` object can optionally be a $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`)
45     object. In this case, the program options are accessible with an internal variable named `options`
46 
47     E.g.:
48     ---
49     auto commands = ProgramCommands!(
50         ProgramOptions!(Options!("opt1", string)),
51         Command!"command1",
52         Command!"command2".args!(
53             ProgramOptions!(Options!("opt2", string))
54         )
55     )();
56 
57     commands.options.opt1; // access the option
58     commands.command1; // access the command
59     commands.command2.op2; // access the options of command2
60     ---
61 
62     Inner_ProgramCommands:
63 
64     You can also pass in sub commands to a `Command` object in the `args` parameter:
65 
66     ---
67     auto commands = ProgramCommands!(
68         ProgramOptions!(Options!("opt1", string)),
69         Command!"command1".args!(
70             ProgramCommands!(
71                 ProgramOptions!(Options!("opt2", string)),
72                 Command!"sub-command1",
73             ),
74         ),
75     )();
76 
77     commands.options.opt1; // access the option
78     commands.command1.subCommand1; // access the command
79     commands.command1.options.op2; // access the options of command2
80     ---
81 */
82 module dcli.program_commands;
83 
84 ///
85 unittest {
86     alias MainCommands = ProgramCommands!(
87         ProgramOptions!(
88             Option!("glob1", string).shortName!"a".description!"desc",
89         ),
90         Command!"cmd1"
91             .args!(
92                 ProgramOptions!(
93                     Option!("opt1", string).shortName!"b".description!"desc",
94                 ),
95         ),
96         Command!"cmd2"
97             .handler!(Fixtures.handleCommand2)
98             .description!"desc",
99         Command!"cmd3"
100             .args!(
101                 ProgramCommands!(
102                     ProgramOptions!(
103                         Option!("opt3", string).shortName!"d".description!"desc",
104                     ),
105                     Command!"sub1"
106                         .args!(
107                             ProgramOptions!(
108                                 Option!("opt4", string).shortName!"e".description!"desc",
109                             ),
110                         )
111                         .handler!(Fixtures.handleCommand3)
112                         .description!"desc",
113                 ),
114             )
115             .handler!(Fixtures.handleCommand3Sub1)
116             .description!"desc",
117     );
118 
119     auto commands = MainCommands();
120 
121     commands.parse([
122         "-ayo",
123         "cmd3",
124         "-d",
125         "hi",
126         "sub1",
127         "-e",
128         "boo",
129     ]);
130 
131     assert(cast(bool)commands.cmd1 == false);
132     assert(cast(bool)commands.cmd2 == false);
133     assert(cast(bool)commands.cmd3 == true);
134 
135     assert(commands.options.glob1 == "yo");
136     assert(commands.cmd3.options.opt3 == "hi");
137     assert(commands.cmd3.sub1.opt4 == "boo");
138 
139     assert(commands.helpText ==
140 `Options:
141   -a  --glob1   desc
142 Commands:
143   cmd1
144   cmd2  desc
145   cmd3  desc`
146   );
147 
148     commands.executeHandlers;
149 
150     assert(!Fixtures.checkResetHandledCommand2);
151     assert( Fixtures.checkResetHandledCommand3);
152     assert( Fixtures.checkResetHandledCommand3Sub1);
153 }
154 
155 import dcli.program_options;
156 import ddash.lang: Void, isVoid;
157 import optional;
158 import ddash.algorithm: indexWhere;
159 import ddash.range: frontOr;
160 
161 import std.algorithm: map;
162 import std.typecons;
163 import std.stdio: writeln;
164 
165 // version = dcli_programcommands_debug;
166 
167 private void debug_print(Args...)(Args args, int line = __LINE__, string file = __FILE__) @trusted @nogc {
168     version(dcli_programcommands_debug) {
169         debug {
170             import std.stdio: writeln;
171             import std.path: baseName, stripExtension, dirName, pathSplitter;
172             import std.range: array;
173             auto stripped = baseName(stripExtension(file));
174             if (stripped == "package") {
175                 stripped = pathSplitter(dirName(file)).array[$ - 1] ~ "/" ~ stripped;
176             }
177             writeln("[", stripped, ":", line, "] : ", args);
178         }
179     }
180 }
181 
182 private template isCommand(T) {
183     import std.traits: isInstanceOf;
184     enum isCommand = isInstanceOf!(CommandImpl, T);
185 }
186 
187 private template isProgramCommands(T) {
188     import std.traits: isInstanceOf;
189     enum isProgramCommands = isInstanceOf!(ProgramCommands, T);
190 }
191 
192 private string makeValidCamelCase(string name)() if (name.length) {
193     import std.uni: toUpper, isAlpha, isAlphaNum;
194 
195     string ret;
196     enum Action {
197         nothing,
198         capitalize,
199     }
200     auto action = Action.nothing;
201 
202     if (name[0].isAlpha || name[0] == '_') { // record first character only if valid first character
203         ret ~= name[0];
204     }
205     for (int i = 1; i < name.length; ++i) {
206         if (!name[i].isAlphaNum && name[0] != '_') {
207             action = Action.capitalize;
208             continue;
209         }
210 
211         final switch (action) {
212         case Action.nothing:
213             ret ~= name[i];
214             break;
215         case Action.capitalize:
216             ret ~= toUpper(name[i]);
217             break;
218         }
219 
220         action = Action.nothing;
221     }
222     return ret;
223 }
224 
225 unittest {
226     static assert(makeValidCamelCase!"a-a" == "aA");
227     static assert(makeValidCamelCase!"-a" == "a");
228     static assert(makeValidCamelCase!"-a-" == "a");
229     static assert(makeValidCamelCase!"9a" == "a");
230     static assert(makeValidCamelCase!"_9a" == "_9a");
231 }
232 
233 private struct CommandImpl(
234     string _name,
235     string _description = null,
236     _Args = Void,
237     alias _handler = null,
238 ) {
239 
240     alias Name = _name;
241     alias Identifier = makeValidCamelCase!Name;
242     alias Description = _description;
243     alias Args = _Args;
244     alias Handler = _handler;
245 
246     public alias description(string value) = CommandImpl!(Name, value, Args, Handler);
247 
248     public static template args(U) if (isProgramOptions!U || isProgramCommands!U) {
249         alias args = CommandImpl!(Name, Description, U, Handler);
250     }
251 
252     public static template handler(alias value) {
253         alias handler = CommandImpl!(Name, Description, Args, value);
254     }
255 
256     private bool active = false;
257     public T opCast(T: bool)() {
258         return active;
259     }
260     public Args _args;
261     alias _args this;
262 }
263 
264 /**
265     Represents one program command which identified the expected string on the command line. One of more of these can be given to
266     a `ProgramCommands` object as template arguments.
267 
268     The `Command` object is accessible from a `ProgramCommands` object via the name given to the command.
269 
270     If the name is not a valid identifier, it is transformed in to camel case by the following rules:
271     <ol>
272         <li> if first character is invalid first character for identifier, it is skipped.
273         <li> if any following character is not a valid identifier character, it is skipped AND the next valid character
274         is capitalized.
275     </ol>
276 
277     Params:
278         name = The name of the command.
279 
280     Named_optional_arguments:
281 
282     A number of named optional arguments can be given to a `Command` for e.g.:
283     ---
284     ProgramCommands!(
285         Command!"push".description!"This command pushes all your bases belongs to us"
286     );
287     ---
288 
289     This will create a commands object that can parse a command `push` on the command line.
290 
291     The following named optional arguments are available:
292 
293     <li>`description`: `string` - description for help message
294     <li>`args`: $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`)|`ProgramCommands` - this can be given a set of program
295         options that can be used with this command, or it can be given a program commands object so that it has sub commands
296     <li>`handler`: `void function(T)` - this is a handler function that will only be called if this command was present on the
297         command line. The type `T` passed in will be your `ProgramCommands` structure instance
298 */
299 public template Command(string name) if (name.length > 0) {
300     alias Command = CommandImpl!name;
301 }
302 
303 /*
304     These two functions are here so because both ProgramCommands and ProgramOptions adhere to an interface that
305     has these two functions. And when a Command is created that is only a command with no inner options or
306     commands, then it resolves to type Void. So these function "fake" a similar interface for Void.
307 */
308 private void parse(Void, const string[]) {}
309 private string toString(Void) { return ""; }
310 
311 /**
312     You can configure a `ProgramCommands` object with a number of `Command`s and then use it to parse
313     an list of command line arguments
314 
315     The object will generate its member variables from the `Command`s you pass in, for e.g.
316 
317     ---
318     auto commands = ProgramCommands!(Command!"one", Command!"two");
319     commands.one // generated variable
320     commands.two // generated variable
321     ---
322 
323     After you parse command line arguments, the commands that are encountered on the command line become
324     "activated" and can be checked by casting them to a boolean, i.e.
325 
326     ---
327     commands.parse(["two"]);
328     if (commands.one) {} // will be false
329     if (commands.two) {} // will be true
330     ---
331 
332     You can also assign "handler" to a command and use the `executeHandlers` function to call them for the
333     commands that were activated.
334 
335     Params:
336         Commands = 1 or more `Command` objects, each representing one command line argument. The first
337             argument may be a `ProgramOptions` type if you want the command to have a set of options
338             that it may handle
339 */
340 public struct ProgramCommands(Commands...) if (Commands.length > 0) {
341     import std.conv: to;
342 
343     static if (isProgramOptions!(Commands[0])) {
344         // We have a global set of options if the first argument is a ProgramOptions type.
345         enum StartIndex = 1;
346         /**
347             The `ProgramOptions` that are associated with this command if any was passed in. If no `ProgramOptions`
348             where passed as an argument then this aliases to a `Void` pseudo type.
349         */
350         public Commands[0] options;
351     } else {
352         enum StartIndex = 0;
353         public Void options;
354     }
355 
356     // Mixin the variables for each command
357     static foreach (I; StartIndex .. Commands.length) {
358         static assert(
359             isCommand!(Commands[I]),
360             "Expected type Command. Found " ~ Commands[I].stringof ~ " for arg " ~ I.to!string
361         );
362         mixin("public Commands[I] " ~ Commands[I].Identifier ~ ";");
363     }
364 
365     // Get all available commands
366     private immutable allCommands = () {
367         string[] result;
368         static foreach (I; StartIndex .. Commands.length) {
369             result ~= Commands[I].Name;
370         }
371         return result;
372     }();
373 
374     private void parseCommand(string cmd, const string[] args) {
375         command: switch (cmd) {
376             static foreach (I; StartIndex .. Commands.length) {
377                 mixin(`case "` ~ Commands[I].Name ~ `":`);
378                     mixin(Commands[I].Identifier ~ ".parse(args);");
379                     break command;
380             }
381             default:
382                 debug_print("cannot parse invalid command: " ~ cmd);
383                 break;
384         }
385     }
386 
387     // Sets a command to true for when it was encountered
388     private void activateCommand(string cmd) {
389         command: switch (cmd) {
390             static foreach (I; StartIndex .. Commands.length) {
391                 mixin(`case "` ~ Commands[I].Name ~ `":`);
392                     mixin(Commands[I].Identifier ~ ".active = true;");
393                     break command;
394             }
395             default:
396                 debug_print("cannot activate invalid command: " ~ cmd);
397                 break;
398         }
399     }
400 
401     // Returns trur if a string is a valid command
402     private bool isValidCommand(string cmd) {
403         import std.algorithm: canFind;
404         return allCommands.canFind(cmd);
405     }
406 
407     private alias PluckCommandResult = Tuple!(const(string)[], string, const(string)[]);
408     private PluckCommandResult pluckCommand(const string[] args) {
409         alias pred = (a) => this.isValidCommand(a);
410         return args.indexWhere!pred
411             .map!((i) {
412                 return PluckCommandResult(
413                     args[0 .. i],
414                     args[i],
415                     i > 0 ? args[i + 1 .. $] : [],
416                 );
417             })
418             .frontOr(PluckCommandResult.init);
419     }
420 
421     /**
422         Parses the command line arguments according to the set of `Commands`s that are passed in.
423     */
424     public void parse(const string[] args) {
425         auto data = pluckCommand(args);
426 
427         debug_print("plucked => ", data);
428 
429         if (data[0].length) {
430             this.options.parse(data[0]);
431         }
432 
433         if (data[1].length) {
434             activateCommand(data[1]);
435             parseCommand(data[1], data[2]);
436         }
437     }
438 
439     /**
440         Returns a string that represents a block of text that can be output to stdout
441         to display a help message
442     */
443     public string helpText() const {
444         string ret;
445         static if (isProgramOptions!(Commands[0])) {
446             ret ~= this.options.helpText;
447             ret ~= "\n";
448         }
449         static if (StartIndex < Commands.length)
450             ret ~= "Commands:\n";
451         static foreach (I; StartIndex .. Commands.length) {
452             ret ~= "  " ~ Commands[I].Name;
453             if (Commands[I].Description.length)
454                 ret ~= "  " ~ Commands[I].Description
455                 ;
456             static if (I < Commands.length - 1)
457                 ret ~= "\n";
458         }
459         return ret;
460     }
461 
462     /**
463         Returns a string that is a stringified object of keys and values denoting commands
464         and their values and options (if present)
465     */
466     public string toString() const {
467         import std.conv: to;
468         string ret = "{ ";
469         static if (isProgramOptions!(Commands[0])) {
470             ret ~= "options: " ~ this.options.toString ~ ", ";
471         }
472         static foreach (I; StartIndex .. Commands.length) {
473             ret ~= Commands[I].Name ~ ": { ";
474             ret ~= "active: " ~ mixin(Commands[I].Identifier ~ ".active ? \"true\" : \"false\"");
475             ret ~= ", ";
476             static if (isProgramOptions!(Commands[I].Args)) {
477                 ret ~= "options";
478             } else {
479                 ret ~= "commands";
480             }
481             ret ~= ": " ~ mixin(Commands[I].Identifier ~ ".toString");
482             ret ~= " }";
483             static if (I < Commands.length - 1) {
484                 ret ~= ", ";
485             }
486         }
487         ret ~= " }";
488         return ret;
489     }
490 
491     /**
492         If any of the `Command`s have any handlers specified. Then those will be called
493         in order of appearance on the command line by calling this function
494     */
495     public void executeHandlers() {
496         import bolts: isNullType;
497         static foreach (I; StartIndex .. Commands.length) {
498             if (mixin(Commands[I].Identifier)) { // if this command is active
499                 static if (!isNullType!(Commands[I].Handler)) {
500                     Commands[I].Handler(this);
501                 }
502                 static if (isProgramCommands!(Commands[I].Args)) {
503                     mixin(Commands[I].Identifier ~ ".executeHandlers();");
504                 }
505             }
506         }
507     }
508 }
509 
510 version (unittest) {
511     struct Fixtures {
512         static:
513 
514         private bool handledCommand2 = false;
515         private bool handledCommand3 = false;
516         private bool handledCommand3Sub1 = false;
517 
518         bool checkResetHandledCommand2() {
519             const ret = handledCommand2;
520             scope(exit) handledCommand2 = false;
521             return ret;
522         }
523         bool checkResetHandledCommand3() {
524             const ret = handledCommand3;
525             scope(exit) handledCommand3 = false;
526             return ret;
527         }
528         bool checkResetHandledCommand3Sub1() {
529             const ret = handledCommand3Sub1;
530             scope(exit) handledCommand3Sub1 = false;
531             return ret;
532         }
533 
534         void handleCommand2(T)(T command) {
535             handledCommand2 = true;
536         }
537 
538         void handleCommand3(T)(T command) {
539             handledCommand3 = true;
540         }
541 
542         void handleCommand3Sub1(T)(T command) {
543             handledCommand3Sub1 = true;
544         }
545     }
546 }
547 
548 unittest {
549     auto commands = ProgramCommands!(Command!"dashed-command")();
550     commands.parse(["dashed-command"]);
551     assert(cast(bool)commands.dashedCommand);
552 }