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 }