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, jaster.cli.infogen, jaster.cli.commandparser, jaster.cli.result;
9     import jaster.ioc;
10 }
11 
12 public
13 {
14     import std.typecons : Nullable;
15 }
16 
17 /// 
18 alias IgnoreFirstArg = Flag!"ignoreFirst";
19 
20 private alias CommandExecuteFunc = Result!int delegate(ArgPullParser parser, scope ref ServiceScope services, HelpTextBuilderSimple helpText);
21 private alias CommandCompleteFunc = void delegate(string[] before, string current, string[] after, ref char[] output);
22 
23 /// See `CommandLineSettings.sink`
24 alias CommandLineSinkFunc = void delegate(string text);
25 
26 /++
27  + A service that allows commands to access the `CommandLineInterface.parseAndExecute` function of the command's `CommandLineInterface`.
28  +
29  + Notes:
30  +  You **must** use `addCommandLineInterfaceService` to add the default implementation of this service into your `ServiceProvider`, you can of course
31  +  create your own implementation, but note that `CommandLineInterface` has special support for the default implementation.
32  +
33  +  Alternatively, don't pass a `ServiceProvider` into your `CommandLineInterface`, and it'll create this service by itself.
34  + ++/
35 interface ICommandLineInterface
36 {
37     /// See: `CommandLineInterface.parseAndExecute`
38     int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes);
39 }
40 
41 private final class ICommandLineInterfaceImpl : ICommandLineInterface
42 {
43     alias ParseAndExecuteT = int delegate(string[], IgnoreFirstArg);
44 
45     private ParseAndExecuteT _func;
46 
47     override int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes)
48     {
49         return this._func(args, ignoreFirst);
50     }
51 }
52 
53 /++
54  + Returns:
55  +  A Singleton `ServiceInfo` providing the default implementation for `ICommandLineInterface`.
56  + ++/
57 ServiceInfo addCommandLineInterfaceService()
58 {
59     return ServiceInfo.asSingleton!(ICommandLineInterface, ICommandLineInterfaceImpl);
60 }
61 
62 /// ditto.
63 ServiceInfo[] addCommandLineInterfaceService(ref ServiceInfo[] services)
64 {
65     services ~= addCommandLineInterfaceService();
66     return services;
67 }
68 
69 /+ COMMAND INFO CREATOR FUNCTIONS +/
70 private HelpTextBuilderSimple createHelpText(alias CommandT, alias ArgBinderInstance)(string appName)
71 {
72     import jaster.cli.commandhelptext;
73     return CommandHelpText!(CommandT, ArgBinderInstance).init.toBuilder(appName);
74 }
75 
76 private CommandCompleteFunc createCommandCompleteFunc(alias CommandT, alias ArgBinderInstance)()
77 {
78     import std.algorithm : filter, map, startsWith, splitter, canFind;
79     import std.exception : assumeUnique;
80 
81     enum Info = getCommandInfoFor!(CommandT, ArgBinderInstance);
82 
83     return (string[] before, string current, string[] after, ref char[] output)
84     {
85         // Check if there's been a null ("--") or '-' ("---"), and if there has, don't bother with completion.
86         // Because anything past that is of course, the raw arg list.
87         if(before.canFind(null) || before.canFind("-"))
88             return;
89 
90         // See if the previous value was a non-boolean argument.
91         const justBefore               = ArgPullParser(before[$-1..$]).front;
92         auto  justBeforeNamedArgResult = Info.namedArgs.filter!(a => a.uda.pattern.matchSpaceless(justBefore.value));
93         if((justBefore.type == ArgTokenType.LongHandArgument || justBefore.type == ArgTokenType.ShortHandArgument)
94         && (!justBeforeNamedArgResult.empty && justBeforeNamedArgResult.front.parseScheme != CommandArgParseScheme.bool_))
95         {
96             // TODO: In the future, add support for specifying values to a parameter, either static and/or dynamically.
97             return;
98         }
99 
100         // Otherwise, we either need to autocomplete an argument's name, or something else that's predefined.
101 
102         string[] names;
103         names.reserve(Info.namedArgs.length * 2);
104 
105         foreach(arg; Info.namedArgs)
106         {
107             foreach(pattern; arg.uda.pattern.byEach)
108             {
109                 // Reminder: Confusingly for this use case, arguments don't have their leading dashes in the before and after arrays.
110                 if(before.canFind(pattern) || after.canFind(pattern))
111                     continue;
112 
113                 names ~= pattern;
114             }
115         }
116 
117         foreach(name; names.filter!(n => n.startsWith(current)))
118         {
119             output ~= (name.length == 1) ? "-" : "--";
120             output ~= name;
121             output ~= ' ';
122         }
123     };
124 }
125 
126 private CommandExecuteFunc createCommandExecuteFunc(alias CommandT, alias ArgBinderInstance)(CommandLineSettings settings)
127 {
128     import std.format    : format;
129     import std.algorithm : filter, map;
130     import std.exception : enforce, collectException;
131 
132     enum Info = getCommandInfoFor!(CommandT, ArgBinderInstance);
133 
134     // This is expecting the parser to have already read in the command's name, leaving only the args.
135     return (ArgPullParser parser, scope ref ServiceScope services, HelpTextBuilderSimple helpText)
136     {
137         if(containsHelpArgument(parser))
138         {
139             settings.sink.get()(helpText.toString() ~ '\n');
140             return Result!int.success(0);
141         }
142 
143         // Cross-stage state.
144         CommandT commandInstance;
145 
146         // Create the command and fetch its arg info.
147         commandInstance = Injector.construct!CommandT(services);
148         static if(is(T == class))
149             assert(commandInstance !is null, "Dependency injection failed somehow.");
150 
151         // Execute stages
152         auto commandParser = CommandParser!(CommandT, ArgBinderInstance)();
153         auto parseResult = commandParser.parse(parser, commandInstance);
154         if(!parseResult.isSuccess)
155             return Result!int.failure(parseResult.asFailure.error);
156 
157         return onExecuteRunCommand!CommandT(/*ref*/ commandInstance);
158     };
159 }
160 
161 private Result!int onExecuteRunCommand(alias T)(ref T commandInstance)
162 {
163     static assert(
164         __traits(compiles, commandInstance.onExecute())
165      || __traits(compiles, { int code = commandInstance.onExecute(); }),
166         "Unable to call the `onExecute` function for command `"~__traits(identifier, T)~"` please ensure it's signature matches either:"
167         ~"\n\tvoid onExecute();"
168         ~"\n\tint onExecute();"
169     );
170 
171     try
172     {
173         static if(__traits(compiles, {int i = commandInstance.onExecute();}))
174             return Result!int.success(commandInstance.onExecute());
175         else
176         {
177             commandInstance.onExecute();
178             return Result!int.success(0);
179         }
180     }
181     catch(Exception ex)
182     {
183         auto error = ex.msg;
184         debug error ~= "\n\nSTACK TRACE:\n" ~ ex.info.toString(); // trace info
185         return Result!int.failure(error);
186     }
187 }
188 
189 
190 /++
191  + Settings that can be provided to `CommandLineInterface` to change certain behaviour.
192  + ++/
193 struct CommandLineSettings
194 {
195     /++
196      + The name of your application, this is only used when displaying error messages and help text.
197      +
198      + If left as `null`, then the executable's name is used instead.
199      + ++/
200     Nullable!string appName;
201 
202     /++
203      + Whether or not `CommandLineInterface` should provide bash completion. Defaults to `false`.
204      +
205      + See_Also: The README for this project.
206      + ++/
207     bool bashCompletion = false;
208 
209     /++
210      + A user-defined sink to call whenever `CommandLineInterface` itself (not it's subcomponents or commands) wants to
211      + output text.
212      +
213      + If left as `null`, then a default sink is made where `std.stdio.write` is used.
214      +
215      + Notes:
216      +  Strings passed to this function will already include a leading new line character where needed.
217      + ++/
218     Nullable!CommandLineSinkFunc sink;
219 }
220 
221 /++
222  + Provides the functionality of parsing command line arguments, and then calling a command.
223  +
224  + Description:
225  +  The `Modules` template parameter is used directly with `jaster.cli.binder.ArgBinder` to provide the arg binding functionality.
226  +  Please refer to `ArgBinder`'s documentation if you are wanting to use custom made binder funcs.
227  +
228  +  Commands are detected by looking over every module in `Modules`, and within each module looking for types marked with `@Command` and matching their patterns
229  +  to the given input.
230  +
231  + Patterns:
232  +  Patterns are pretty simple.
233  +
234  +  Example #1: The pattern "run" will match if the given command line args starts with "run".
235  +
236  +  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)
237  +
238  +  Example #3: The pattern "r|run" will match if the given command line args starts with "r", or "run".
239  +
240  +  Longer patterns take higher priority than shorter ones.
241  +
242  +  Patterns with spaces are only allowed inside of `@Command` pattern UDAs. The `@CommandNamedArg` UDA is a bit more special.
243  +
244  +  For `@CommandNamedArg`, spaces are not allowed, since named arguments can't be split into spaces.
245  +
246  +  For `@CommandNamedArg`, patterns or subpatterns (When "|" is used to have multiple patterns) will be treated differently depending on their length.
247  +  For patterns with only 1 character, they will be matched using short-hand argument form (See `ArgPullParser`'s documentation).
248  +  For pattern with more than 1 character, they will be matched using long-hand argument form.
249  +
250  +  Example #4: The pattern (for `@CommandNamedArg`) "v|verbose" will match when either "-v" or "--verbose" is used.
251  +
252  +  Internally, `CommandResolver` is used to perform command resolution, and a solution custom to `CommandLineInterface` is used for everything else
253  +  regarding patterns.
254  +
255  + Commands:
256  +  A command is a struct or class that is marked with `@Command`.
257  +
258  +  A default command can be specified using `@CommandDefault` instead.
259  +
260  +  Commands have only one requirement - They have a function called `onExecute`.
261  +
262  +  The `onExecute` function is called whenever the command's pattern is matched with the command line arguments.
263  +
264  +  The `onExecute` function must be compatible with one of these signatures:
265  +      `void onExecute();`
266  +      `int onExecute();`
267  +
268  +  The signature that returns an `int` is used to return a custom status code.
269  +
270  +  If a command has its pattern matched, then its arguments will be parsed before `onExecute` is called.
271  +
272  +  Arguments are either positional (`@CommandPositionalArg`) or named (`@CommandNamedArg`).
273  +
274  + Dependency_Injection:
275  +  Whenever a command object is created, it is created using dependency injection (via the `jioc` library).
276  +
277  +  Each command is given its own service scope, even when a command calls another command.
278  +
279  + Positional_Arguments:
280  +  A positional arg is an argument that appears in a certain 'position'. For example, imagine we had a command that we wanted to
281  +  execute by using `"myTool create SomeFile.txt \"This is some content\""`.
282  +
283  +  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".
284  +  We are then left with the other two strings.
285  +
286  +  `"SomeFile.txt"` is in the 0th position, so its value will be binded to the field marked with `@CommandPositionalArg(0)`.
287  +
288  +  `"This is some content"` is in the 1st position, so its value will be binded to the field marked with `@CommandPositionalArg(1)`.
289  +
290  + Named_Arguments:
291  +  A named arg is an argument that follows a name. Names are either in long-hand form ("--file") or short-hand form ("-f").
292  +
293  +  For example, imagine we execute a custom tool with `"myTool create -f=SomeFile.txt --content \"This is some content\""`.
294  +
295  +  The shell will pass `["create", "-f=SomeFile.txt", "--content", "This is some content"]`. Notice how the '-f' uses an '=' sign, but '--content' doesn't.
296  +  This is because the `ArgPullParser` supports various different forms of named arguments (e.g. ones that use '=', and ones that don't).
297  +  Please refer to its documentation for more information.
298  +
299  +  Imagine we already have a command made that matches with "create". We are then left with the rest of the arguments.
300  +
301  +  "-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), 
302  +  we perform the binding of "SomeFile.txt" to whichever field marked with `@CommandNamedArg` matches with the name "f".
303  +
304  +  `["--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.
305  +
306  + Binding_Arguments:
307  +  Once we have matched a field marked with either `@CommandPositionalArg` or `@CommandNamedArg` with a position or name (respectively), then we
308  +  need to bind the value to the field.
309  +
310  +  This is where the `ArgBinder` is used. First of all, please refer to its documentation as it's kind of important.
311  +  Second of all, we esentially generate a call similar to: `ArgBinderInstance.bind(myCommandInstance.myMatchedField, valueToBind)`
312  +
313  +  So imagine we have this field inside a command - `@CommandPositionalArg(0) int myIntField;`
314  +
315  +  Now imagine we have the value "200" in the 0th position. This means it'll be matchd with `myIntField`.
316  +
317  +  This will esentially generate this call: `ArgBinderInstance.bind(myCommandInstance.myIntField, "200")`
318  +
319  +  From there, ArgBinder will do its thing of binding/converting the string "200" into the integer 200.
320  +
321  +  `ArgBinder` has support for user-defined binders (in fact, all of the built-in binders use this mechanism!). Please
322  +  refer to its documentation for more information, or see example-04.
323  +
324  +  You can also specify validation for arguments, by attaching structs (that match the definition specified in `ArgBinder`'s documentation) as
325  +  UDAs onto your fields.
326  +
327  +  $(B Beware) you need to attach your validation struct as `@Struct()` (or with args) and not `@Struct`, notice the first one has parenthesis.
328  +
329  + Boolean_Binding:
330  +  Bool arguments have special logic in place.
331  +
332  +  By only passing the name of a boolean argument (e.g. "--verbose"), this is treated as setting "verbose" to "true" using the `ArgBinder`.
333  +
334  +  By passing a value alongside a boolean argument that is either "true" or "false" (e.g. "--verbose true", "--verbose=false"), then the resulting
335  +  value is passed to the `ArgBinder` as usual. In other words, "--verbose" is equivalent to "--verbose true".
336  +
337  +  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;
338  +  the boolean argument will be set to true.
339  +
340  +  For example, "--verbose" sets "verbose" to "true". Passing "--verbose=false/true" will set "verbose" to "false" or "true" respectively. Passing
341  +  "--verbose push" would leave "push" as a positional argument, and then set "verbose" to "true".
342  +
343  +  These special rules are made so that boolean arguments can be given an explicit value, without them 'randomly' treating positional arguments as their value.
344  +
345  + Optional_And_Required_Arguments:
346  +  By default, all arguments are required.
347  +
348  +  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.
349  +
350  +  Note that `Nullable` is publicly imported by this module, for ease of use.
351  +
352  +  Before a nullable argument is binded, it is first lowered down into its base type before being passed to the `ArgBinder`.
353  +  In other words, a `Nullable!int` argument will be treated as a normal `int` by the ArgBinder.
354  +
355  +  If **any** required argument is not provided by the user, then an exception is thrown (which in turn ends up showing an error message).
356  +  This does not occur with missing optional arguments.
357  +
358  + Raw_Arguments:
359  +  For some applications, they may allow the ability for the user to provide a set of unparsed arguments. For example, dub allows the user
360  +  to provide a set of arguments to the resulting output, when using the likes of `dub run`, e.g. `dub run -- value1 value2 etc.`
361  +
362  +  `CommandLineInterface` also provides this ability. You can use either the double dash like in dub ('--') or a triple dash (legacy reasons, '---').
363  +
364  +  After that, as long as your command contains a `string[]` field marked with `@CommandRawListArg`, then any args after the triple dash are treated as "raw args" - they
365  +  won't be parsed, passed to the ArgBinder, etc. they'll just be passed into the variable as-is.
366  +
367  +  For example, you have the following member in a command `@CommandRawListArg string[] rawList;`, and you are given the following command - 
368  +  `["command", "value1", "--", "rawValue1", "rawValue2"]`, which will result in `rawList`'s value becoming `["rawValue1", "rawValue2"]`
369  +
370  + Arguments_Groups:
371  +  Arguments can be grouped together so they are displayed in a more logical manner within your command's help text.
372  +
373  +  The recommended way to make an argument group, is to create an `@CommandArgGroup` UDA block:
374  +
375  +  ```
376  +  @CommandArgGroup("Debug", "Flags relating the debugging.")
377  +  {
378  +      @CommandNamedArg("trace|t", "Enable tracing") Nullable!bool trace;
379  +      ...
380  +  }
381  +  ```
382  +
383  +  While you *can* apply the UDA individually to each argument, there's one behaviour that you should be aware of - the group's description
384  +  as displayed in the help text will use the description of the $(B last) found `@CommandArgGroup` UDA.
385  +
386  + Params:
387  +  Modules = The modules that contain the commands and/or binder funcs to use.
388  +
389  + See_Also:
390  +  `jaster.cli.infogen` if you'd like to introspect information about commands yourself.
391  +
392  +  `jaster.cli.commandparser` if you only require the ability to parse commands.
393  + +/
394 final class CommandLineInterface(Modules...)
395 {
396     private alias DefaultCommands = getSymbolsByUDAInModules!(CommandDefault, Modules);
397     static assert(DefaultCommands.length <= 1, "Multiple default commands defined " ~ DefaultCommands.stringof);
398 
399     static if(DefaultCommands.length > 0)
400     {
401         static assert(is(DefaultCommands[0] == struct) || is(DefaultCommands[0] == class),
402             "Only structs and classes can be marked with @CommandDefault. Issue Symbol = " ~ __traits(identifier, DefaultCommands[0])
403         );
404         static assert(!hasUDA!(DefaultCommands[0], Command),
405             "Both @CommandDefault and @Command are used for symbol " ~ __traits(identifier, DefaultCommands[0])
406         );
407     }
408 
409     alias ArgBinderInstance = ArgBinder!Modules;
410 
411     private enum Mode
412     {
413         execute,
414         complete,
415         bashCompletion
416     }
417 
418     private enum ParseResultType
419     {
420         commandFound,
421         commandNotFound,
422         showHelpText
423     }
424 
425     private struct ParseResult
426     {
427         ParseResultType type;
428         CommandInfo     command;
429         string          helpText;
430         ArgPullParser   argParserAfterAttempt;
431         ArgPullParser   argParserBeforeAttempt;
432         ServiceScope    services;
433     }
434 
435     private struct CommandInfo
436     {
437         Pattern               pattern; // Patterns (and their helper functions) are still being kept around, so previous code can work unimpeded from the migration to CommandResolver.
438         string                description;
439         HelpTextBuilderSimple helpText;
440         CommandExecuteFunc    doExecute;
441         CommandCompleteFunc   doComplete;
442     }
443 
444     /+ VARIABLES +/
445     private
446     {
447         CommandResolver!CommandInfo _resolver;
448         CommandLineSettings         _settings;
449         ServiceProvider             _services;
450         Nullable!CommandInfo        _defaultCommand;
451     }
452 
453     /+ PUBLIC INTERFACE +/
454     public final
455     {
456         this(ServiceProvider services = null)
457         {
458             this(CommandLineSettings.init, services);
459         }
460 
461         /++
462          + Params:
463          +  services = The `ServiceProvider` to use for dependency injection.
464          +             If this value is `null`, then a new `ServiceProvider` will be created containing an `ICommandLineInterface` service.
465          + ++/
466         this(CommandLineSettings settings, ServiceProvider services = null)
467         {
468             import std.algorithm : sort;
469             import std.file      : thisExePath;
470             import std.path      : baseName;
471             import std.stdio     : write;
472 
473             if(settings.appName.isNull)
474                 settings.appName = thisExePath.baseName;
475 
476             if(settings.sink.isNull)
477                 settings.sink = (string str) { write(str); };
478 
479             if(services is null)
480                 services = new ServiceProvider([addCommandLineInterfaceService()]);
481 
482             this._services = services;
483             this._settings = settings;
484             this._resolver = new CommandResolver!CommandInfo();
485 
486             addDefaultCommand();
487 
488             static foreach(mod; Modules)
489                 this.addCommandsFromModule!mod();
490         }
491         
492         /++
493          + Parses the given `args`, and then executes the appropriate command (if one was found).
494          +
495          + Notes:
496          +  If an exception is thrown, the error message is displayed on screen (as well as the stack trace, for non-release builds)
497          +  and then -1 is returned.
498          +
499          + See_Also:
500          +  The documentation for `ArgPullParser` to understand the format for `args`.
501          +
502          + Params:
503          +  args        = The args to parse.
504          +  ignoreFirst = Whether to ignore the first value of `args` or not.
505          +                If `args` is passed as-is from the main function, then the first value will
506          +                be the path to the executable, and should be ignored.
507          +
508          + Returns:
509          +  The status code returned by the command, or -1 if an exception is thrown.
510          + +/
511         int parseAndExecute(string[] args, IgnoreFirstArg ignoreFirst = IgnoreFirstArg.yes)
512         {
513             if(ignoreFirst)
514             {
515                 if(args.length <= 1)
516                     args.length = 0;
517                 else
518                     args = args[1..$];
519             }
520 
521             return this.parseAndExecute(ArgPullParser(args));
522         } 
523 
524         /// ditto
525         int parseAndExecute(ArgPullParser args)
526         {
527             import std.algorithm : filter, any;
528             import std.exception : enforce;
529             import std.format    : format;
530 
531             if(args.empty && this._defaultCommand.isNull)
532             {
533                 this.writeln(this.makeErrorf("No command was given."));
534                 this.writeln(this.createAvailableCommandsHelpText(args, "Available commands").toString());
535                 return -1;
536             }
537 
538             Mode mode = Mode.execute;
539 
540             if(this._settings.bashCompletion && args.front.type == ArgTokenType.Text)
541             {
542                 if(args.front.value == "__jcli:complete")
543                     mode = Mode.complete;
544                 else if(args.front.value == "__jcli:bash_complete_script")
545                     mode = Mode.bashCompletion;
546             }
547 
548             ParseResult parseResult;
549 
550             parseResult.argParserBeforeAttempt = args; // If we can't find the exact command, sometimes we can get a partial match when showing help text.
551             parseResult.type                   = ParseResultType.commandFound; // Default to command found.
552             auto result                        = this._resolver.resolveAndAdvance(args);
553 
554             if(!result.success || result.value.type == CommandNodeType.partialWord)
555             {
556                 if(args.containsHelpArgument())
557                 {
558                     parseResult.type = ParseResultType.showHelpText;
559                     if(!this._defaultCommand.isNull)
560                         parseResult.helpText ~= this._defaultCommand.get.helpText.toString();
561 
562                     if(this._resolver.finalWords.length > 0)
563                         parseResult.helpText ~= this.createAvailableCommandsHelpText(parseResult.argParserBeforeAttempt, "Available commands").toString();
564                 }
565                 else if(this._defaultCommand.isNull)
566                 {
567                     parseResult.type      = ParseResultType.commandNotFound;
568                     parseResult.helpText ~= this.makeErrorf("Unknown command '%s'.\n", parseResult.argParserBeforeAttempt.front.value);
569                     parseResult.helpText ~= this.createAvailableCommandsHelpText(parseResult.argParserBeforeAttempt).toString();
570                 }
571                 else
572                     parseResult.command = this._defaultCommand.get;
573             }
574             else
575                 parseResult.command = result.value.userData;
576 
577             parseResult.argParserAfterAttempt = args;
578             parseResult.services              = this._services.createScope(); // Reminder: ServiceScope uses RAII.
579 
580             // Special support: For our default implementation of `ICommandLineInterface`, set its value.
581             auto proxy = cast(ICommandLineInterfaceImpl)parseResult.services.getServiceOrNull!ICommandLineInterface();
582             if(proxy !is null)
583                 proxy._func = &this.parseAndExecute;
584 
585             final switch(mode) with(Mode)
586             {
587                 case execute:        return this.onExecute(parseResult);
588                 case complete:       return this.onComplete(parseResult);
589                 case bashCompletion: return this.onBashCompletionScript();
590             }
591         }
592     }
593 
594     /+ COMMAND DISCOVERY AND REGISTRATION +/
595     private final
596     {
597         void addDefaultCommand()
598         {
599             static if(DefaultCommands.length > 0)
600                 _defaultCommand = getCommand!(DefaultCommands[0]);
601         }
602 
603         void addCommandsFromModule(alias Module)()
604         {
605             import std.traits : getSymbolsByUDA;
606 
607             static foreach(symbol; getSymbolsByUDA!(Module, Command))
608             {{
609                 static assert(is(symbol == struct) || is(symbol == class),
610                     "Only structs and classes can be marked with @Command. Issue Symbol = " ~ __traits(identifier, symbol)
611                 );
612 
613                 enum Info = getCommandInfoFor!(symbol, ArgBinderInstance);
614 
615                 auto info = getCommand!(symbol);
616                 info.pattern = Info.pattern;
617                 info.description = Info.description;
618 
619                 foreach(pattern; info.pattern.byEach)
620                     this._resolver.define(pattern, info);
621             }}
622         }
623 
624         CommandInfo getCommand(T)()
625         {
626             CommandInfo info;
627             info.helpText   = createHelpText!(T, ArgBinderInstance)(this._settings.appName.get);
628             info.doExecute  = createCommandExecuteFunc!(T, ArgBinderInstance)(this._settings);
629             info.doComplete = createCommandCompleteFunc!(T, ArgBinderInstance)();
630 
631             return info;
632         }
633     }
634 
635     /+ MODE EXECUTORS +/
636     private final
637     {
638         int onExecute(ref ParseResult result)
639         {
640             final switch(result.type) with(ParseResultType)
641             {
642                 case showHelpText:
643                     this.writeln(result.helpText);
644                     return 0;
645 
646                 case commandNotFound:
647                     this.writeln(result.helpText);
648                     return -1;
649 
650                 case commandFound: break;
651             }
652 
653             auto statusCode = result.command.doExecute(result.argParserAfterAttempt, result.services, result.command.helpText);
654             if(!statusCode.isSuccess)
655             {
656                 this.writeln(this.makeErrorf(statusCode.asFailure.error));
657                 return -1;
658             }
659 
660             return statusCode.asSuccess.value;
661         }
662 
663         int onComplete(ref ParseResult result)
664         {
665             // Parsing here shouldn't be affected by user-defined ArgBinders, so stuff being done here is done manually.
666             // This way we gain reliability.
667             //
668             // Since this is also an internal function, error checking is much more lax.
669             import std.array     : array;
670             import std.algorithm : map, filter, splitter, equal, startsWith;
671             import std.conv      : to;
672             import std.stdio     : writeln; // Planning on moving this into its own component soon, so we'll just leave this writeln here.
673 
674             // Expected args:
675             //  [0]    = COMP_CWORD
676             //  [1..$] = COMP_WORDS
677             result.argParserAfterAttempt.popFront(); // Skip __jcli:complete
678             auto cword = result.argParserAfterAttempt.front.value.to!uint;
679             result.argParserAfterAttempt.popFront();
680             auto  words = result.argParserAfterAttempt.map!(t => t.value).array;
681 
682             cword -= 1;
683             words = words[1..$]; // [0] is the exe name, which we don't care about.
684             auto before  = words[0..cword];
685             auto current = (cword < words.length)     ? words[cword]      : [];
686             auto after   = (cword + 1 < words.length) ? words[cword+1..$] : [];
687 
688             auto beforeParser = ArgPullParser(before);
689             auto commandInfo  = this._resolver.resolveAndAdvance(beforeParser);
690 
691             // Can't find command, so we're in "display command name" mode.
692             if(!commandInfo.success || commandInfo.value.type == CommandNodeType.partialWord)
693             {
694                 char[] output;
695                 output.reserve(1024); // Gonna be doing a good bit of concat.
696 
697                 // Special case: When we have no text to look for, just display the first word of every command path.
698                 if(before.length == 0 && current is null)
699                     commandInfo.value = this._resolver.root;
700 
701                 // Otherwise try to match using the existing text.
702 
703                 // Display the word of all children of the current command word.
704                 //
705                 // If the current argument word isn't null, then use that as a further filter.
706                 //
707                 // e.g.
708                 // Before  = ["name"]
709                 // Pattern = "name get"
710                 // Output  = "get"
711                 foreach(child; commandInfo.value.children)
712                 {
713                     if(current.length > 0 && !child.word.startsWith(current))
714                         continue;
715 
716                     output ~= child.word;
717                     output ~= " ";
718                 }
719 
720                 writeln(output);
721                 return 0;
722             }
723 
724             // Found command, so we're in "display possible args" mode.
725             char[] output;
726             output.reserve(1024);
727 
728             commandInfo.value.userData.doComplete(before, current, after, /*ref*/ output); // We need black magic, so this is generated in addCommand.
729             writeln(output);
730 
731             return 0;
732         }
733 
734         int onBashCompletionScript()
735         {
736             import std.stdio : writefln;
737             import std.file  : thisExePath;
738             import std.path  : baseName;
739             import jaster.cli.views.bash_complete : BASH_COMPLETION_TEMPLATE;
740 
741             const fullPath = thisExePath;
742             const exeName  = fullPath.baseName;
743 
744             writefln(BASH_COMPLETION_TEMPLATE,
745                 exeName,
746                 fullPath,
747                 exeName,
748                 exeName
749             );
750             return 0;
751         }
752     }
753 
754     /+ UNCATEGORISED HELPERS +/
755     private final
756     {
757         HelpTextBuilderTechnical createAvailableCommandsHelpText(ArgPullParser args, string sectionName = "Did you mean")
758         {
759             import std.array     : array;
760             import std.algorithm : filter, sort, map, splitter, uniq;
761 
762             auto command = this._resolver.root;
763             auto result  = this._resolver.resolveAndAdvance(args);
764             if(result.success)
765                 command = result.value;
766 
767             auto builder = new HelpTextBuilderTechnical();
768             builder.addSection(sectionName)
769                    .addContent(
770                        new HelpSectionArgInfoContent(
771                            command.finalWords
772                                   .uniq!((a, b) => a.userData.pattern == b.userData.pattern)
773                                   .map!(c => HelpSectionArgInfoContent.ArgInfo(
774                                        [c.userData.pattern.byEach.front],
775                                        c.userData.description,
776                                        ArgIsOptional.no
777                                   ))
778                                   .array
779                                   .sort!"a.names[0] < b.names[0]"
780                                   .array, // eww...
781                             AutoAddArgDashes.no
782                        )
783             );
784 
785             return builder;
786         }
787 
788         string makeErrorf(Args...)(string formatString, Args args)
789         {
790             import std.format : format;
791             return "%s: %s".format(this._settings.appName.get, formatString.format(args));
792         }
793 
794         void writeln(string str)
795         {
796             assert(!this._settings.sink.isNull, "The ctor should've set this.");
797 
798             auto sink = this._settings.sink.get();
799             assert(sink !is null, "The sink was set, but it's still null.");
800 
801             sink(str);
802             sink("\n");
803         }
804     }
805 }
806 
807 // HELPER FUNCS
808 
809 private bool containsHelpArgument(ArgPullParser args)
810 {
811     import std.algorithm : any;
812 
813     return args.any!(t => t.type == ArgTokenType.ShortHandArgument && t.value == "h"
814                        || t.type == ArgTokenType.LongHandArgument && t.value == "help");
815 }
816 
817 version(unittest)
818 {
819     import jaster.cli.result;
820     private alias InstansiationTest = CommandLineInterface!(jaster.cli.core);
821 
822     @CommandDefault("This is the default command.")
823     private struct DefaultCommandTest
824     {
825         @CommandNamedArg("var", "A variable")
826         int a;
827 
828         int onExecute()
829         {
830             return a % 2 == 0
831             ? a
832             : 0;
833         }
834     }
835 
836     @("Default command test")
837     unittest
838     {
839         auto cli = new CommandLineInterface!(jaster.cli.core);
840         assert(cli.parseAndExecute(["--var 1"], IgnoreFirstArg.no) == 0);
841         assert(cli.parseAndExecute(["--var 2"], IgnoreFirstArg.no) == 2);
842     }
843 
844     @Command("arg group test", "Test arg groups work")
845     private struct ArgGroupTestCommand
846     {
847         @CommandPositionalArg(0)
848         string a;
849 
850         @CommandNamedArg("b")
851         string b;
852 
853         @CommandArgGroup("group1", "This is group 1")
854         {
855             @CommandPositionalArg(1)
856             string c;
857 
858             @CommandNamedArg("d")
859             string d;
860         }
861 
862         void onExecute(){}
863     }
864     @("Test that @CommandArgGroup is handled properly.")
865     unittest
866     {
867         import std.algorithm : canFind;
868 
869         // Accessing a lot of private state here, but that's because we don't have a mechanism to extract the output properly.
870         auto cli = new CommandLineInterface!(jaster.cli.core);
871         auto helpText = cli._resolver.resolve("arg group test").value.userData.helpText;
872 
873         assert(helpText.toString().canFind(
874             "group1:\n"
875            ~"    This is group 1\n"
876            ~"\n"
877            ~"    VALUE"
878         ));
879     }
880 
881     @("Test that CommandLineInterface's sink works")
882     unittest
883     {
884         import std.algorithm : canFind;
885 
886         string log;
887 
888         CommandLineSettings settings;
889         settings.sink = (string str) { log ~= str; };
890 
891         auto cli = new CommandLineInterface!(jaster.cli.core)(settings);
892         cli.parseAndExecute(["--help"], IgnoreFirstArg.no);
893 
894         assert(log.length > 0);
895         assert(log.canFind("arg group test"), log); // The name of that unittest command has no real reason to change or to be removed, so I feel safe relying on it.
896     }
897 }