1 /// The default core provided by JCLI, the 'heart' of your command line tool.
2 module jaster.cli.core;
3 
4 private
5 {
6     import std.typecons : Flag;
7     import std.traits   : isSomeChar, hasUDA;
8     import jaster.cli.parser, jaster.cli.udas, jaster.cli.binder, jaster.cli.helptext, jaster.cli.resolver;
9     import jaster.ioc;
10 }
11 
12 public
13 {
14     import std.typecons : Nullable;
15 }
16 
17 /// 
18 alias IgnoreFirstArg = Flag!"ignoreFirst";
19 
20 /++
21  + Attach any value from this enum onto an argument to specify what parsing action should be performed on it.
22  + ++/
23 enum CommandArgAction
24 {
25     /// Perform the default parsing action.
26     default_,
27 
28     /++
29      + Increments an argument for every time it is defined inside the parameters.
30      +
31      + Arg Type: Named
32      + Value Type: Any type that supports `++`.
33      + Arg becomes optional: true
34      + ++/
35     count,
36 }
37 
38 /++
39  + Attach this to any struct/class that represents a command.
40  +
41  + See_Also:
42  +  `jaster.cli.core.CommandLineInterface` for more details.
43  + +/
44 struct Command
45 {
46     /// The pattern to match against.
47     string pattern;
48 
49     /// The description of this command.
50     string description;
51 }
52 
53 /++
54  + Attach this to any struct/class that represents the default command.
55  +
56  + See_Also:
57  +  `jaster.cli
58  + ++/
59 struct CommandDefault
60 {
61     /// The description of this command.
62     string description;
63 }
64 
65 /++
66  + Attach this to any member field to mark it as a named argument.
67  +
68  + See_Also:
69  +  `jaster.cli.core.CommandLineInterface` for more details.
70  + +/
71 struct CommandNamedArg
72 {
73     /// The pattern/"name" to match against.
74     string pattern;
75 
76     /// The description of this argument.
77     string description;
78 }
79 
80 /++
81  + Attach this to any member field to mark it as a positional argument.
82  +
83  + See_Also:
84  +  `jaster.cli.core.CommandLineInterface` for more details.
85  + +/
86 struct CommandPositionalArg
87 {
88     /// The position this argument appears at.
89     size_t position;
90 
91     /// The name of this argument. This is only used for the generated help text, and can be left null.
92     string name;
93 
94     /// The description of this argument.
95     string description;
96 }
97 
98 /++
99  + Attach this to any member field to add it to a help text group.
100  +
101  + See_Also:
102  +  `jaster.cli.core.CommandLineInterface` for more details.
103  + +/
104 struct CommandArgGroup
105 {
106     /// The name of the group to put the arg under.
107     string group;
108 
109     /++
110      + The description of the group.
111      +
112      + Notes:
113      +  The intended usage of this UDA is to apply it to a group of args at the same time, instead of attaching it onto
114      +  singular args:
115      +
116      +  ```
117      +  @CommandArgGroup("group1", "Some description")
118      +  {
119      +      @CommandPositionalArg...
120      +  }
121      +  ```
122      + ++/
123     string description;
124 }
125 
126 /++
127  + Attach this onto a `string[]` member field to mark it as the "raw arg list".
128  +
129  + TLDR; Given the command `"tool.exe command value1 value2 --- value3 value4 value5"`, the member field this UDA is attached to
130  + will be populated as `["value3", "value4", "value5"]`
131  + ++/
132 struct CommandRawArg
133 {}
134 
135 /++
136  + A service that allows commands to access the `CommandLineInterface.parseAndExecute` function of the command's `CommandLineInterface`.
137  +
138  + Notes:
139  +  You **must** use `addCommandLineInterfaceService` to add the default implementation of this service into your `ServiceProvider`, you can of course
140  +  create your own implementation, but note that `CommandLineInterface` has special support for the default implementation.
141  +
142  +  Alternatively, don't pass a `ServiceProvider` into your `CommandLineInterface`, and it'll create this service by itself.
143  + ++/
144 interface ICommandLineInterface
145 {
146     /// See: `CommandLineInterface.parseAndExecute`
147     int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes);
148 }
149 
150 private final class ICommandLineInterfaceImpl : ICommandLineInterface
151 {
152     alias ParseAndExecuteT = int delegate(string[], IgnoreFirstArg);
153 
154     private ParseAndExecuteT _func;
155 
156     override int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes)
157     {
158         return this._func(args, ignoreFirst);
159     }
160 }
161 
162 /++
163  + Returns:
164  +  A Singleton `ServiceInfo` providing the default implementation for `ICommandLineInterface`.
165  + ++/
166 ServiceInfo addCommandLineInterfaceService()
167 {
168     return ServiceInfo.asSingleton!(ICommandLineInterface, ICommandLineInterfaceImpl);
169 }
170 
171 /// ditto.
172 ServiceInfo[] addCommandLineInterfaceService(ref ServiceInfo[] services)
173 {
174     services ~= addCommandLineInterfaceService();
175     return services;
176 }
177 
178 private alias CommandExecuteFunc    = int delegate(ArgPullParser, ref string errorMessageIfThereWasOne, scope ref ServiceScope, HelpTextBuilderSimple);
179 private alias CommandCompleteFunc   = void delegate(string[] before, string current, string[] after, ref char[] buffer);
180 private alias ArgValueSetterFunc(T) = void function(ArgToken, ref T);
181 
182 private struct ArgInfo(UDA, T)
183 {
184     UDA uda;
185     ArgValueSetterFunc!T setter;
186     Nullable!CommandArgGroup group;
187     CommandArgAction action;
188     bool wasFound; // For nullables, this is ignore. Otherwise, anytime this is false we need to throw.
189     bool isNullable;
190     bool isBool;
191 }
192 private alias NamedArgInfo(T) = ArgInfo!(CommandNamedArg, T);
193 private alias PositionalArgInfo(T) = ArgInfo!(CommandPositionalArg, T);
194 
195 private struct CommandArguments(T)
196 {
197     NamedArgInfo!T[] namedArgs;
198     PositionalArgInfo!T[] positionalArgs;
199 }
200 
201 private struct CommandInfo
202 {
203     Command               pattern; // Patterns (and their helper functions) are still being kept around, so previous code can work unimpeded from the migration to CommandResolver.
204     HelpTextBuilderSimple helpText;
205     CommandExecuteFunc    doExecute;
206     CommandCompleteFunc   doComplete;
207 }
208 
209 /+ COMMAND INFO CREATOR FUNCTIONS +/
210 private HelpTextBuilderSimple createHelpText(T, Command UDA)(string appName, in CommandArguments!T commandArgs)
211 {
212     import std.array     : array;
213 
214     auto builder = new HelpTextBuilderSimple();
215 
216     void handleGroup(Nullable!CommandArgGroup uda)
217     {
218         if(uda.isNull)
219             return;
220 
221         builder.setGroupDescription(uda.get.group, uda.get.description);
222     }
223 
224     foreach(arg; commandArgs.namedArgs)
225     {
226         builder.addNamedArg(
227             (arg.group.isNull) ? null : arg.group.get.group,
228             arg.uda.pattern.byPatternNames.array,
229             arg.uda.description,
230             cast(ArgIsOptional)arg.isNullable
231         );
232         handleGroup(arg.group);
233     }
234 
235     foreach(arg; commandArgs.positionalArgs)
236     {
237         builder.addPositionalArg(
238             (arg.group.isNull) ? null : arg.group.get.group,
239             arg.uda.position,
240             arg.uda.description,
241             cast(ArgIsOptional)arg.isNullable,
242             arg.uda.name
243         );
244         handleGroup(arg.group);
245     }
246 
247     builder.commandName = appName ~ " " ~ UDA.pattern;
248     builder.description = UDA.description;
249 
250     return builder;
251 }
252 
253 private CommandCompleteFunc createCommandCompleteFunc(alias T)(CommandArguments!T commandArgs)
254 {
255     import std.algorithm : filter, map, startsWith, splitter, canFind;
256     import std.exception : assumeUnique;
257 
258     return (string[] before, string current, string[] after, ref char[] output)
259     {
260         // Check if there's been a null ("--") or '-' ("---"), and if there has, don't bother with completion.
261         // Because anything past that is of course, the raw arg list.
262         if(before.canFind(null) || before.canFind("-"))
263             return;
264 
265         // See if the previous value was a non-boolean argument.
266         const justBefore               = ArgPullParser(before[$-1..$]).front;
267         auto  justBeforeNamedArgResult = commandArgs.namedArgs.filter!(a => matchSpacelessPattern(a.uda.pattern, justBefore.value));
268         if((justBefore.type == ArgTokenType.LongHandArgument || justBefore.type == ArgTokenType.ShortHandArgument)
269         && (!justBeforeNamedArgResult.empty && !justBeforeNamedArgResult.front.isBool))
270         {
271             // TODO: In the future, add support for specifying values to a parameter, either static and/or dynamically.
272             return;
273         }
274 
275         // Otherwise, we either need to autocomplete an argument's name, or something else that's predefined.
276 
277         string[] names;
278         names.reserve(commandArgs.namedArgs.length * 2);
279 
280         foreach(arg; commandArgs.namedArgs)
281         {
282             foreach(pattern; arg.uda.pattern.byPatternNames)
283             {
284                 // Reminder: Confusingly for this use case, arguments don't have their leading dashes in the before and after arrays.
285                 if(before.canFind(pattern) || after.canFind(pattern))
286                     continue;
287 
288                 names ~= pattern;
289             }
290         }
291 
292         foreach(name; names.filter!(n => n.startsWith(current)))
293         {
294             output ~= (name.length == 1) ? "-" : "--";
295             output ~= name;
296             output ~= ' ';
297         }
298     };
299 }
300 
301 private CommandExecuteFunc createCommandExecuteFunc(alias T)(CommandArguments!T commandArgs)
302 {
303     import std.format    : format;
304     import std.algorithm : filter, map;
305     import std.exception : enforce, collectException;
306 
307     // This is expecting the parser to have already read in the command's name, leaving only the args.
308     return (ArgPullParser parser, ref string executionError, scope ref ServiceScope services, HelpTextBuilderSimple helpText)
309     {
310         if(containsHelpArgument(parser))
311         {
312             import std.stdio : writeln;
313             writeln(helpText.toString());
314             return 0;
315         }
316 
317         // Cross-stage state.
318         T        commandInstance;
319         bool     processRawList = false;
320         string[] rawList;
321 
322         // Create the command and fetch its arg info.
323         commandInstance = Injector.construct!T(services);
324         static if(is(T == class))
325             assert(commandInstance !is null, "Dependency injection failed somehow.");
326 
327         // Execute stages
328         const argsWereParsed = onExecuteParseArgs!T(
329             commandArgs,
330             /*ref*/ commandInstance,
331             /*ref*/ parser,
332             /*ref*/ executionError,
333             /*ref*/ processRawList,
334             /*ref*/ rawList,
335         );
336         if(!argsWereParsed)
337             return -1;
338 
339         const argsWereValidated = onExecuteValidateArgs!T(
340             commandArgs,
341             /*ref*/ executionError
342         );
343         if(!argsWereValidated)
344             return -1;
345 
346         if(processRawList)
347             insertRawList!T(/*ref*/ commandInstance, rawList);
348 
349         return onExecuteRunCommand!T(
350             /*ref*/ commandInstance,
351             /*ref*/ executionError
352         );
353     };
354 }
355 
356 
357 /+ COMMAND EXECUTION STAGES +/
358 private bool onExecuteParseArgs(alias T)(
359     CommandArguments!T      commandArgs,
360     ref T                   commandInstance,
361     ref ArgPullParser       parser,
362     ref string              executionError,
363     ref bool                processRawList,
364     ref string[]            rawList
365 )
366 {
367     import std.algorithm : all;
368     import std.format    : format;
369 
370     // Parse args.
371     size_t positionalArgIndex = 0;
372     for(; !parser.empty && !processRawList; parser.popFront())
373     {
374         const  token = parser.front;
375         string debugName; // Used for when there's a validation error
376         try final switch(token.type) with(ArgTokenType)
377         {
378             case Text:
379                 if(positionalArgIndex >= commandArgs.positionalArgs.length)
380                 {
381                     executionError = "too many arguments starting at '"~token.value~"'";
382                     return false;
383                 }
384 
385                 debugName = "positional arg %s(%s)".format(positionalArgIndex, commandArgs.positionalArgs[positionalArgIndex].uda.name);
386                 commandArgs.positionalArgs[positionalArgIndex].setter(token, /*ref*/ commandInstance);
387                 commandArgs.positionalArgs[positionalArgIndex++].wasFound = true;
388                 break;
389 
390                 case LongHandArgument:
391                 if(token.value == "-" || token.value == "") // --- || --
392                 {
393                     processRawList = true;
394                     rawList = parser.unparsedArgs;
395                     break;
396                 }
397                 goto case;
398             case ShortHandArgument:
399                 NamedArgInfo!T result;
400                 foreach(ref arg; commandArgs.namedArgs)
401                 {
402                     if(/*static member*/matchSpacelessPattern(arg.uda.pattern, token.value))
403                     {
404                         arg.wasFound = true;
405                         result       = arg;
406                         debugName    = "named argument "~arg.uda.pattern;
407                         break;
408                     }
409                 }
410 
411                 if(result == NamedArgInfo!T.init)
412                 {
413                     executionError = "Unknown named argument: '"~token.value~"'";
414                     return false;
415                 }
416 
417                 // TODO: Bother breaking this up into functions.
418                 final switch(result.action) with(CommandArgAction)
419                 {
420                     case default_:
421                         if(result.isBool)
422                         {
423                             import std.algorithm : canFind;
424                             // Bools have special support:
425                             //  If they are defined, they are assumed to be true, however:
426                             //      If the next token is Text, and its value is one of a predefined list, then it is then sent to the ArgBinder instead of defaulting to true.
427 
428                             auto parserCopy = parser;
429                             parserCopy.popFront();
430 
431                             if(parserCopy.empty
432                             || parserCopy.front.type != ArgTokenType.Text
433                             || !["true", "false"].canFind(parserCopy.front.value))
434                             {
435                                 result.setter(ArgToken("true", ArgTokenType.Text), /*ref*/ commandInstance);
436                                 break;
437                             }
438 
439                             result.setter(parserCopy.front, /*ref*/ commandInstance);
440                             parser.popFront(); // Keep the main parser up to date.
441                         }
442                         else
443                         {
444                             parser.popFront();
445 
446                             if(parser.front.type == ArgTokenType.EOF)
447                             {
448                                 executionError = "Named arg '"~result.uda.pattern~"' was specified, but wasn't given a value.";
449                                 return false;
450                             }
451 
452                             result.setter(parser.front, /*ref*/ commandInstance);
453                         }
454                         break;
455 
456                     case count:
457                         auto parserCopy  = parser;
458                         auto incrementBy = 1;
459                         
460                         // Support "-vvvvv" syntax.
461                         parserCopy.popFront();
462                         if(token.type == ShortHandArgument 
463                         && parserCopy.front.type == Text
464                         && parserCopy.front.value.all!(c => c == token.value[0]))
465                         {
466                             incrementBy += parserCopy.front.value.length;
467                             parser.popFront(); // keep main parser up to date.
468                         }
469 
470                         // .setter will perform an increment each call.
471                         foreach(i; 0..incrementBy)
472                             result.setter(ArgToken.init, /*ref*/ commandInstance);
473                         break;
474                 }
475                 break;
476 
477             case None:
478                 throw new Exception("An Unknown error occured when parsing the arguments.");
479 
480             case EOF:
481                 break;
482         }
483         catch(Exception ex)
484         {
485             executionError = "For "~debugName~": "~ex.msg;
486             return false;
487         }
488     }
489 
490     return true;
491 }
492 
493 private bool onExecuteValidateArgs(alias T)(
494     CommandArguments!T  commandArgs,
495     ref string          executionError
496 )
497 {
498     import std.algorithm : filter, map;
499     import std.format    : format;
500     import std.exception : assumeUnique;
501 
502     char[] error;
503     error.reserve(512);
504 
505     // Check for missing args.
506     auto missingNamedArgs      = commandArgs.namedArgs.filter!(a => !a.isNullable && !a.wasFound);
507     auto missingPositionalArgs = commandArgs.positionalArgs.filter!(a => !a.isNullable && !a.wasFound);
508     if(!missingNamedArgs.empty)
509     {
510         foreach(arg; missingNamedArgs)
511         {
512             const name = arg.uda.pattern.byPatternNames.front;
513             error ~= (name.length == 1) ? "-" : "--";
514             error ~= name;
515             error ~= ", ";
516         }
517     }
518     if(!missingPositionalArgs.empty)
519     {
520         foreach(arg; missingPositionalArgs)
521         {
522             error ~= "<";
523             error ~= arg.uda.name;
524             error ~= ">, ";
525         }
526     }
527 
528     if(error.length > 0)
529     {
530         error = error[0..$-2]; // Skip extra ", "
531         executionError = "Missing required arguments " ~ error.assumeUnique;
532         return false;
533     }
534 
535     return true;
536 }
537 
538 private int onExecuteRunCommand(alias T)(
539     ref T      commandInstance,
540     ref string executionError
541 )
542 {
543     static assert(
544         __traits(compiles, commandInstance.onExecute())
545      || __traits(compiles, { int code = commandInstance.onExecute(); }),
546         "Unable to call the `onExecute` function for command `"~__traits(identifier, T)~"` please ensure it's signature matches either:"
547         ~"\n\tvoid onExecute();"
548         ~"\n\tint onExecute();"
549     );
550 
551     try
552     {
553         static if(__traits(compiles, {int i = commandInstance.onExecute();}))
554             return commandInstance.onExecute();
555         else
556         {
557             commandInstance.onExecute();
558             return 0;
559         }
560     }
561     catch(Exception ex)
562     {
563         executionError = ex.msg;
564         debug executionError ~= "\n\nSTACK TRACE:\n" ~ ex.info.toString(); // trace info
565         return -1;
566     }
567 }
568 
569 
570 /+ COMMAND RUNTIME HELPERS +/
571 private void insertRawList(T)(ref T command, string[] rawList)
572 {
573     import std.traits : getSymbolsByUDA;
574 
575     alias RawListArgs = getSymbolsByUDA!(T, CommandRawArg);
576     static assert(RawListArgs.length < 2, "Only a single `@CommandRawArg` can exist for command "~T.stringof);
577 
578     static if(RawListArgs.length > 0)
579     {
580         alias RawListArg = RawListArgs[0];
581         static assert(
582             is(typeof(RawListArg) == string[]),
583             "`@CommandRawArg` can ONLY be used with `string[]`, not `" ~ typeof(RawListArg).stringof ~ "` in command " ~ T.stringof
584         );
585 
586         const RawListName = __traits(identifier, RawListArg);
587         static assert(RawListName != "RawListName", "__traits(identifier) failed.");
588 
589         mixin("command."~RawListName~" = rawList;");
590     }
591 }
592 
593 /+ OTHER HELPERS +/
594 private template GetArgAction(alias T)
595 {
596     import std.meta : Filter;
597 
598     enum FilterFunc(alias T) = __traits(compiles, typeof(T)) && is(typeof(T) == CommandArgAction);
599     alias Filtered           = Filter!(FilterFunc, __traits(getAttributes, T));
600 
601     static if(Filtered.length == 0)
602         enum GetArgAction = CommandArgAction.default_;
603     else static if(Filtered.length > 1)
604         static assert(false, "Argument `"~T.stringof~"` has multiple instance of `CommandArgAction`, only 1 is allowed.");
605     else
606         enum GetArgAction = Filtered[0];
607 }
608 
609 /++
610  + Settings that can be provided to `CommandLineInterface` to change certain behaviour.
611  + ++/
612 struct CommandLineSettings
613 {
614     /++
615      + The name of your application, this is only used when displaying error messages and help text.
616      +
617      + If left as `null`, then the executable's name is used instead.
618      + ++/
619     string appName;
620 
621     /++
622      + Whether or not `CommandLineInterface` should provide bash completion. Defaults to `false`.
623      +
624      + See_Also: The README for this project.
625      + ++/
626     bool bashCompletion = false;
627 }
628 
629 /++
630  + Provides the functionality of parsing command line arguments, and then calling a command.
631  +
632  + Description:
633  +  The `Modules` template parameter is used directly with `jaster.cli.binder.ArgBinder` to provide the arg binding functionality.
634  +  Please refer to `ArgBinder`'s documentation if you are wanting to use custom made binder funcs.
635  +
636  +  Commands are detected by looking over every module in `Modules`, and within each module looking for types marked with `@Command` and matching their patterns
637  +  to the given input.
638  +
639  + Patterns:
640  +  Patterns are pretty simple.
641  +
642  +  Example #1: The pattern "run" will match if the given command line args starts with "run".
643  +
644  +  Example #2: The pattern "run all" will match if the given command line args starts with "run all" (["run all"] won't work right now, only ["run", "all"] will)
645  +
646  +  Example #3: The pattern "r|run" will match if the given command line args starts with "r", or "run".
647  +
648  +  Longer patterns take higher priority than shorter ones.
649  +
650  +  Patterns with spaces are only allowed inside of `@Command` pattern UDAs. The `@CommandNamedArg` UDA is a bit more special.
651  +
652  +  For `@CommandNamedArg`, spaces are not allowed, since named arguments can't be split into spaces.
653  +
654  +  For `@CommandNamedArg`, patterns or subpatterns (When "|" is used to have multiple patterns) will be treated differently depending on their length.
655  +  For patterns with only 1 character, they will be matched using short-hand argument form (See `ArgPullParser`'s documentation).
656  +  For pattern with more than 1 character, they will be matched using long-hand argument form.
657  +
658  +  Example #4: The pattern (for `@CommandNamedArg`) "v|verbose" will match when either "-v" or "--verbose" is used.
659  +
660  +  Internally, `CommandResolver` is used to perform command resolution, and a solution custom to `CommandLineInterface` is used for everything else
661  +  regarding patterns.
662  +
663  + Commands:
664  +  A command is a struct or class that is marked with `@Command`.
665  +
666  +  A default command can be specified using `@CommandDefault` instead.
667  +
668  +  Commands have only one requirement - They have a function called `onExecute`.
669  +
670  +  The `onExecute` function is called whenever the command's pattern is matched with the command line arguments.
671  +
672  +  The `onExecute` function must be compatible with one of these signatures:
673  +      `void onExecute();`
674  +      `int onExecute();`
675  +
676  +  The signature that returns an `int` is used to return a custom status code.
677  +
678  +  If a command has its pattern matched, then its arguments will be parsed before `onExecute` is called.
679  +
680  +  Arguments are either positional (`@CommandPositionalArg`) or named (`@CommandNamedArg`).
681  +
682  + Dependency_Injection:
683  +  Whenever a command object is created, it is created using dependency injection (via the `jioc` library).
684  +
685  +  Each command is given its own service scope, even when a command calls another command.
686  +
687  + Positional_Arguments:
688  +  A positional arg is an argument that appears in a certain 'position'. For example, imagine we had a command that we wanted to
689  +  execute by using `"myTool create SomeFile.txt \"This is some content\""`.
690  +
691  +  The shell will pass `["create", "SomeFile.txt", "This is some content"]` to our program. We will assume we already have a command that will match with "create".
692  +  We are then left with the other two strings.
693  +
694  +  `"SomeFile.txt"` is in the 0th position, so its value will be binded to the field marked with `@CommandPositionalArg(0)`.
695  +
696  +  `"This is some content"` is in the 1st position, so its value will be binded to the field marked with `@CommandPositionalArg(1)`.
697  +
698  + Named_Arguments:
699  +  A named arg is an argument that follows a name. Names are either in long-hand form ("--file") or short-hand form ("-f").
700  +
701  +  For example, imagine we execute a custom tool with `"myTool create -f=SomeFile.txt --content \"This is some content\""`.
702  +
703  +  The shell will pass `["create", "-f=SomeFile.txt", "--content", "This is some content"]`. Notice how the '-f' uses an '=' sign, but '--content' doesn't.
704  +  This is because the `ArgPullParser` supports various different forms of named arguments (e.g. ones that use '=', and ones that don't).
705  +  Please refer to its documentation for more information.
706  +
707  +  Imagine we already have a command made that matches with "create". We are then left with the rest of the arguments.
708  +
709  +  "-f=SomeFile.txt" is parsed as an argument called "f" with the value "SomeFile.txt". Using the logic specified in the "Binding Arguments" section (below), 
710  +  we perform the binding of "SomeFile.txt" to whichever field marked with `@CommandNamedArg` matches with the name "f".
711  +
712  +  `["--content", "This is some content"]` is parsed as an argument called "content" with the value "This is some content". We apply the same logic as above.
713  +
714  + Binding_Arguments:
715  +  Once we have matched a field marked with either `@CommandPositionalArg` or `@CommandNamedArg` with a position or name (respectively), then we
716  +  need to bind the value to the field.
717  +
718  +  This is where the `ArgBinder` is used. First of all, please refer to its documentation as it's kind of important.
719  +  Second of all, we esentially generate a call similar to: `ArgBinderInstance.bind(myCommandInstance.myMatchedField, valueToBind)`
720  +
721  +  So imagine we have this field inside a command - `@CommandPositionalArg(0) int myIntField;`
722  +
723  +  Now imagine we have the value "200" in the 0th position. This means it'll be matchd with `myIntField`.
724  +
725  +  This will esentially generate this call: `ArgBinderInstance.bind(myCommandInstance.myIntField, "200")`
726  +
727  +  From there, ArgBinder will do its thing of binding/converting the string "200" into the integer 200.
728  +
729  +  `ArgBinder` has support for user-defined binders (in fact, all of the built-in binders use this mechanism!). Please
730  +  refer to its documentation for more information, or see example-04.
731  +
732  +  You can also specify validation for arguments, by attaching structs (that match the definition specified in `ArgBinder`'s documentation) as
733  +  UDAs onto your fields.
734  +
735  +  $(B Beware) you need to attach your validation struct as `@Struct()` (or with args) and not `@Struct`, notice the first one has parenthesis.
736  +
737  + Boolean_Binding:
738  +  Bool arguments have special logic in place.
739  +
740  +  By only passing the name of a boolean argument (e.g. "--verbose"), this is treated as setting "verbose" to "true" using the `ArgBinder`.
741  +
742  +  By passing a value alongside a boolean argument that is either "true" or "false" (e.g. "--verbose true", "--verbose=false"), then the resulting
743  +  value is passed to the `ArgBinder` as usual. In other words, "--verbose" is equivalent to "--verbose true".
744  +
745  +  By passing a value alongside a boolean argument that $(B isn't) one of the preapproved words then: The value will be treated as a positional argument;
746  +  the boolean argument will be set to true.
747  +
748  +  For example, "--verbose" sets "verbose" to "true". Passing "--verbose=false/true" will set "verbose" to "false" or "true" respectively. Passing
749  +  "--verbose push" would leave "push" as a positional argument, and then set "verbose" to "true".
750  +
751  +  These special rules are made so that boolean arguments can be given an explicit value, without them 'randomly' treating positional arguments as their value.
752  +
753  + Optional_And_Required_Arguments:
754  +  By default, all arguments are required.
755  +
756  +  To make an optional argument, you must make it `Nullable`. For example, to have an optional `int` argument you'd use `Nullable!int` as the type.
757  +
758  +  Note that `Nullable` is publicly imported by this module, for ease of use.
759  +
760  +  Before a nullable argument is binded, it is first lowered down into its base type before being passed to the `ArgBinder`.
761  +  In other words, a `Nullable!int` argument will be treated as a normal `int` by the ArgBinder.
762  +
763  +  If **any** required argument is not provided by the user, then an exception is thrown (which in turn ends up showing an error message).
764  +  This does not occur with missing optional arguments.
765  +
766  + Raw_Arguments:
767  +  For some applications, they may allow the ability for the user to provide a set of unparsed arguments. For example, dub allows the user
768  +  to provide a set of arguments to the resulting output, when using the likes of `dub run`, e.g. `dub run -- value1 value2 etc.`
769  +
770  +  `CommandLineInterface` also provides this ability. You can use either the double dash like in dub ('--') or a triple dash (legacy reasons, '---').
771  +
772  +  After that, as long as your command contains a `string[]` field marked with `@CommandRawArg`, then any args after the triple dash are treated as "raw args" - they
773  +  won't be parsed, passed to the ArgBinder, etc. they'll just be passed into the variable as-is.
774  +
775  +  For example, you have the following member in a command `@CommandRawArg string[] rawList;`, and you are given the following command - 
776  +  `["command", "value1", "--", "rawValue1", "rawValue2"]`, which will result in `rawList`'s value becoming `["rawValue1", "rawValue2"]`
777  +
778  + Arguments_Groups:
779  +  Arguments can be grouped together so they are displayed in a more logical manner within your command's help text.
780  +
781  +  The recommended way to make an argument group, is to create an `@CommandArgGroup` UDA block:
782  +
783  +  ```
784  +  @CommandArgGroup("Debug", "Flags relating the debugging.")
785  +  {
786  +      @CommandNamedArg("trace|t", "Enable tracing") Nullable!bool trace;
787  +      ...
788  +  }
789  +  ```
790  +
791  +  While you *can* apply the UDA individually to each argument, there's one behaviour that you should be aware of - the group's description
792  +  as displayed in the help text will use the description of the $(B last) found `@CommandArgGroup` UDA.
793  +
794  + Params:
795  +  Modules = The modules that contain the commands and/or binder funcs to use.
796  + +/
797 final class CommandLineInterface(Modules...)
798 {
799     private alias defaultCommands = getSymbolsByUDAInModules!(CommandDefault, Modules);
800     static assert(defaultCommands.length <= 1, "Multiple default commands defined " ~ defaultCommands.stringof);
801 
802     static if(defaultCommands.length > 0)
803     {
804         static assert(is(defaultCommands[0] == struct) || is(defaultCommands[0] == class),
805             "Only structs and classes can be marked with @CommandDefault. Issue Symbol = " ~ __traits(identifier, defaultCommands[0])
806         );
807         static assert(!hasUDA!(defaultCommands[0], Command),
808             "Both @CommandDefault and @Command are used for symbol " ~ __traits(identifier, defaultCommands[0])
809         );
810     }
811 
812     alias ArgBinderInstance = ArgBinder!Modules;
813 
814     private enum Mode
815     {
816         execute,
817         complete,
818         bashCompletion
819     }
820 
821     private enum ParseResultType
822     {
823         commandFound,
824         commandNotFound,
825         showHelpText
826     }
827 
828     private struct ParseResult
829     {
830         ParseResultType type;
831         CommandInfo     command;
832         string          helpText;
833         ArgPullParser   argParserAfterAttempt;
834         ArgPullParser   argParserBeforeAttempt;
835         ServiceScope    services;
836     }
837 
838     /+ VARIABLES +/
839     private
840     {
841         CommandResolver!CommandInfo _resolver;
842         CommandLineSettings         _settings;
843         ServiceProvider             _services;
844         Nullable!CommandInfo        _defaultCommand;
845     }
846 
847     /+ PUBLIC INTERFACE +/
848     public final
849     {
850         this(ServiceProvider services = null)
851         {
852             this(CommandLineSettings.init, services);
853         }
854 
855         /++
856          + Params:
857          +  services = The `ServiceProvider` to use for dependency injection.
858          +             If this value is `null`, then a new `ServiceProvider` will be created containing an `ICommandLineInterface` service.
859          + ++/
860         this(CommandLineSettings settings, ServiceProvider services = null)
861         {
862             import std.algorithm : sort;
863             import std.file      : thisExePath;
864             import std.path      : baseName;
865 
866             if(settings.appName is null)
867                 settings.appName = thisExePath.baseName;
868 
869             if(services is null)
870                 services = new ServiceProvider([addCommandLineInterfaceService()]);
871 
872             this._services = services;
873             this._settings = settings;
874             this._resolver = new CommandResolver!CommandInfo();
875 
876             addDefaultCommand();
877 
878             static foreach(mod; Modules)
879                 this.addCommandsFromModule!mod();
880         }
881         
882         /++
883          + Parses the given `args`, and then executes the appropriate command (if one was found).
884          +
885          + Notes:
886          +  If an exception is thrown, the error message is displayed on screen (as well as the stack trace, for non-release builds)
887          +  and then -1 is returned.
888          +
889          + See_Also:
890          +  The documentation for `ArgPullParser` to understand the format for `args`.
891          +
892          + Params:
893          +  args        = The args to parse.
894          +  ignoreFirst = Whether to ignore the first value of `args` or not.
895          +                If `args` is passed as-is from the main function, then the first value will
896          +                be the path to the executable, and should be ignored.
897          +
898          + Returns:
899          +  The status code returned by the command, or -1 if an exception is thrown.
900          + +/
901         int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes)
902         {
903             if(ignoreFirst)
904             {
905                 if(args.length <= 1)
906                     args.length = 0;
907                 else
908                     args = args[1..$];
909             }
910 
911             return this.parseAndExecute(ArgPullParser(args));
912         } 
913 
914         /// ditto
915         int parseAndExecute(ArgPullParser args)
916         {
917             import std.algorithm : filter, any;
918             import std.exception : enforce;
919             import std.stdio     : writefln;
920             import std.format    : format;
921 
922             if(args.empty && this._defaultCommand.isNull)
923             {
924                 writefln(this.makeErrorf("No command was given."));
925                 writefln(this.createAvailableCommandsHelpText(args, "Available commands").toString());
926                 return -1;
927             }
928 
929             Mode mode = Mode.execute;
930 
931             if(this._settings.bashCompletion && args.front.type == ArgTokenType.Text)
932             {
933                 if(args.front.value == "__jcli:complete")
934                     mode = Mode.complete;
935                 else if(args.front.value == "__jcli:bash_complete_script")
936                     mode = Mode.bashCompletion;
937             }
938 
939             ParseResult parseResult;
940 
941             parseResult.argParserBeforeAttempt = args; // If we can't find the exact command, sometimes we can get a partial match when showing help text.
942             parseResult.type                   = ParseResultType.commandFound; // Default to command found.
943             auto result                        = this._resolver.resolveAndAdvance(args);
944 
945             if(!result.success || result.value.type == CommandNodeType.partialWord)
946             {
947                 if(args.containsHelpArgument())
948                 {
949                     parseResult.type = ParseResultType.showHelpText;
950                     if(!this._defaultCommand.isNull)
951                         parseResult.helpText ~= this._defaultCommand.get.helpText.toString();
952 
953                     if(this._resolver.finalWords.length > 0)
954                         parseResult.helpText ~= this.createAvailableCommandsHelpText(parseResult.argParserBeforeAttempt, "Available commands").toString();
955                 }
956                 else if(this._defaultCommand.isNull)
957                 {
958                     parseResult.type      = ParseResultType.commandNotFound;
959                     parseResult.helpText ~= this.makeErrorf("Unknown command '%s'.\n", parseResult.argParserBeforeAttempt.front.value);
960                     parseResult.helpText ~= this.createAvailableCommandsHelpText(parseResult.argParserBeforeAttempt).toString();
961                 }
962                 else
963                     parseResult.command = this._defaultCommand.get;
964             }
965             else
966                 parseResult.command = result.value.userData;
967 
968             parseResult.argParserAfterAttempt = args;
969             parseResult.services              = this._services.createScope(); // Reminder: ServiceScope uses RAII.
970 
971             // Special support: For our default implementation of `ICommandLineInterface`, set its value.
972             auto proxy = cast(ICommandLineInterfaceImpl)parseResult.services.getServiceOrNull!ICommandLineInterface();
973             if(proxy !is null)
974                 proxy._func = &this.parseAndExecute;
975 
976             final switch(mode) with(Mode)
977             {
978                 case execute:        return this.onExecute(parseResult);
979                 case complete:       return this.onComplete(parseResult);
980                 case bashCompletion: return this.onBashCompletionScript();
981             }
982         }
983     }
984 
985     /+ COMMAND DISCOVERY AND REGISTRATION +/
986     private final
987     {
988         void addDefaultCommand()
989         {
990             static if(defaultCommands.length > 0)
991             {
992                 enum UDA = Command("DEFAULT", getSingleUDA!(defaultCommands[0], CommandDefault).description);
993 
994                 _defaultCommand = getCommand!(defaultCommands[0], UDA);
995             }
996         }
997 
998         void addCommandsFromModule(alias Module)()
999         {
1000             import std.traits : getSymbolsByUDA;
1001 
1002             static foreach(symbol; getSymbolsByUDA!(Module, Command))
1003             {{
1004                 static assert(is(symbol == struct) || is(symbol == class),
1005                     "Only structs and classes can be marked with @Command. Issue Symbol = " ~ __traits(identifier, symbol)
1006                 );
1007 
1008                 enum UDA = getSingleUDA!(symbol, Command);
1009                 static assert(UDA.pattern !is null, "Null command names are deprecated, please use `@CommandDefault` instead.");
1010 
1011                 auto info = getCommand!(symbol, UDA);
1012                 info.pattern = UDA;
1013 
1014                 foreach(pattern; info.pattern.pattern.byPatternNames)
1015                     this._resolver.define(pattern, info);
1016             }}
1017         }
1018 
1019         CommandInfo getCommand(T, Command UDA)()
1020         {
1021             // Get arg info.
1022             CommandArguments!T commandArgs = getArgs!T;
1023 
1024             CommandInfo info;
1025             info.helpText   = createHelpText!(T, UDA)(this._settings.appName, commandArgs);
1026             info.doExecute  = createCommandExecuteFunc!T(commandArgs);
1027             info.doComplete = createCommandCompleteFunc!T(commandArgs);
1028 
1029             return info;
1030         }
1031     }
1032 
1033     /+ MODE EXECUTORS +/
1034     private final
1035     {
1036         int onExecute(ref ParseResult result)
1037         {
1038             import std.stdio : writeln, writefln;
1039 
1040             final switch(result.type) with(ParseResultType)
1041             {
1042                 case showHelpText:
1043                     writeln(result.helpText);
1044                     return 0;
1045 
1046                 case commandNotFound:
1047                     writeln(result.helpText);
1048                     return -1;
1049 
1050                 case commandFound: break;
1051             }
1052 
1053             string errorMessage;
1054             auto statusCode = result.command.doExecute(result.argParserAfterAttempt, /*ref*/ errorMessage, result.services, result.command.helpText);
1055 
1056             if(errorMessage !is null)
1057                 writeln(this.makeErrorf(errorMessage));
1058 
1059             return statusCode;
1060         }
1061 
1062         int onComplete(ref ParseResult result)
1063         {
1064             // Parsing here shouldn't be affected by user-defined ArgBinders, so stuff being done here is done manually.
1065             // This way we gain reliability.
1066             //
1067             // Since this is also an internal function, error checking is much more lax.
1068             import std.array     : array;
1069             import std.algorithm : map, filter, splitter, equal, startsWith;
1070             import std.conv      : to;
1071             import std.stdio     : writeln;
1072 
1073             // Expected args:
1074             //  [0]    = COMP_CWORD
1075             //  [1..$] = COMP_WORDS
1076             result.argParserAfterAttempt.popFront(); // Skip __jcli:complete
1077             auto cword = result.argParserAfterAttempt.front.value.to!uint;
1078             result.argParserAfterAttempt.popFront();
1079             auto  words = result.argParserAfterAttempt.map!(t => t.value).array;
1080 
1081             cword -= 1;
1082             words = words[1..$]; // [0] is the exe name, which we don't care about.
1083             auto before  = words[0..cword];
1084             auto current = (cword < words.length)     ? words[cword]      : [];
1085             auto after   = (cword + 1 < words.length) ? words[cword+1..$] : [];
1086 
1087             auto beforeParser = ArgPullParser(before);
1088             auto commandInfo  = this._resolver.resolveAndAdvance(beforeParser);
1089 
1090             // Can't find command, so we're in "display command name" mode.
1091             if(!commandInfo.success || commandInfo.value.type == CommandNodeType.partialWord)
1092             {
1093                 char[] output;
1094                 output.reserve(1024); // Gonna be doing a good bit of concat.
1095 
1096                 // Special case: When we have no text to look for, just display the first word of every command path.
1097                 if(before.length == 0 && current is null)
1098                     commandInfo.value = this._resolver.root;
1099 
1100                 // Otherwise try to match using the existing text.
1101 
1102                 // Display the word of all children of the current command word.
1103                 //
1104                 // If the current argument word isn't null, then use that as a further filter.
1105                 //
1106                 // e.g.
1107                 // Before  = ["name"]
1108                 // Pattern = "name get"
1109                 // Output  = "get"
1110                 foreach(child; commandInfo.value.children)
1111                 {
1112                     if(current.length > 0 && !child.word.startsWith(current))
1113                         continue;
1114 
1115                     output ~= child.word;
1116                     output ~= " ";
1117                 }
1118 
1119                 writeln(output);
1120                 return 0;
1121             }
1122 
1123             // Found command, so we're in "display possible args" mode.
1124             char[] output;
1125             output.reserve(1024);
1126 
1127             commandInfo.value.userData.doComplete(before, current, after, /*ref*/ output); // We need black magic, so this is generated in addCommand.
1128             writeln(output);
1129 
1130             return 0;
1131         }
1132 
1133         int onBashCompletionScript()
1134         {
1135             import std.stdio : writefln;
1136             import std.file  : thisExePath;
1137             import std.path  : baseName;
1138             import jaster.cli.views.bash_complete : BASH_COMPLETION_TEMPLATE;
1139 
1140             const fullPath = thisExePath;
1141             const exeName  = fullPath.baseName;
1142 
1143             writefln(BASH_COMPLETION_TEMPLATE,
1144                 exeName,
1145                 fullPath,
1146                 exeName,
1147                 exeName
1148             );
1149             return 0;
1150         }
1151     }
1152   
1153     /+ COMMAND RUNTIME HELPERS +/
1154     private final
1155     {
1156         static CommandArguments!T getArgs(T)()
1157         {
1158             import std.format : format;
1159             import std.meta   : staticMap, Filter;
1160             import std.traits : isType, hasUDA, isInstanceOf, ReturnType, Unqual, isBuiltinType;
1161 
1162             alias NameToMember(string Name) = __traits(getMember, T, Name);
1163             alias MemberNames               = __traits(allMembers, T);
1164 
1165             CommandArguments!T commandArgs;
1166 
1167             static foreach(symbolName; MemberNames)
1168             {{
1169                 static if(__traits(compiles, NameToMember!symbolName))
1170                 {
1171                     // The postfix is necessary so the below `if` works, without forcing the user to not use the name 'symbol' in their code.
1172                     alias symbol_SOME_RANDOM_CRAP = NameToMember!symbolName; 
1173                     
1174                     // Skip over aliases, nested types, and enums.
1175                     static if(!isType!symbol_SOME_RANDOM_CRAP
1176                         && !is(symbol_SOME_RANDOM_CRAP == enum)
1177                         && __traits(identifier, symbol_SOME_RANDOM_CRAP) != "symbol_SOME_RANDOM_CRAP"
1178                     )
1179                     {
1180                         // I wish there were a convinent way to 'continue' a static foreach...
1181 
1182                         alias Symbol     = symbol_SOME_RANDOM_CRAP;
1183                         alias SymbolType = typeof(Symbol);
1184                         const SymbolName = __traits(identifier, Symbol);
1185 
1186                         enum IsField = (
1187                             isBuiltinType!SymbolType
1188                          || is(SymbolType == struct)
1189                          || is(SymbolType == class)
1190                         );
1191 
1192                         static if(
1193                                 IsField
1194                             && (hasUDA!(Symbol, CommandNamedArg) || hasUDA!(Symbol, CommandPositionalArg))
1195                         ) 
1196                         {
1197                             alias SymbolUDAs = __traits(getAttributes, Symbol);
1198 
1199                             // Determine the argument type.
1200                             static if(hasUDA!(Symbol, CommandNamedArg))
1201                             {
1202                                 NamedArgInfo!T arg;
1203                                 arg.uda = getSingleUDA!(Symbol, CommandNamedArg);
1204                             }
1205                             else static if(hasUDA!(Symbol, CommandPositionalArg))
1206                             {
1207                                 PositionalArgInfo!T arg;
1208                                 arg.uda = getSingleUDA!(Symbol, CommandPositionalArg);
1209 
1210                                 if(arg.uda.name.length == 0)
1211                                     arg.uda.name = "VALUE";
1212                             }
1213                             else static assert(false, "Bug with parent if statement.");
1214 
1215                             // See if the arg is part of a group + transfer some Compile-time info into runtime..
1216                             static if(hasUDA!(Symbol, CommandArgGroup))
1217                                 arg.group = getSingleUDA!(Symbol, CommandArgGroup);
1218 
1219                             arg.isNullable = isInstanceOf!(Nullable, SymbolType);
1220                             arg.isBool     = is(SymbolType == bool) || is(SymbolType == Nullable!bool);
1221 
1222                             // Generate the setter func.
1223                             enum ArgAction = GetArgAction!Symbol;
1224                             arg.action = ArgAction;
1225 
1226                             static if(ArgAction == CommandArgAction.default_)
1227                             {
1228                                 arg.setter = (ArgToken tok, ref T commandInstance)
1229                                 {
1230                                     import std.exception : enforce;
1231                                     import std.conv : to;
1232                                     assert(tok.type == ArgTokenType.Text, tok.to!string);
1233 
1234                                     static if(isInstanceOf!(Nullable, SymbolType))
1235                                     {
1236                                         // The Unqual removes the `inout` that `get` uses.
1237                                         alias ResultT = Unqual!(ReturnType!(SymbolType.get));
1238                                     }
1239                                     else
1240                                         alias ResultT = SymbolType;
1241 
1242                                     auto result = ArgBinderInstance.bind!(ResultT, SymbolUDAs)(tok.value);
1243                                     enforce(result.isSuccess, result.asFailure.error);
1244 
1245                                     mixin("commandInstance.%s = result.asSuccess.value;".format(SymbolName));
1246                                 };
1247                             }
1248                             else static if(ArgAction == CommandArgAction.count)
1249                             {
1250                                 static assert(hasUDA!(Symbol, CommandNamedArg), "The 'count' action is only supported on named arguments.");
1251                                 arg.setter = (ArgToken _, ref T commandInstance)
1252                                 {
1253                                     mixin("commandInstance.%s++;".format(SymbolName));
1254                                 };
1255                                 arg.isNullable = true;
1256                             }
1257                             else static assert(false, "Unsupported arg action. Please report this issue.");
1258 
1259                             static if(hasUDA!(Symbol, CommandNamedArg)) commandArgs.namedArgs ~= arg;
1260                             else                                        commandArgs.positionalArgs ~= arg;
1261                         }
1262                     }
1263                 }
1264             }}
1265 
1266             return commandArgs;
1267         }
1268     }
1269 
1270     /+ UNCATEGORISED HELPERS +/
1271     private final
1272     {
1273         HelpTextBuilderTechnical createAvailableCommandsHelpText(ArgPullParser args, string sectionName = "Did you mean")
1274         {
1275             import std.array     : array;
1276             import std.algorithm : filter, sort, map, splitter, uniq;
1277 
1278             auto command = this._resolver.root;
1279             auto result  = this._resolver.resolveAndAdvance(args);
1280             if(result.success)
1281                 command = result.value;
1282 
1283             auto builder = new HelpTextBuilderTechnical();
1284             builder.addSection(sectionName)
1285                    .addContent(
1286                        new HelpSectionArgInfoContent(
1287                            command.finalWords
1288                                   .uniq!((a, b) => a.userData.pattern == b.userData.pattern)
1289                                   .map!(c => HelpSectionArgInfoContent.ArgInfo(
1290                                        [c.userData.pattern.byPatternNames.front],
1291                                        c.userData.pattern.description,
1292                                        ArgIsOptional.no
1293                                   ))
1294                                   .array
1295                                   .sort!"a.names[0] < b.names[0]"
1296                                   .array, // eww...
1297                             AutoAddArgDashes.no
1298                        )
1299             );
1300 
1301             return builder;
1302         }
1303 
1304         string makeErrorf(Args...)(string formatString, Args args)
1305         {
1306             import std.format : format;
1307             return "%s: %s".format(this._settings.appName, formatString.format(args));
1308         }
1309     }
1310 }
1311 
1312 // HELPER FUNCS
1313 
1314 private alias AllowPartialMatch = Flag!"partialMatch";
1315 
1316 private auto byPatternNames(string pattern)
1317 {
1318     import std.algorithm : splitter;
1319     return pattern.splitter('|');
1320 }
1321 
1322 private auto byPatternNames(T)(T uda)
1323 if(is(T == struct))
1324 {
1325     return uda.pattern.byPatternNames();
1326 }
1327 
1328 bool containsHelpArgument(ArgPullParser args)
1329 {
1330     import std.algorithm : any;
1331 
1332     return args.any!(t => t.type == ArgTokenType.ShortHandArgument && t.value == "h"
1333                        || t.type == ArgTokenType.LongHandArgument && t.value == "help");
1334 }
1335 
1336 bool matchSpacelessPattern(string pattern, string toTestAgainst)
1337 {
1338     import std.algorithm : any;
1339 
1340     return pattern.byPatternNames.any!(str => str == toTestAgainst);
1341 }
1342 ///
1343 unittest
1344 {
1345     assert(matchSpacelessPattern("v|verbose", "v"));
1346     assert(matchSpacelessPattern("v|verbose", "verbose"));
1347     assert(!matchSpacelessPattern("v|verbose", "lalafell"));
1348 }
1349 
1350 bool matchSpacefullPattern(string pattern, ref ArgPullParser parser, AllowPartialMatch allowPartial = AllowPartialMatch.no)
1351 {
1352     import std.algorithm : splitter;
1353 
1354     foreach(subpattern; pattern.byPatternNames)
1355     {
1356         auto savedParser = parser.save();
1357         bool isAMatch = true;
1358         bool isAPartialMatch = false;
1359         foreach(split; subpattern.splitter(" "))
1360         {
1361             if(savedParser.empty
1362             || !(savedParser.front.type == ArgTokenType.Text && savedParser.front.value == split))
1363             {
1364                 isAMatch = false;
1365                 break;
1366             }
1367 
1368             isAPartialMatch = true;
1369             savedParser.popFront();
1370         }
1371 
1372         if(isAMatch
1373         || (isAPartialMatch && allowPartial))
1374         {
1375             parser = savedParser;
1376             return true;
1377         }
1378     }
1379 
1380     return false;
1381 }
1382 ///
1383 unittest
1384 {
1385     // Test empty parsers.
1386     auto parser = ArgPullParser([]);
1387     assert(!matchSpacefullPattern("v", parser));
1388 
1389     // Test that the parser's position is moved forward correctly.
1390     parser = ArgPullParser(["v", "verbose"]);
1391     assert(matchSpacefullPattern("v", parser));
1392     assert(matchSpacefullPattern("verbose", parser));
1393     assert(parser.empty);
1394 
1395     // Test that a parser that fails to match isn't moved forward at all.
1396     parser = ArgPullParser(["v", "verbose"]);
1397     assert(!matchSpacefullPattern("lel", parser));
1398     assert(parser.front.value == "v");
1399 
1400     // Test that a pattern with spaces works.
1401     parser = ArgPullParser(["give", "me", "chocolate"]);
1402     assert(matchSpacefullPattern("give me", parser));
1403     assert(parser.front.value == "chocolate");
1404 
1405     // Test that multiple patterns work.
1406     parser = ArgPullParser(["v", "verbose"]);
1407     assert(matchSpacefullPattern("lel|v|verbose", parser));
1408     assert(matchSpacefullPattern("lel|v|verbose", parser));
1409     assert(parser.empty);
1410 }
1411 
1412 version(unittest)
1413 {
1414     import jaster.cli.result;
1415     private alias InstansiationTest = CommandLineInterface!(jaster.cli.core);
1416 
1417     // NOTE: The only reason it can see and use private @Commands is because they're in the same module.
1418     @Command("execute t|execute test|et|e test", "This is a test command")
1419     private struct CommandTest
1420     {
1421         // These are added to test that they are safely ignored.
1422         alias al = int;
1423         enum e = 2;
1424         struct S
1425         {
1426         }
1427         void f () {}
1428 
1429         @CommandNamedArg("a|avar", "A variable")
1430         int a;
1431 
1432         @CommandPositionalArg(0, "b")
1433         Nullable!string b;
1434 
1435         @CommandNamedArg("c")
1436         Nullable!bool c;
1437 
1438         int onExecute()
1439         {
1440             import std.conv : to;
1441             
1442             return (b.isNull || !c.isNull) ? 0
1443                                            : (b.get() == a.to!string) ? 1
1444                                                                       : -1;
1445         }
1446     }
1447 
1448     // Should always return 1 via `CommandTest`
1449     @Command("test injection")
1450     private struct CallCommandTest
1451     {
1452         private ICommandLineInterface _cli;
1453         this(ICommandLineInterface cli)
1454         {
1455             this._cli = cli;
1456             assert(cli !is null);
1457         }
1458 
1459         int onExecute()
1460         {
1461             return this._cli.parseAndExecute(["et", "20", "-a 20"], IgnoreFirstArg.no);
1462         }
1463     }
1464 
1465     @CommandDefault("This is the default command.")
1466     private struct DefaultCommandTest
1467     {
1468         @CommandNamedArg("var", "A variable")
1469         int a;
1470 
1471         int onExecute()
1472         {
1473             return a % 2 == 0
1474             ? a
1475             : 0;
1476         }
1477     }
1478 
1479     @("General test")
1480     unittest
1481     {
1482         auto cli = new CommandLineInterface!(jaster.cli.core);
1483         assert(cli.parseAndExecute(["execute", "t", "-a 20"],              IgnoreFirstArg.no) == 0); // b is null
1484         assert(cli.parseAndExecute(["execute", "test", "20", "--avar 21"], IgnoreFirstArg.no) == -1); // a and b don't match
1485         assert(cli.parseAndExecute(["et", "20", "-a 20"],                  IgnoreFirstArg.no) == 1); // a and b match
1486         assert(cli.parseAndExecute(["e", "test", "20", "-a 20", "-c"],     IgnoreFirstArg.no) == 0); // -c is used
1487     }
1488 
1489     @("Test ICommandLineInterface injection")
1490     unittest
1491     {
1492         auto provider = new ServiceProvider([
1493             addCommandLineInterfaceService()
1494         ]);
1495 
1496         auto cli = new CommandLineInterface!(jaster.cli.core)(provider);
1497         assert(cli.parseAndExecute(["test", "injection"], IgnoreFirstArg.no) == 1);
1498     }
1499 
1500     @("Default command test")
1501     unittest
1502     {
1503         auto cli = new CommandLineInterface!(jaster.cli.core);
1504         assert(cli.parseAndExecute(["--var 1"], IgnoreFirstArg.no) == 0);
1505         assert(cli.parseAndExecute(["--var 2"], IgnoreFirstArg.no) == 2);
1506     }
1507 
1508     @Command("booltest", "Bool test")
1509     private struct BoolTestCommand
1510     {
1511         @CommandNamedArg("a")
1512         bool definedNoValue;
1513 
1514         @CommandNamedArg("b")
1515         bool definedFalseValue;
1516 
1517         @CommandNamedArg("c")
1518         bool definedTrueValue;
1519 
1520         @CommandNamedArg("d")
1521         bool definedNoValueWithArg;
1522 
1523         @CommandPositionalArg(0)
1524         string comesAfterD;
1525 
1526         void onExecute()
1527         {
1528             assert(this.definedNoValue,            "-a doesn't equal true.");
1529             assert(!this.definedFalseValue,        "-b=false doesn't equal false");
1530             assert(this.definedTrueValue,          "-c true doesn't equal true");
1531             assert(this.definedNoValueWithArg,     "-d Lalafell doesn't equal true");
1532             assert(this.comesAfterD == "Lalafell", "Lalafell was eaten incorrectly.");
1533         }
1534     }
1535     @("Test that booleans are handled properly")
1536     unittest
1537     {
1538         auto cli = new CommandLineInterface!(jaster.cli.core);
1539         assert(
1540             cli.parseAndExecute(
1541                 ["booltest", "-a", "-b=false", "-c", "true", "-d", "Lalafell"],
1542                 // Unforunately due to ArgParser discarding some info, "-d=Lalafell" won't become an error as its treated the same as "-d Lalafell".
1543                 IgnoreFirstArg.no
1544             ) == 0
1545         );
1546     }
1547 
1548     @Command("rawListTest", "Test raw lists")
1549     private struct RawListTestCommand
1550     {
1551         @CommandNamedArg("a")
1552         bool dummyThicc;
1553 
1554         @CommandRawArg
1555         string[] rawList;
1556 
1557         void onExecute()
1558         {
1559             assert(rawList.length == 2);
1560         }
1561     }
1562     @("Test that raw lists work")
1563     unittest
1564     {
1565         auto cli = new CommandLineInterface!(jaster.cli.core);
1566 
1567         // Legacy triple dash.
1568         assert(
1569             cli.parseAndExecute(
1570                 ["rawListTest", "-a", "---", "raw1", "raw2"],
1571                 IgnoreFirstArg.no
1572             ) == 0
1573         );
1574 
1575         // Double dash
1576         assert(
1577             cli.parseAndExecute(
1578                 ["rawListTest", "-a", "--", "raw1", "raw2"],
1579                 IgnoreFirstArg.no
1580             ) == 0
1581         );
1582     }
1583 
1584     @ArgValidator
1585     private struct Expect(T)
1586     {
1587         T value;
1588 
1589         Result!void onValidate(T boundValue)
1590         {
1591             import std.format : format;
1592 
1593             return this.value == boundValue
1594             ? Result!void.success()
1595             : Result!void.failure("Expected value to equal '%s', not '%s'.".format(this.value, boundValue));
1596         }
1597     }
1598 
1599     @Command("validationTest", "Test validation")
1600     private struct ValidationTestCommand
1601     {
1602         @CommandPositionalArg(0)
1603         @Expect!string("lol")
1604         string value;
1605         
1606         void onExecute(){}
1607     }
1608     @("Test ArgBinder validation integration")
1609     unittest
1610     {
1611         auto cli = new CommandLineInterface!(jaster.cli.core);
1612         assert(
1613             cli.parseAndExecute(
1614                 ["validationTest", "lol"],
1615                 IgnoreFirstArg.no
1616             ) == 0
1617         );
1618 
1619         assert(
1620             cli.parseAndExecute(
1621                 ["validationTest", "non"],
1622                 IgnoreFirstArg.no
1623             ) == -1
1624         );
1625     }
1626 
1627     @Command("arg group test", "Test arg groups work")
1628     private struct ArgGroupTestCommand
1629     {
1630         @CommandPositionalArg(0)
1631         string a;
1632 
1633         @CommandNamedArg("b")
1634         string b;
1635 
1636         @CommandArgGroup("group1", "This is group 1")
1637         {
1638             @CommandPositionalArg(1)
1639             string c;
1640 
1641             @CommandNamedArg("d")
1642             string d;
1643         }
1644 
1645         void onExecute(){}
1646     }
1647     @("Test that @CommandArgGroup is handled properly.")
1648     unittest
1649     {
1650         import std.algorithm : canFind;
1651 
1652         // Accessing a lot of private state here, but that's because we don't have a mechanism to extract the output properly.
1653         auto cli = new CommandLineInterface!(jaster.cli.core);
1654         auto helpText = cli._resolver.resolve("arg group test").value.userData.helpText;
1655 
1656         assert(helpText.toString().canFind(
1657             "group1:\n"
1658            ~"    This is group 1\n"
1659            ~"\n"
1660            ~"    VALUE"
1661         ));
1662     }
1663 
1664     @Command("arg action count", "Test that the count arg action works")
1665     private struct ArgActionCount
1666     {
1667         @CommandNamedArg("c")
1668         @(CommandArgAction.count)
1669         int c;
1670 
1671         int onExecute()
1672         {
1673             return this.c;
1674         }
1675     }
1676     @("Test that CommandArgAction.count works.")
1677     unittest
1678     {
1679         auto cli = new CommandLineInterface!(jaster.cli.core);
1680 
1681         assert(cli.parseAndExecute(["arg", "action", "count"], IgnoreFirstArg.no) == 0);
1682         assert(cli.parseAndExecute(["arg", "action", "count", "-c"], IgnoreFirstArg.no) == 1);
1683         assert(cli.parseAndExecute(["arg", "action", "count", "-c", "-c"], IgnoreFirstArg.no) == 2);
1684         assert(cli.parseAndExecute(["arg", "action", "count", "-ccccc"], IgnoreFirstArg.no) == 5);
1685         assert(cli.parseAndExecute(["arg", "action", "count", "-ccv"], IgnoreFirstArg.no) == -1); // -ccv -> [name '-c', positional 'cv']. -1 because too many positional args.
1686 
1687         // Unfortunately this case also works because of limitations in ArgPullParser
1688         assert(cli.parseAndExecute(["arg", "action", "count", "-c", "cccc"], IgnoreFirstArg.no) == 5);
1689     }
1690 }