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 }