1 module argparse; 2 3 4 import std.typecons: Nullable; 5 import std.sumtype: SumType, match; 6 import std.traits; 7 8 private enum DEFAULT_COMMAND = ""; 9 10 struct Config 11 { 12 /** 13 The assignment character used in options with parameters. 14 Defaults to '='. 15 */ 16 char assignChar = '='; 17 18 /** 19 When set to char.init, parameters to array and associative array receivers are 20 treated as an individual argument. That is, only one argument is appended or 21 inserted per appearance of the option switch. If `arraySep` is set to 22 something else, then each parameter is first split by the separator, and the 23 individual pieces are treated as arguments to the same option. 24 25 Defaults to char.init 26 */ 27 char arraySep = char.init; 28 29 /** 30 The option character. 31 Defaults to '-'. 32 */ 33 char namedArgChar = '-'; 34 35 /** 36 The string that conventionally marks the end of all options. 37 Assigning an empty string to `endOfArgs` effectively disables it. 38 Defaults to "--". 39 */ 40 string endOfArgs = "--"; 41 42 /** 43 If set then argument names are case-sensitive. 44 Defaults to true. 45 */ 46 bool caseSensitive = true; 47 48 /** 49 Single-letter arguments can be bundled together, i.e. "-abc" is the same as "-a -b -c". 50 Disabled by default. 51 */ 52 bool bundling = false; 53 54 /** 55 Add a -h/--help option to the parser. 56 Defaults to true. 57 */ 58 bool addHelp = true; 59 60 /** 61 Delegate that processes error messages if they happen during argument parsing. 62 By default all errors are printed to stderr. 63 */ 64 private void delegate(string s) nothrow errorHandlerFunc; 65 66 @property auto errorHandler(void function(string s) nothrow func) 67 { 68 return errorHandlerFunc = (string msg) { func(msg); }; 69 } 70 71 @property auto errorHandler(void delegate(string s) nothrow func) 72 { 73 return errorHandlerFunc = func; 74 } 75 76 77 private void onError(A...)(A args) const nothrow 78 { 79 import std.conv: text; 80 import std.stdio: stderr, writeln; 81 82 try 83 { 84 if(errorHandlerFunc) 85 errorHandlerFunc(text!A(args)); 86 else 87 stderr.writeln("Error: ", args); 88 } 89 catch(Exception e) 90 { 91 throw new Error(e.msg); 92 } 93 } 94 } 95 96 unittest 97 { 98 Config.init.onError("--just testing error func--",1,2.3,false); 99 Config c; 100 c.errorHandler = (string s){}; 101 c.onError("--just testing error func--",1,2.3,false); 102 } 103 104 105 struct Param(VALUE_TYPE) 106 { 107 const Config config; 108 string name; 109 110 static if(!is(VALUE_TYPE == void)) 111 VALUE_TYPE value; 112 } 113 114 alias RawParam = Param!(string[]); 115 116 117 private template defaultValuesCount(T) 118 if(!is(T == void)) 119 { 120 import std.traits; 121 122 static if(isBoolean!T) 123 { 124 enum min = 0; 125 enum max = 0; 126 } 127 else static if(isSomeString!T || isScalarType!T) 128 { 129 enum min = 1; 130 enum max = 1; 131 } 132 else static if(isStaticArray!T) 133 { 134 enum min = 1; 135 enum max = T.length; 136 } 137 else static if(isArray!T || isAssociativeArray!T) 138 { 139 enum min = 1; 140 enum max = ulong.max; 141 } 142 else static if(is(T == function)) 143 { 144 // ... function() 145 static if(__traits(compiles, { T(); })) 146 { 147 enum min = 0; 148 enum max = 0; 149 } 150 // ... function(string value) 151 else static if(__traits(compiles, { T(string.init); })) 152 { 153 enum min = 1; 154 enum max = 1; 155 } 156 // ... function(string[] value) 157 else static if(__traits(compiles, { T([string.init]); })) 158 { 159 enum min = 0; 160 enum max = ulong.max; 161 } 162 // ... function(RawParam param) 163 else static if(__traits(compiles, { T(RawParam.init); })) 164 { 165 enum min = 1; 166 enum max = ulong.max; 167 } 168 else 169 static assert(false, "Unsupported callback: " ~ T.stringof); 170 } 171 else 172 static assert(false, "Type is not supported: " ~ T.stringof); 173 } 174 175 176 private template EnumMembersAsStrings(E) 177 { 178 enum EnumMembersAsStrings = { 179 import std.traits: EnumMembers; 180 alias members = EnumMembers!E; 181 182 typeof(__traits(identifier, members[0]))[] res; 183 static foreach (i, _; members) 184 res ~= __traits(identifier, members[i]); 185 186 return res; 187 }(); 188 } 189 190 unittest 191 { 192 enum E { abc, def, ghi } 193 assert(EnumMembersAsStrings!E == ["abc", "def", "ghi"]); 194 } 195 196 private auto setDefaults(ArgumentInfo uda, TYPE, alias symbol)() 197 { 198 ArgumentInfo info = uda; 199 200 static if(!isBoolean!TYPE) 201 info.allowBooleanNegation = false; 202 203 static if(is(TYPE == enum)) 204 info.setAllowedValues!(EnumMembersAsStrings!TYPE); 205 206 static if(uda.names.length == 0) 207 info.names = [ symbol ]; 208 209 static if(uda.minValuesCount.isNull) info.minValuesCount = defaultValuesCount!TYPE.min; 210 static if(uda.maxValuesCount.isNull) info.maxValuesCount = defaultValuesCount!TYPE.max; 211 212 static if(uda.placeholder.length == 0) 213 if(info.placeholder.length == 0) 214 { 215 import std.uni : toUpper; 216 info.placeholder = info.positional ? symbol : symbol.toUpper; 217 } 218 219 return info; 220 } 221 222 unittest 223 { 224 auto createInfo(string placeholder = "")() 225 { 226 ArgumentInfo info; 227 info.allowBooleanNegation = true; 228 info.position = 0; 229 info.placeholder = placeholder; 230 return info; 231 } 232 assert(createInfo().allowBooleanNegation); // make codecov happy 233 234 auto res = setDefaults!(createInfo(), int, "default-name"); 235 assert(!res.allowBooleanNegation); 236 assert(res.names == [ "default-name" ]); 237 assert(res.minValuesCount == defaultValuesCount!int.min); 238 assert(res.maxValuesCount == defaultValuesCount!int.max); 239 assert(res.placeholder == "default-name"); 240 241 res = setDefaults!(createInfo!"myvalue", int, "default-name"); 242 assert(res.placeholder == "myvalue"); 243 } 244 245 unittest 246 { 247 auto createInfo(string placeholder = "")() 248 { 249 ArgumentInfo info; 250 info.allowBooleanNegation = true; 251 info.placeholder = placeholder; 252 return info; 253 } 254 assert(createInfo().allowBooleanNegation); // make codecov happy 255 256 auto res = setDefaults!(createInfo(), bool, "default_name"); 257 assert(res.allowBooleanNegation); 258 assert(res.names == ["default_name"]); 259 assert(res.minValuesCount == defaultValuesCount!bool.min); 260 assert(res.maxValuesCount == defaultValuesCount!bool.max); 261 assert(res.placeholder == "DEFAULT_NAME"); 262 263 res = setDefaults!(createInfo!"myvalue", bool, "default_name"); 264 assert(res.placeholder == "myvalue"); 265 } 266 267 unittest 268 { 269 enum E { a=1, b=1, c } 270 static assert(EnumMembersAsStrings!E == ["a","b","c"]); 271 272 auto createInfo(string placeholder = "")() 273 { 274 ArgumentInfo info; 275 info.placeholder = placeholder; 276 return info; 277 } 278 assert(createInfo().allowBooleanNegation); // make codecov happy 279 280 auto res = setDefaults!(createInfo(), E, "default-name"); 281 assert(res.placeholder == "{a,b,c}"); 282 283 res = setDefaults!(createInfo!"myvalue", E, "default-name"); 284 assert(res.placeholder == "myvalue"); 285 } 286 287 288 private auto checkDuplicates(alias sortedRange, string errorMsg)() { 289 static if(sortedRange.length >= 2) 290 { 291 enum value = { 292 import std.conv : to; 293 294 foreach(i; 1..sortedRange.length-1) 295 if(sortedRange[i-1] == sortedRange[i]) 296 return sortedRange[i].to!string; 297 298 return ""; 299 }(); 300 static assert(value.length == 0, errorMsg ~ value); 301 } 302 303 return true; 304 } 305 306 private bool checkArgumentNames(T)() 307 { 308 enum names = () { 309 import std.algorithm : sort; 310 311 string[] names; 312 static foreach (sym; getSymbolsByUDA!(T, ArgumentUDA)) 313 {{ 314 enum argUDA = getUDAs!(__traits(getMember, T, __traits(identifier, sym)), ArgumentUDA)[0]; 315 316 static assert(!argUDA.info.positional || argUDA.info.names.length <= 1, 317 "Positional argument should have exactly one name: "~T.stringof~"."~sym.stringof); 318 319 static foreach (name; argUDA.info.names) 320 { 321 static assert(name.length > 0, "Argument name can't be empty: "~T.stringof~"."~sym.stringof); 322 323 names ~= name; 324 } 325 }} 326 327 return names.sort; 328 }(); 329 330 return checkDuplicates!(names, "Argument name appears more than once: "); 331 } 332 333 private bool checkPositionalIndexes(T)() 334 { 335 import std.conv : to; 336 import std.range : lockstep, iota; 337 338 339 enum positions = () { 340 import std.algorithm : sort; 341 342 uint[] positions; 343 static foreach (sym; getSymbolsByUDA!(T, ArgumentUDA)) 344 {{ 345 enum argUDA = getUDAs!(__traits(getMember, T, __traits(identifier, sym)), ArgumentUDA)[0]; 346 347 static if (argUDA.info.positional) 348 positions ~= argUDA.info.position.get; 349 }} 350 351 return positions.sort; 352 }(); 353 354 if(!checkDuplicates!(positions, "Positional arguments have duplicated position: ")) 355 return false; 356 357 static foreach (i, pos; lockstep(iota(0, positions.length), positions)) 358 static assert(i == pos, "Positional arguments have missed position: " ~ i.to!string); 359 360 return true; 361 } 362 363 private struct Group 364 { 365 string name; 366 string description; 367 368 private size_t[] arguments; 369 370 auto ref Description(string text) 371 { 372 description = text; 373 return this; 374 } 375 376 } 377 378 auto ArgumentGroup(string name) 379 { 380 return Group(name); 381 } 382 383 unittest 384 { 385 auto g = ArgumentGroup("name").Description("description"); 386 assert(g.name == "name"); 387 assert(g.description == "description"); 388 } 389 390 391 private struct RestrictionGroup 392 { 393 string location; 394 395 enum Type { together, exclusive } 396 Type type; 397 398 private size_t[] arguments; 399 400 private bool required; 401 402 auto ref Required() 403 { 404 required = true; 405 return this; 406 } 407 } 408 409 auto RequiredTogether(string file=__FILE__, uint line = __LINE__)() 410 { 411 import std.conv: to; 412 return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.together); 413 } 414 415 auto MutuallyExclusive(string file=__FILE__, uint line = __LINE__)() 416 { 417 import std.conv: to; 418 return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.exclusive); 419 } 420 421 unittest 422 { 423 auto t = RequiredTogether(); 424 assert(t.location.length > 0); 425 assert(t.type == RestrictionGroup.Type.together); 426 427 auto e = MutuallyExclusive(); 428 assert(e.location.length > 0); 429 assert(e.type == RestrictionGroup.Type.exclusive); 430 } 431 432 433 private alias ParseFunction(RECEIVER) = Result delegate(in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs); 434 private alias ParseSubCommandFunction(RECEIVER) = Result delegate(ref Parser parser, const ref Parser.Argument arg, bool isDefaultCmd, ref RECEIVER receiver); 435 private alias InitSubCommandFunction(RECEIVER) = Result delegate(ref RECEIVER receiver); 436 private alias Restriction = bool delegate(in Config config, in bool[size_t] cliArgs); 437 438 // Have to do this magic because closures are not supported in CFTE 439 // DMD v2.098.0 prints "Error: closures are not yet supported in CTFE" 440 private auto partiallyApply(alias fun,C...)(C context) 441 { 442 import std.traits: ParameterTypeTuple; 443 import core.lifetime: move, forward; 444 445 return &new class(move(context)) 446 { 447 C context; 448 449 this(C ctx) 450 { 451 foreach(i, ref c; context) 452 c = move(ctx[i]); 453 } 454 455 auto opCall(ParameterTypeTuple!fun[context.length..$] args) const 456 { 457 return fun(context, forward!args); 458 } 459 }.opCall; 460 } 461 462 private struct Restrictions 463 { 464 static Restriction RequiredArg(ArgumentInfo info)(size_t index) 465 { 466 return partiallyApply!((size_t index, in Config config, in bool[size_t] cliArgs) 467 { 468 if(index in cliArgs) 469 return true; 470 471 config.onError("The following argument is required: ", info.names[0].getArgumentName(config)); 472 return false; 473 })(index); 474 } 475 476 static bool RequiredTogether(in Config config, 477 in bool[size_t] cliArgs, 478 in size_t[] restrictionArgs, 479 in ArgumentInfo[] allArgs) 480 { 481 size_t foundIndex = size_t.max; 482 size_t missedIndex = size_t.max; 483 484 foreach(index; restrictionArgs) 485 { 486 if(index in cliArgs) 487 { 488 if(foundIndex == size_t.max) 489 foundIndex = index; 490 } 491 else if(missedIndex == size_t.max) 492 missedIndex = index; 493 494 if(foundIndex != size_t.max && missedIndex != size_t.max) 495 { 496 config.onError("Missed argument '", allArgs[missedIndex].names[0].getArgumentName(config), 497 "' - it is required by argument '", allArgs[foundIndex].names[0].getArgumentName(config),"'"); 498 return false; 499 } 500 } 501 502 return true; 503 } 504 505 static bool MutuallyExclusive(in Config config, 506 in bool[size_t] cliArgs, 507 in size_t[] restrictionArgs, 508 in ArgumentInfo[] allArgs) 509 { 510 size_t foundIndex = size_t.max; 511 512 foreach(index; restrictionArgs) 513 if(index in cliArgs) 514 { 515 if(foundIndex == size_t.max) 516 foundIndex = index; 517 else 518 { 519 config.onError("Argument '", allArgs[foundIndex].names[0].getArgumentName(config), 520 "' is not allowed with argument '", allArgs[index].names[0].getArgumentName(config),"'"); 521 return false; 522 } 523 524 } 525 526 return true; 527 } 528 529 static bool RequiredAnyOf(in Config config, 530 in bool[size_t] cliArgs, 531 in size_t[] restrictionArgs, 532 in ArgumentInfo[] allArgs) 533 { 534 import std.algorithm: map; 535 import std.array: join; 536 537 foreach(index; restrictionArgs) 538 if(index in cliArgs) 539 return true; 540 541 config.onError("One of the following arguments is required: '", restrictionArgs.map!(_ => allArgs[_].names[0].getArgumentName(config)).join("', '"), "'"); 542 543 return false; 544 } 545 } 546 547 private struct Arguments 548 { 549 550 immutable string function(string str) convertCase; 551 552 private ArgumentInfo[] arguments; 553 554 // named arguments 555 private size_t[string] argsNamed; 556 557 // positional arguments 558 private size_t[] argsPositional; 559 560 private const Arguments* parentArguments; 561 562 Group[] groups; 563 enum requiredGroupIndex = 0; 564 enum optionalGroupIndex = 1; 565 566 size_t[string] groupsByName; 567 568 Restriction[] restrictions; 569 RestrictionGroup[] restrictionGroups; 570 571 @property ref Group requiredGroup() { return groups[requiredGroupIndex]; } 572 @property ref const(Group) requiredGroup() const { return groups[requiredGroupIndex]; } 573 @property ref Group optionalGroup() { return groups[optionalGroupIndex]; } 574 @property ref const(Group) optionalGroup() const { return groups[optionalGroupIndex]; } 575 576 @property auto positionalArguments() const { return argsPositional; } 577 578 579 this(bool caseSensitive, const Arguments* parentArguments = null) 580 { 581 if(caseSensitive) 582 convertCase = s => s; 583 else 584 convertCase = (string str) 585 { 586 import std.uni : toUpper; 587 return str.toUpper; 588 }; 589 590 this.parentArguments = parentArguments; 591 592 groups = [ Group("Required arguments"), Group("Optional arguments") ]; 593 } 594 595 private void addArgument(ArgumentInfo info, RestrictionGroup[] restrictions, Group group)() 596 { 597 auto index = (group.name in groupsByName); 598 if(index !is null) 599 addArgument!(info, restrictions)(groups[*index]); 600 else 601 { 602 groupsByName[group.name] = groups.length; 603 groups ~= group; 604 addArgument!(info, restrictions)(groups[$-1]); 605 } 606 } 607 608 private void addArgument(ArgumentInfo info, RestrictionGroup[] restrictions = [])() 609 { 610 static if(info.required) 611 addArgument!(info, restrictions)(requiredGroup); 612 else 613 addArgument!(info, restrictions)(optionalGroup); 614 } 615 616 private void addArgument(ArgumentInfo info, RestrictionGroup[] argRestrictions = [])( ref Group group) 617 { 618 static assert(info.names.length > 0); 619 620 immutable index = arguments.length; 621 622 static if(info.positional) 623 { 624 if(argsPositional.length <= info.position.get) 625 argsPositional.length = info.position.get + 1; 626 627 argsPositional[info.position.get] = index; 628 } 629 else 630 static foreach(name; info.names) 631 { 632 assert(!(name in argsNamed), "Duplicated argument name: "~name); 633 argsNamed[convertCase(name)] = index; 634 } 635 636 arguments ~= info; 637 group.arguments ~= index; 638 639 static if(info.required) 640 restrictions ~= Restrictions.RequiredArg!info(index); 641 642 static foreach(restriction; argRestrictions) 643 addRestriction!(info, restriction)(index); 644 } 645 646 private void addRestriction(ArgumentInfo info, RestrictionGroup restriction)(size_t argIndex) 647 { 648 auto groupIndex = (restriction.location in groupsByName); 649 auto index = groupIndex !is null 650 ? *groupIndex 651 : { 652 auto index = groupsByName[restriction.location] = restrictionGroups.length; 653 restrictionGroups ~= restriction; 654 return index; 655 }(); 656 657 restrictionGroups[index].arguments ~= argIndex; 658 } 659 660 661 private bool checkRestrictions(in bool[size_t] cliArgs, in Config config) const 662 { 663 foreach(restriction; restrictions) 664 if(!restriction(config, cliArgs)) 665 return false; 666 667 foreach(restriction; restrictionGroups) 668 { 669 if(restriction.required && !Restrictions.RequiredAnyOf(config, cliArgs, restriction.arguments, arguments)) 670 return false; 671 672 final switch(restriction.type) 673 { 674 case RestrictionGroup.Type.together: 675 if(!Restrictions.RequiredTogether(config, cliArgs, restriction.arguments, arguments)) 676 return false; 677 break; 678 case RestrictionGroup.Type.exclusive: 679 if(!Restrictions.MutuallyExclusive(config, cliArgs, restriction.arguments, arguments)) 680 return false; 681 break; 682 } 683 } 684 685 return true; 686 } 687 688 689 private auto findArgumentImpl(const size_t* pIndex) const 690 { 691 struct Result 692 { 693 size_t index = size_t.max; 694 const(ArgumentInfo)* arg; 695 } 696 697 return pIndex ? Result(*pIndex, &arguments[*pIndex]) : Result.init; 698 } 699 700 auto findPositionalArgument(size_t position) const 701 { 702 return findArgumentImpl(position < argsPositional.length ? &argsPositional[position] : null); 703 } 704 705 auto findNamedArgument(string name) const 706 { 707 return findArgumentImpl(convertCase(name) in argsNamed); 708 } 709 } 710 711 private alias ParsingArgument(alias symbol, alias uda, ArgumentInfo info, RECEIVER, bool completionMode) = 712 delegate(in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) 713 { 714 static if(completionMode) 715 { 716 if(rawValue is null) 717 consumeValuesFromCLI(rawArgs, info, config); 718 719 return Result.Success; 720 } 721 else 722 { 723 try 724 { 725 auto rawValues = rawValue !is null ? [ rawValue ] : consumeValuesFromCLI(rawArgs, info, config); 726 727 auto res = info.checkValuesCount(argName, rawValues.length); 728 if(!res) 729 { 730 config.onError(res.errorMsg); 731 return res; 732 } 733 734 auto param = RawParam(config, argName, rawValues); 735 736 auto target = &__traits(getMember, receiver, symbol); 737 738 static if(is(typeof(target) == function) || is(typeof(target) == delegate)) 739 return uda.parsingFunc.parse(target, param) ? Result.Success : Result.Failure; 740 else 741 return uda.parsingFunc.parse(*target, param) ? Result.Success : Result.Failure; 742 } 743 catch(Exception e) 744 { 745 config.onError(argName, ": ", e.msg); 746 return Result.Failure; 747 } 748 } 749 }; 750 751 private auto ParsingSubCommandArgument(COMMAND_TYPE, CommandInfo info, RECEIVER, alias symbol, bool completionMode)(const CommandArguments!RECEIVER* parentArguments) 752 { 753 return delegate(ref Parser parser, const ref Parser.Argument arg, bool isDefaultCmd, ref RECEIVER receiver) 754 { 755 auto target = &__traits(getMember, receiver, symbol); 756 757 alias parse = (ref COMMAND_TYPE cmdTarget) 758 { 759 static if(!is(COMMAND_TYPE == Default!TYPE, TYPE)) 760 alias TYPE = COMMAND_TYPE; 761 762 auto command = CommandArguments!TYPE(parser.config, info, parentArguments); 763 764 return parser.parse!completionMode(command, isDefaultCmd, cmdTarget, arg); 765 }; 766 767 768 static if(typeof(*target).Types.length == 1) 769 return (*target).match!parse; 770 else 771 { 772 return (*target).match!(parse, 773 (_) 774 { 775 assert(false, "This should never happen"); 776 return Result.Failure; 777 } 778 ); 779 } 780 }; 781 } 782 783 private alias ParsingSubCommandInit(COMMAND_TYPE, RECEIVER, alias symbol) = 784 delegate(ref RECEIVER receiver) 785 { 786 auto target = &__traits(getMember, receiver, symbol); 787 788 static if(typeof(*target).Types.length > 1) 789 if((*target).match!((COMMAND_TYPE t) => false, _ => true)) 790 *target = COMMAND_TYPE.init; 791 792 return Result.Success; 793 }; 794 795 struct SubCommands {} 796 797 // Default subcommand 798 struct Default(COMMAND) 799 { 800 COMMAND command; 801 alias command this; 802 } 803 804 unittest 805 { 806 struct T 807 { 808 @NamedArgument 809 int a; 810 @(NamedArgument.Optional()) 811 int b; 812 @(NamedArgument.Required()) 813 int c; 814 @NamedArgument 815 int d; 816 @(NamedArgument.Required()) 817 int e; 818 @NamedArgument 819 int f; 820 } 821 822 enum config = { 823 Config config; 824 config.addHelp = false; 825 return config; 826 }(); 827 828 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 829 830 auto a = CommandArguments!T(config); 831 assert(a.arguments.requiredGroup.arguments == [2,4]); 832 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 833 assert(a.arguments.argsPositional == []); 834 } 835 836 unittest 837 { 838 struct T 839 { 840 int a,b,c,d,e,f; 841 } 842 843 enum config = { 844 Config config; 845 config.addHelp = false; 846 return config; 847 }(); 848 849 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 850 851 auto a = CommandArguments!T(config); 852 assert(a.arguments.requiredGroup.arguments == []); 853 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 854 assert(a.arguments.argsPositional == []); 855 } 856 857 unittest 858 { 859 struct T1 860 { 861 @(NamedArgument("1")) 862 @(NamedArgument("2")) 863 int a; 864 } 865 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 866 867 struct T2 868 { 869 @(NamedArgument("1")) 870 int a; 871 @(NamedArgument("1")) 872 int b; 873 } 874 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 875 876 struct T3 877 { 878 @(PositionalArgument(0)) int a; 879 @(PositionalArgument(0)) int b; 880 } 881 static assert(!__traits(compiles, { CommandArguments!T3(Config.init); })); 882 883 struct T4 884 { 885 @(PositionalArgument(0)) int a; 886 @(PositionalArgument(2)) int b; 887 } 888 static assert(!__traits(compiles, { CommandArguments!T4(Config.init); })); 889 } 890 891 private void checkArgumentName(T)(char namedArgChar) 892 { 893 import std.exception: enforce; 894 895 static foreach(sym; getSymbolsByUDA!(T, ArgumentUDA)) 896 static foreach(name; getUDAs!(__traits(getMember, T, __traits(identifier, sym)), ArgumentUDA)[0].info.names) 897 enforce(name[0] != namedArgChar, "Name of argument should not begin with '"~namedArgChar~"': "~name); 898 } 899 900 private auto consumeValuesFromCLI(ref string[] args, in ArgumentInfo argumentInfo, in Config config) 901 { 902 import std.range: empty, front, popFront; 903 904 immutable minValuesCount = argumentInfo.minValuesCount.get; 905 immutable maxValuesCount = argumentInfo.maxValuesCount.get; 906 907 string[] values; 908 909 if(minValuesCount > 0) 910 { 911 if(minValuesCount < args.length) 912 { 913 values = args[0..minValuesCount]; 914 args = args[minValuesCount..$]; 915 } 916 else 917 { 918 values = args; 919 args = []; 920 } 921 } 922 923 while(!args.empty && 924 values.length < maxValuesCount && 925 (args.front.length == 0 || args.front[0] != config.namedArgChar)) 926 { 927 values ~= args.front; 928 args.popFront(); 929 } 930 931 return values; 932 } 933 934 935 private enum helpArgument = { 936 ArgumentInfo arg; 937 arg.names = ["h","help"]; 938 arg.description = "Show this help message and exit"; 939 arg.minValuesCount = 0; 940 arg.maxValuesCount = 0; 941 arg.allowBooleanNegation = false; 942 arg.ignoreInDefaultCommand = true; 943 return arg; 944 }(); 945 946 private bool isHelpArgument(string name) 947 { 948 static foreach(n; helpArgument.names) 949 if(n == name) 950 return true; 951 952 return false; 953 } 954 955 unittest 956 { 957 assert(isHelpArgument("h")); 958 assert(isHelpArgument("help")); 959 assert(!isHelpArgument("a")); 960 assert(!isHelpArgument("help1")); 961 } 962 963 struct Result 964 { 965 int resultCode; 966 967 private enum Status { failure, success, unknownArgument }; 968 private Status status; 969 970 private string errorMsg; 971 972 private const(string)[] suggestions; 973 974 bool opCast(type)() const if (is(type == bool)) 975 { 976 return status == Status.success; 977 } 978 979 private static enum Failure = Result(1, Status.failure); 980 private static enum Success = Result(0, Status.success); 981 private static enum UnknownArgument = Result(0, Status.unknownArgument); 982 983 private static auto Error(A...)(A args) nothrow 984 { 985 import std.conv: text; 986 import std.stdio: stderr, writeln; 987 988 return Result(1, Status.failure, text!A(args)); 989 } 990 } 991 992 private struct Parser 993 { 994 struct Unknown {} 995 struct EndOfArgs {} 996 struct Positional {} 997 struct NamedShort { 998 string name; 999 string nameWithDash; 1000 string value = null; // null when there is no value 1001 } 1002 struct NamedLong { 1003 string name; 1004 string nameWithDash; 1005 string value = null; // null when there is no value 1006 } 1007 1008 alias Argument = SumType!(Unknown, EndOfArgs, Positional, NamedShort, NamedLong); 1009 1010 immutable Config config; 1011 1012 string[] args; 1013 string[] unrecognizedArgs; 1014 1015 bool[size_t] idxParsedArgs; 1016 size_t idxNextPositional = 0; 1017 1018 struct CmdParser 1019 { 1020 Result delegate(const ref Argument) parse; 1021 Result delegate(const ref Argument) complete; 1022 const(string)[] completeSuggestion; 1023 bool isDefault; 1024 } 1025 CmdParser[] cmdStack; 1026 1027 Argument splitArgumentNameValue(string arg) 1028 { 1029 import std.string : indexOf; 1030 1031 if(arg.length == 0) 1032 return Argument.init; 1033 1034 if(arg == config.endOfArgs) 1035 return Argument(EndOfArgs.init); 1036 1037 if(arg[0] != config.namedArgChar) 1038 return Argument(Positional.init); 1039 1040 if(arg.length == 1 || arg.length == 2 && arg[1] == config.namedArgChar) 1041 return Argument.init; 1042 1043 auto idxAssignChar = config.assignChar == char.init ? -1 : arg.indexOf(config.assignChar); 1044 1045 immutable string nameWithDash = idxAssignChar < 0 ? arg : arg[0 .. idxAssignChar]; 1046 immutable string value = idxAssignChar < 0 ? null : arg[idxAssignChar + 1 .. $]; 1047 1048 return arg[1] == config.namedArgChar 1049 ? Argument(NamedLong (nameWithDash[2..$], nameWithDash, value)) 1050 : Argument(NamedShort(nameWithDash[1..$], nameWithDash, value)); 1051 } 1052 1053 auto parseArgument(T, PARSE)(PARSE parse, ref T receiver, string value, string nameWithDash, size_t argIndex) 1054 { 1055 auto res = parse(config, nameWithDash, receiver, value, args); 1056 if(!res) 1057 return res; 1058 1059 idxParsedArgs[argIndex] = true; 1060 1061 return Result.Success; 1062 } 1063 1064 auto parseSubCommand(T)(const ref CommandArguments!T cmd, ref T receiver) 1065 { 1066 import std.range: front, popFront; 1067 1068 auto found = cmd.findSubCommand(args.front); 1069 if(found.parse is null) 1070 return Result.UnknownArgument; 1071 1072 if(found.level < cmdStack.length) 1073 cmdStack.length = found.level; 1074 1075 cmdStack ~= CmdParser((const ref arg) => found.parse(this, arg, false, receiver), (const ref arg) => found.complete(this, arg, false, receiver)); 1076 1077 found.initialize(receiver); 1078 args.popFront(); 1079 1080 return Result.Success; 1081 } 1082 1083 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Unknown) 1084 { 1085 static if(completionMode) 1086 { 1087 import std.range: front, popFront; 1088 import std.algorithm: filter; 1089 import std.string:startsWith; 1090 import std.array:array; 1091 if(args.length == 1) 1092 { 1093 // last arg means we need to provide args and subcommands 1094 auto A = args[0] == "" ? cmd.completeSuggestion : cmd.completeSuggestion.filter!(_ => _.startsWith(args[0])).array; 1095 return Result(0, Result.Status.success, "", A); 1096 } 1097 } 1098 1099 return Result.UnknownArgument; 1100 } 1101 1102 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, EndOfArgs) 1103 { 1104 static if(!completionMode) 1105 { 1106 static if (is(typeof(cmd.setTrailingArgs))) 1107 cmd.setTrailingArgs(receiver, args[1..$]); 1108 else 1109 unrecognizedArgs ~= args[1..$]; 1110 } 1111 1112 args = []; 1113 1114 return Result.Success; 1115 } 1116 1117 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Positional) 1118 { 1119 auto foundArg = cmd.findPositionalArgument(idxNextPositional); 1120 if(foundArg.arg is null) 1121 return parseSubCommand(cmd, receiver); 1122 1123 auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, null, foundArg.arg.names[0], foundArg.index); 1124 if(!res) 1125 return res; 1126 1127 idxNextPositional++; 1128 1129 return Result.Success; 1130 } 1131 1132 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedLong arg) 1133 { 1134 import std.algorithm : startsWith; 1135 import std.range: popFront; 1136 1137 auto foundArg = cmd.findNamedArgument(arg.name); 1138 1139 if(foundArg.arg is null && arg.name.startsWith("no-")) 1140 { 1141 foundArg = cmd.findNamedArgument(arg.name[3..$]); 1142 if(foundArg.arg is null || !foundArg.arg.allowBooleanNegation) 1143 return Result.UnknownArgument; 1144 1145 arg.value = "false"; 1146 } 1147 1148 if(foundArg.arg is null) 1149 return Result.UnknownArgument; 1150 1151 if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand) 1152 return Result.UnknownArgument; 1153 1154 args.popFront(); 1155 return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); 1156 } 1157 1158 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedShort arg) 1159 { 1160 import std.range: popFront; 1161 1162 auto foundArg = cmd.findNamedArgument(arg.name); 1163 if(foundArg.arg !is null) 1164 { 1165 if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand) 1166 return Result.UnknownArgument; 1167 1168 args.popFront(); 1169 return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); 1170 } 1171 1172 // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments 1173 do 1174 { 1175 auto name = [arg.name[0]]; 1176 foundArg = cmd.findNamedArgument(name); 1177 if(foundArg.arg is null) 1178 return Result.UnknownArgument; 1179 1180 // In case of bundling there can be no or one argument value 1181 if(config.bundling && foundArg.arg.minValuesCount.get > 1) 1182 return Result.UnknownArgument; 1183 1184 // In case of NO bundling there MUST be one argument value 1185 if(!config.bundling && foundArg.arg.minValuesCount.get != 1) 1186 return Result.UnknownArgument; 1187 1188 string value; 1189 if(foundArg.arg.minValuesCount == 0) 1190 arg.name = arg.name[1..$]; 1191 else 1192 { 1193 // Bundling case: try to parse "-ABvalue" where "A","B" are different single-letter arguments and "value" is a value for "B" 1194 // No bundling case: try to parse "-Avalue" where "A" is a single-letter argument and "value" is its value 1195 value = arg.name[1..$]; 1196 arg.name = ""; 1197 } 1198 1199 auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, value, "-"~name, foundArg.index); 1200 if(!res) 1201 return res; 1202 } 1203 while(arg.name.length > 0); 1204 1205 args.popFront(); 1206 return Result.Success; 1207 } 1208 1209 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Argument arg) 1210 { 1211 return arg.match!(_ => parse!completionMode(cmd, isDefaultCmd, receiver, _)); 1212 } 1213 1214 auto parse(bool completionMode)(Argument arg) 1215 { 1216 import std.range: front, popFront; 1217 1218 auto result = Result.Success; 1219 1220 const argsCount = args.length; 1221 1222 foreach_reverse(cmdParser; cmdStack) 1223 { 1224 static if(completionMode) 1225 { 1226 auto res = cmdParser.complete(arg); 1227 if(res) 1228 result.suggestions ~= res.suggestions; 1229 } 1230 else 1231 { 1232 auto res = cmdParser.parse(arg); 1233 1234 if(res.status != Result.Status.unknownArgument) 1235 return res; 1236 } 1237 } 1238 1239 if(args.length > 0 && argsCount == args.length) 1240 { 1241 unrecognizedArgs ~= args.front; 1242 args.popFront(); 1243 } 1244 1245 return result; 1246 } 1247 1248 auto parseAll(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver) 1249 { 1250 import std.range: empty, front; 1251 1252 cmdStack ~= CmdParser( 1253 (const ref arg) 1254 { 1255 return parse!completionMode(cmd, false, receiver, arg); 1256 }, 1257 (const ref arg) 1258 { 1259 return parse!completionMode(cmd, false, receiver, arg); 1260 }, 1261 cmd.completeSuggestion); 1262 1263 auto found = cmd.findSubCommand(DEFAULT_COMMAND); 1264 if(found.parse !is null) 1265 { 1266 auto p = CmdParser((const ref arg) => found.parse(this, arg, true, receiver)); 1267 p.isDefault = true; 1268 cmdStack ~= p; 1269 found.initialize(receiver); 1270 } 1271 1272 while(!args.empty) 1273 { 1274 static if(completionMode) 1275 auto res = parse!completionMode(args.length > 1 ? splitArgumentNameValue(args.front) : Argument.init); 1276 else 1277 auto res = parse!completionMode(splitArgumentNameValue(args.front)); 1278 if(!res) 1279 return res; 1280 1281 static if(completionMode) 1282 if(args.empty) 1283 return res; 1284 } 1285 1286 if(!cmd.checkRestrictions(idxParsedArgs, config)) 1287 return Result.Failure; 1288 1289 return Result.Success; 1290 } 1291 } 1292 1293 unittest 1294 { 1295 assert(Parser.init.splitArgumentNameValue("") == Parser.Argument(Parser.Unknown.init)); 1296 assert(Parser.init.splitArgumentNameValue("-") == Parser.Argument(Parser.Unknown.init)); 1297 assert(Parser.init.splitArgumentNameValue("--") == Parser.Argument(Parser.EndOfArgs.init)); 1298 assert(Parser.init.splitArgumentNameValue("abc=4") == Parser.Argument(Parser.Positional.init)); 1299 assert(Parser.init.splitArgumentNameValue("-abc") == Parser.Argument(Parser.NamedShort("abc", "-abc", null))); 1300 assert(Parser.init.splitArgumentNameValue("--abc") == Parser.Argument(Parser.NamedLong("abc", "--abc", null))); 1301 assert(Parser.init.splitArgumentNameValue("-abc=fd") == Parser.Argument(Parser.NamedShort("abc", "-abc", "fd"))); 1302 assert(Parser.init.splitArgumentNameValue("--abc=fd") == Parser.Argument(Parser.NamedLong("abc", "--abc", "fd"))); 1303 assert(Parser.init.splitArgumentNameValue("-abc=") == Parser.Argument(Parser.NamedShort("abc", "-abc", ""))); 1304 assert(Parser.init.splitArgumentNameValue("--abc=") == Parser.Argument(Parser.NamedLong("abc", "--abc", ""))); 1305 assert(Parser.init.splitArgumentNameValue("-=abc") == Parser.Argument(Parser.NamedShort("", "-", "abc"))); 1306 assert(Parser.init.splitArgumentNameValue("--=abc") == Parser.Argument(Parser.NamedLong("", "--", "abc"))); 1307 } 1308 1309 1310 private Result parseCLIKnownArgs(T)(ref T receiver, 1311 string[] args, 1312 out string[] unrecognizedArgs, 1313 const ref CommandArguments!T cmd, 1314 in Config config) 1315 { 1316 auto parser = Parser(config, args); 1317 1318 auto res = parser.parseAll!false(cmd, receiver); 1319 if(!res) 1320 return res; 1321 1322 unrecognizedArgs = parser.unrecognizedArgs; 1323 1324 return Result.Success; 1325 } 1326 1327 deprecated("Use CLI!(config, COMMAND).parseKnownArgs") 1328 Result parseCLIKnownArgs(T)(ref T receiver, 1329 string[] args, 1330 out string[] unrecognizedArgs, 1331 in Config config) 1332 { 1333 auto command = CommandArguments!T(config); 1334 return parseCLIKnownArgs(receiver, args, unrecognizedArgs, command, config); 1335 } 1336 1337 deprecated("Use CLI!COMMAND.parseKnownArgs") 1338 Result parseCLIKnownArgs(T)(ref T receiver, 1339 string[] args, 1340 out string[] unrecognizedArgs) 1341 { 1342 return CLI!T.parseKnownArgs(receiver, args, unrecognizedArgs); 1343 } 1344 1345 deprecated("Use CLI!(config, COMMAND).parseKnownArgs") 1346 auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config) 1347 { 1348 string[] unrecognizedArgs; 1349 1350 auto res = parseCLIKnownArgs(receiver, args, unrecognizedArgs, config); 1351 if(res) 1352 args = unrecognizedArgs; 1353 1354 return res; 1355 } 1356 1357 deprecated("Use CLI!COMMAND.parseKnownArgs") 1358 auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args) 1359 { 1360 return CLI!T.parseKnownArgs(receiver, args); 1361 } 1362 1363 deprecated("Use CLI!(config, COMMAND).parseKnownArgs") 1364 Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config) 1365 { 1366 import std.typecons : nullable; 1367 1368 T receiver; 1369 1370 return parseCLIKnownArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; 1371 } 1372 1373 deprecated("Use CLI!COMMAND.parseKnownArgs") 1374 Nullable!T parseCLIKnownArgs(T)(ref string[] args) 1375 { 1376 import std.typecons : nullable; 1377 1378 T receiver; 1379 1380 return CLI!T.parseKnownArgs(receiver, args) ? receiver.nullable : Nullable!T.init; 1381 } 1382 1383 deprecated("Use CLI!(config, COMMAND).parseArgs") 1384 int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func, in Config config, T initialValue = T.init) 1385 if(__traits(compiles, { func(T.init, args); })) 1386 { 1387 alias value = initialValue; 1388 1389 auto res = parseCLIKnownArgs(value, args, config); 1390 if(!res) 1391 return res.resultCode; 1392 1393 static if(__traits(compiles, { int a = cast(int) func(value, args); })) 1394 return cast(int) func(value, args); 1395 else 1396 { 1397 func(value, args); 1398 return 0; 1399 } 1400 } 1401 1402 deprecated("Use CLI!COMMAND.parseArgs") 1403 int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func) 1404 if(__traits(compiles, { func(T.init, args); })) 1405 { 1406 T value; 1407 1408 auto res = CLI!T.parseKnownArgs(value, args); 1409 if(!res) 1410 return res.resultCode; 1411 1412 static if(__traits(compiles, { int a = cast(int) func(value, args); })) 1413 return cast(int) func(value, args); 1414 else 1415 { 1416 func(value, args); 1417 return 0; 1418 } 1419 } 1420 1421 1422 deprecated("Use CLI!(config, COMMAND).parseArgs") 1423 auto parseCLIArgs(T)(ref T receiver, string[] args, in Config config) 1424 { 1425 string[] unrecognizedArgs; 1426 1427 auto res = parseCLIKnownArgs(receiver, args, unrecognizedArgs, config); 1428 1429 if(res && unrecognizedArgs.length > 0) 1430 { 1431 config.onError("Unrecognized arguments: ", unrecognizedArgs); 1432 return Result.Failure; 1433 } 1434 1435 return res; 1436 } 1437 1438 deprecated("Use CLI!COMMAND.parseArgs") 1439 auto parseCLIArgs(T)(ref T receiver, string[] args) 1440 { 1441 return CLI!T.parseArgs(receiver, args); 1442 } 1443 1444 deprecated("Use CLI!(config, COMMAND).parseArgs") 1445 Nullable!T parseCLIArgs(T)(string[] args, in Config config) 1446 { 1447 import std.typecons : nullable; 1448 1449 T receiver; 1450 1451 return parseCLIArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; 1452 } 1453 1454 deprecated("Use CLI!COMMAND.parseArgs") 1455 Nullable!T parseCLIArgs(T)(string[] args) 1456 { 1457 import std.typecons : nullable; 1458 1459 T receiver; 1460 1461 return CLI!T.parseArgs(receiver, args) ? receiver.nullable : Nullable!T.init; 1462 } 1463 1464 deprecated("Use CLI!(config, COMMAND).parseArgs") 1465 int parseCLIArgs(T, FUNC)(string[] args, FUNC func, in Config config, T initialValue = T.init) 1466 if(__traits(compiles, { func(T.init); })) 1467 { 1468 alias value = initialValue; 1469 1470 auto res = parseCLIArgs(value, args, config); 1471 if(!res) 1472 return res.resultCode; 1473 1474 static if(__traits(compiles, { int a = cast(int) func(value); })) 1475 return cast(int) func(value); 1476 else 1477 { 1478 func(value); 1479 return 0; 1480 } 1481 } 1482 1483 deprecated("Use CLI!COMMAND.parseArgs") 1484 int parseCLIArgs(T, FUNC)(string[] args, FUNC func) 1485 if(__traits(compiles, { func(T.init); })) 1486 { 1487 T value; 1488 1489 auto res = CLI!T.parseArgs(value, args); 1490 if(!res) 1491 return res.resultCode; 1492 1493 static if(__traits(compiles, { int a = cast(int) func(value); })) 1494 return cast(int) func(value); 1495 else 1496 { 1497 func(value); 1498 return 0; 1499 } 1500 } 1501 1502 unittest 1503 { 1504 import std.exception; 1505 1506 struct T 1507 { 1508 @(NamedArgument("--")) 1509 int a; 1510 } 1511 static assert(!__traits(compiles, { enum p = parseCLIArgs!T([]); })); 1512 assertThrown(parseCLIArgs!T([])); 1513 } 1514 1515 unittest 1516 { 1517 1518 import std.conv; 1519 import std.traits; 1520 1521 struct params 1522 { 1523 int no_a; 1524 1525 @(PositionalArgument(0, "a") 1526 .Description("Argument 'a'") 1527 .Validation!((int a) { return a > 3;}) 1528 .PreValidation!((string s) { return s.length > 0;}) 1529 .Validation!((int a) { return a > 0;}) 1530 ) 1531 int a; 1532 1533 int no_b; 1534 1535 @(NamedArgument(["b", "boo"]).Description("Flag boo") 1536 .AllowNoValue!55 1537 ) 1538 int b; 1539 1540 int no_c; 1541 } 1542 1543 enum p = CommandArguments!params(Config.init); 1544 static assert(p.findNamedArgument("a").arg is null); 1545 static assert(p.findNamedArgument("b").arg !is null); 1546 static assert(p.findNamedArgument("boo").arg !is null); 1547 static assert(p.findPositionalArgument(0).arg !is null); 1548 static assert(p.findPositionalArgument(1).arg is null); 1549 static assert(p.getParseFunction!false(p.findNamedArgument("b").index) !is null); 1550 static assert(p.getParseFunction!true(p.findNamedArgument("b").index) !is null); 1551 } 1552 1553 unittest 1554 { 1555 import std.typecons : tuple; 1556 1557 struct T 1558 { 1559 string a; 1560 string b; 1561 } 1562 1563 auto test(string[] args) 1564 { 1565 return tuple(args.parseCLIKnownArgs!T.get, args); 1566 } 1567 1568 assert(test(["-a","A","--"]) == tuple(T("A"), [])); 1569 static assert(test(["-a","A","--","-b","B"]) == tuple(T("A"), ["-b","B"])); 1570 1571 { 1572 T args; 1573 1574 args.parseCLIArgs([ "-a", "A"]); 1575 args.parseCLIArgs([ "-b", "B"]); 1576 1577 assert(args == T("A","B")); 1578 } 1579 } 1580 1581 unittest 1582 { 1583 struct T 1584 { 1585 string a; 1586 } 1587 1588 { 1589 auto test_called(string[] args) 1590 { 1591 bool called; 1592 auto dg = (T t) { 1593 called = true; 1594 }; 1595 assert(args.parseCLIArgs!T(dg) == 0 || !called); 1596 return called; 1597 } 1598 1599 static assert(test_called([])); 1600 assert(test_called([])); 1601 assert(!test_called(["-g"])); 1602 } 1603 { 1604 auto test_called(string[] args) 1605 { 1606 bool called; 1607 auto dg = (T t, string[] args) { 1608 assert(args.length == 0 || args == ["-g"]); 1609 called = true; 1610 }; 1611 assert(args.parseCLIKnownArgs!T(dg) == 0); 1612 return called; 1613 } 1614 1615 assert(test_called([])); 1616 static assert(test_called(["-g"])); 1617 } 1618 } 1619 1620 unittest 1621 { 1622 struct T 1623 { 1624 string a; 1625 } 1626 1627 int my_main(T command) 1628 { 1629 // do something 1630 return 0; 1631 } 1632 1633 static assert(["-a","aa"].parseCLIArgs!T(&my_main) == 0); 1634 assert(["-a","aa"].parseCLIArgs!T(&my_main) == 0); 1635 } 1636 1637 unittest 1638 { 1639 struct T 1640 { 1641 string a; 1642 } 1643 1644 auto args = [ "-a", "A", "-c", "C" ]; 1645 1646 assert(parseCLIKnownArgs!T(args).get == T("A")); 1647 assert(args == ["-c", "C"]); 1648 } 1649 1650 unittest 1651 { 1652 1653 struct T 1654 { 1655 @NamedArgument string x; 1656 @NamedArgument string foo; 1657 @(PositionalArgument(0, "a").Optional()) string a; 1658 @(PositionalArgument(1, "b").Optional()) string[] b; 1659 } 1660 static assert(["--foo","FOO","-x","X"].parseCLIArgs!T.get == T("X", "FOO")); 1661 static assert(["--foo=FOO","-x=X"].parseCLIArgs!T.get == T("X", "FOO")); 1662 static assert(["--foo=FOO","1","-x=X"].parseCLIArgs!T.get == T("X", "FOO", "1")); 1663 static assert(["--foo=FOO","1","2","3","4"].parseCLIArgs!T.get == T(string.init, "FOO", "1",["2","3","4"])); 1664 static assert(["-xX"].parseCLIArgs!T.get == T("X")); 1665 assert(["--foo","FOO","-x","X"].parseCLIArgs!T.get == T("X", "FOO")); 1666 assert(["--foo=FOO","-x=X"].parseCLIArgs!T.get == T("X", "FOO")); 1667 assert(["--foo=FOO","1","-x=X"].parseCLIArgs!T.get == T("X", "FOO", "1")); 1668 assert(["--foo=FOO","1","2","3","4"].parseCLIArgs!T.get == T(string.init, "FOO", "1",["2","3","4"])); 1669 assert(["-xX"].parseCLIArgs!T.get == T("X")); 1670 1671 struct T1 1672 { 1673 @(PositionalArgument(0, "a")) string[3] a; 1674 @(PositionalArgument(1, "b")) string[] b; 1675 } 1676 static assert(["1","2","3","4","5","6"].parseCLIArgs!T1.get == T1(["1","2","3"],["4","5","6"])); 1677 assert(["1","2","3","4","5","6"].parseCLIArgs!T1.get == T1(["1","2","3"],["4","5","6"])); 1678 1679 struct T2 1680 { 1681 bool foo = true; 1682 } 1683 static assert(["--no-foo"].parseCLIArgs!T2.get == T2(false)); 1684 assert(["--no-foo"].parseCLIArgs!T2.get == T2(false)); 1685 } 1686 1687 unittest 1688 { 1689 struct T 1690 { 1691 @(PositionalArgument(0, "a").Optional()) 1692 string a = "not set"; 1693 1694 @(NamedArgument.Required()) 1695 int b; 1696 } 1697 1698 assert(CLI!T.parseArgs!((T t) { assert(t == T("not set", 4)); })(["-b", "4"]) == 0); 1699 static assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); 1700 assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); 1701 } 1702 1703 unittest 1704 { 1705 struct T 1706 { 1707 string x; 1708 string foo; 1709 } 1710 1711 auto test(T)(string[] args) 1712 { 1713 Config config; 1714 config.caseSensitive = false; 1715 1716 return args.parseCLIArgs!T(config).get; 1717 } 1718 1719 static assert(test!T(["--Foo","FOO","-X","X"]) == T("X", "FOO")); 1720 static assert(test!T(["--FOo=FOO","-X=X"]) == T("X", "FOO")); 1721 assert(test!T(["--Foo","FOO","-X","X"]) == T("X", "FOO")); 1722 assert(test!T(["--FOo=FOO","-X=X"]) == T("X", "FOO")); 1723 } 1724 1725 unittest 1726 { 1727 auto test(T)(string[] args) 1728 { 1729 Config config; 1730 config.bundling = true; 1731 1732 return args.parseCLIArgs!T(config).get; 1733 } 1734 1735 struct T 1736 { 1737 bool a; 1738 bool b; 1739 } 1740 static assert(test!T(["-a","-b"]) == T(true, true)); 1741 static assert(test!T(["-ab"]) == T(true, true)); 1742 assert(test!T(["-a","-b"]) == T(true, true)); 1743 assert(test!T(["-ab"]) == T(true, true)); 1744 } 1745 1746 unittest 1747 { 1748 struct T 1749 { 1750 bool b; 1751 } 1752 1753 assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); })(["-b"]) == 0); 1754 assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); })(["-b=true"]) == 0); 1755 assert(CLI!T.parseArgs!((T t) { assert(t == T(false)); })(["-b=false"]) == 0); 1756 static assert(["-b"] .parseCLIArgs!T.get == T(true)); 1757 static assert(["-b=true"] .parseCLIArgs!T.get == T(true)); 1758 static assert(["-b=false"] .parseCLIArgs!T.get == T(false)); 1759 assert(["-b"] .parseCLIArgs!T.get == T(true)); 1760 assert(["-b=true"] .parseCLIArgs!T.get == T(true)); 1761 assert(["-b=false"] .parseCLIArgs!T.get == T(false)); 1762 } 1763 1764 unittest 1765 { 1766 struct T 1767 { 1768 struct cmd1 { string a; } 1769 struct cmd2 1770 { 1771 string b; 1772 1773 @TrailingArguments 1774 string[] args; 1775 } 1776 1777 string c; 1778 string d; 1779 1780 SumType!(cmd1, cmd2) cmd; 1781 } 1782 1783 assert(["-c","C","cmd2","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(T.cmd2("B")))); 1784 assert(["-c","C","cmd2","--","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(T.cmd2("",["-b","B"])))); 1785 } 1786 1787 unittest 1788 { 1789 struct T 1790 { 1791 struct cmd1 {} 1792 struct cmd2 {} 1793 1794 SumType!(cmd1, cmd2) cmd; 1795 } 1796 1797 assert(["cmd2"].parseCLIArgs!T.get == T(typeof(T.cmd)(T.cmd2.init))); 1798 assert(["cmd1"].parseCLIArgs!T.get == T(typeof(T.cmd)(T.cmd1.init))); 1799 } 1800 1801 unittest 1802 { 1803 struct T 1804 { 1805 struct cmd1 { string a; } 1806 struct cmd2 { string b; } 1807 1808 string c; 1809 string d; 1810 1811 SumType!(cmd1, Default!cmd2) cmd; 1812 } 1813 1814 assert(["-c","C","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(Default!(T.cmd2)(T.cmd2("B"))))); 1815 assert(!CLI!T.parseArgs!((_) {assert(false);})(["-h"])); 1816 assert(!CLI!T.parseArgs!((_) {assert(false);})(["--help"])); 1817 } 1818 1819 deprecated("Use CLI!(Config, COMMAND).main or CLI!(COMMAND).main") 1820 struct Main 1821 { 1822 mixin template parseCLIKnownArgs(TYPE, alias newMain, Config config = Config.init) 1823 { 1824 mixin CLI!(config, TYPE).main!newMain; 1825 } 1826 1827 mixin template parseCLIArgs(TYPE, alias newMain, Config config = Config.init) 1828 { 1829 mixin CLI!(config, TYPE).main!newMain; 1830 } 1831 } 1832 1833 private template defaultCommandName(COMMAND) 1834 { 1835 static if(getUDAs!(COMMAND, CommandInfo).length > 0) 1836 enum defaultCommandName = getUDAs!(COMMAND, CommandInfo)[0].names[0]; 1837 else 1838 enum defaultCommandName = COMMAND.stringof; 1839 } 1840 1841 private struct Complete(COMMAND) 1842 { 1843 @(Command("init") 1844 .Description("Print initialization script for shell completion.") 1845 .ShortDescription("Print initialization script.") 1846 ) 1847 struct Init 1848 { 1849 @MutuallyExclusive 1850 { 1851 @(NamedArgument.Description("Provide completion for bash.")) 1852 bool bash; 1853 @(NamedArgument.Description("Provide completion for zsh.")) 1854 bool zsh; 1855 @(NamedArgument.Description("Provide completion for tcsh.")) 1856 bool tcsh; 1857 @(NamedArgument.Description("Provide completion for fish.")) 1858 bool fish; 1859 } 1860 1861 @(NamedArgument.Description("Path to completer. Default value: path to this executable.")) 1862 string completerPath; // path to this binary 1863 1864 @(NamedArgument.Description("Command name. Default value: "~defaultCommandName!COMMAND~".")) 1865 string commandName = defaultCommandName!COMMAND; // command to complete 1866 1867 void execute(Config config)() 1868 { 1869 import std.stdio: writeln; 1870 1871 if(completerPath.length == 0) 1872 { 1873 import std.file: thisExePath; 1874 completerPath = thisExePath(); 1875 } 1876 1877 string commandNameArg; 1878 if(commandName != defaultCommandName!COMMAND) 1879 commandNameArg = " --commandName "~commandName; 1880 1881 if(bash) 1882 { 1883 // According to bash documentation: 1884 // When the function or command is invoked, the first argument ($1) is the name of the command whose 1885 // arguments are being completed, the second` argument ($2) is the word being completed, and the third 1886 // argument ($3) is the word preceding the word being completed on the current command line. 1887 // 1888 // So we add "---" argument to distinguish between the end of actual parameters and those that were added by bash 1889 1890 writeln("# Add this source command into .bashrc:"); 1891 writeln("# source <(", completerPath, " init --bash", commandNameArg, ")"); 1892 // 'eval' is used to properly get arguments with spaces. For example, in case of "1 2" argument, 1893 // we will get "1 2" as is, compare to "\"1", "2\"" without 'eval'. 1894 writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName); 1895 } 1896 else if(zsh) 1897 { 1898 // We use bash completion for zsh 1899 writeln("# Ensure that you called compinit and bashcompinit like below in your .zshrc:"); 1900 writeln("# autoload -Uz compinit && compinit"); 1901 writeln("# autoload -Uz bashcompinit && bashcompinit"); 1902 writeln("# And then add this source command after them into your .zshrc:"); 1903 writeln("# source <(", completerPath, " init --zsh", commandNameArg, ")"); 1904 writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName); 1905 } 1906 else if(tcsh) 1907 { 1908 // Comments start with ":" in tsch 1909 writeln(": Add this eval command into .tcshrc: ;"); 1910 writeln(": eval `", completerPath, " init --tcsh", commandNameArg, "` ;"); 1911 writeln("complete ", commandName, " 'p,*,`", completerPath, " --tcsh -- $COMMAND_LINE`,'"); 1912 } 1913 else if(fish) 1914 { 1915 writeln("# Add this source command into ~/.config/fish/config.fish:"); 1916 writeln("# ", completerPath, " init --fish", commandNameArg, " | source"); 1917 writeln("complete -c ", commandName, " -a '(COMMAND_LINE=(commandline -p) ", completerPath, " --fish -- (commandline -op))' --no-files"); 1918 } 1919 } 1920 } 1921 1922 @(Command("complete") 1923 .Description("Print completion.") 1924 ) 1925 struct Complete 1926 { 1927 @MutuallyExclusive 1928 { 1929 @(NamedArgument.Description("Provide completion for bash.")) 1930 bool bash; 1931 @(NamedArgument.Description("Provide completion for tcsh.")) 1932 bool tcsh; 1933 @(NamedArgument.Description("Provide completion for fish.")) 1934 bool fish; 1935 } 1936 1937 @TrailingArguments 1938 string[] args; 1939 1940 void execute(Config config)() 1941 { 1942 import std.process: environment; 1943 import std.stdio: writeln; 1944 import std.algorithm: each; 1945 1946 if(bash) 1947 { 1948 // According to bash documentation: 1949 // When the function or command is invoked, the first argument ($1) is the name of the command whose 1950 // arguments are being completed, the second` argument ($2) is the word being completed, and the third 1951 // argument ($3) is the word preceding the word being completed on the current command line. 1952 // 1953 // We don't use these arguments so we just remove those after "---" including itself 1954 while(args.length > 0 && args[$-1] != "---") 1955 args = args[0..$-1]; 1956 1957 // Remove "---" 1958 if(args.length > 0 && args[$-1] == "---") 1959 args = args[0..$-1]; 1960 1961 // COMP_LINE environment variable contains current command line so if it ends with space ' ' then we 1962 // should provide all available arguments. To do so, we add an empty argument 1963 auto cmdLine = environment.get("COMP_LINE", ""); 1964 if(cmdLine.length > 0 && cmdLine[$-1] == ' ') 1965 args ~= ""; 1966 } 1967 else if(tcsh || fish) 1968 { 1969 // COMMAND_LINE environment variable contains current command line so if it ends with space ' ' then we 1970 // should provide all available arguments. To do so, we add an empty argument 1971 auto cmdLine = environment.get("COMMAND_LINE", ""); 1972 if(cmdLine.length > 0 && cmdLine[$-1] == ' ') 1973 args ~= ""; 1974 } 1975 1976 CLI!(config, COMMAND).completeArgs(args).each!writeln; 1977 } 1978 } 1979 1980 @SubCommands 1981 SumType!(Init, Default!Complete) cmd; 1982 } 1983 1984 template CLI(Config config, COMMANDS...) 1985 { 1986 mixin template main(alias newMain) 1987 { 1988 import std.sumtype: SumType, match; 1989 1990 private struct Program 1991 { 1992 SumType!COMMANDS cmd; // Sub-commands 1993 } 1994 1995 private auto forwardMain(Args...)(Program prog, auto ref Args args) 1996 { 1997 import core.lifetime: forward; 1998 return prog.cmd.match!(_ => newMain(_, forward!args)); 1999 } 2000 2001 mixin CLI!(config, Program).main!forwardMain; 2002 } 2003 } 2004 2005 template CLI(Config config, COMMAND) 2006 { 2007 static Result parseKnownArgs(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) 2008 { 2009 auto parser = Parser(config, args); 2010 2011 auto command = CommandArguments!COMMAND(config); 2012 auto res = parser.parseAll!false(command, receiver); 2013 if(!res) 2014 return res; 2015 2016 unrecognizedArgs = parser.unrecognizedArgs; 2017 2018 return Result.Success; 2019 } 2020 2021 static Result parseKnownArgs(ref COMMAND receiver, ref string[] args) 2022 { 2023 string[] unrecognizedArgs; 2024 2025 auto res = parseKnownArgs(receiver, args, unrecognizedArgs); 2026 if(res) 2027 args = unrecognizedArgs; 2028 2029 return res; 2030 } 2031 2032 static Result parseArgs(ref COMMAND receiver, string[] args) 2033 { 2034 auto res = parseKnownArgs(receiver, args); 2035 if(res && args.length > 0) 2036 { 2037 config.onError("Unrecognized arguments: ", args); 2038 return Result.Failure; 2039 } 2040 2041 return res; 2042 } 2043 2044 static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) 2045 if(__traits(compiles, { newMain(COMMAND.init); })) 2046 { 2047 alias value = initialValue; 2048 2049 auto res = parseArgs(value, args); 2050 if(!res) 2051 return res.resultCode; 2052 2053 static if(__traits(compiles, { int a = cast(int) newMain(value); })) 2054 return cast(int) newMain(value); 2055 else 2056 { 2057 newMain(value); 2058 return 0; 2059 } 2060 } 2061 2062 static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) 2063 if(__traits(compiles, { newMain(COMMAND.init, string[].init); })) 2064 { 2065 alias value = initialValue; 2066 2067 auto res = parseKnownArgs(value, args); 2068 if(!res) 2069 return res.resultCode; 2070 2071 static if(__traits(compiles, { int a = cast(int) newMain(value, args); })) 2072 return cast(int) newMain(value, args); 2073 else 2074 { 2075 newMain(value, args); 2076 return 0; 2077 } 2078 } 2079 2080 string[] completeArgs(string[] args) 2081 { 2082 import std.algorithm: sort, uniq; 2083 import std.array: array; 2084 2085 auto command = CommandArguments!COMMAND(config); 2086 2087 auto parser = Parser(config, args.length == 0 ? [""] : args); 2088 2089 COMMAND dummy; 2090 2091 auto res = parser.parseAll!true(command, dummy); 2092 2093 return res ? res.suggestions.dup.sort.uniq.array : []; 2094 } 2095 2096 int complete(string[] args) 2097 { 2098 // dmd fails with core.exception.OutOfMemoryError@core\lifetime.d(137): Memory allocation failed 2099 // if we call anything from CLI!(config, Complete!COMMAND) so we have to directly call parser here 2100 2101 Complete!COMMAND receiver; 2102 2103 auto parser = Parser(config, args); 2104 2105 auto command = CommandArguments!(Complete!COMMAND)(config); 2106 auto res = parser.parseAll!false(command, receiver); 2107 if(!res) 2108 return 1; 2109 2110 if(res && parser.unrecognizedArgs.length > 0) 2111 { 2112 config.onError("Unrecognized arguments: ", parser.unrecognizedArgs); 2113 return 1; 2114 } 2115 2116 receiver.cmd.match!(_ => _.execute!config()); 2117 2118 return 0; 2119 } 2120 2121 mixin template mainComplete() 2122 { 2123 int main(string[] argv) 2124 { 2125 return CLI!(config, COMMAND).complete(argv[1..$]); 2126 } 2127 } 2128 2129 mixin template main(alias newMain) 2130 { 2131 version(argparse_completion) 2132 { 2133 mixin CLI!(config, COMMAND).mainComplete; 2134 } 2135 else 2136 { 2137 int main(string[] argv) 2138 { 2139 return CLI!(config, COMMAND).parseArgs!(newMain)(argv[1..$]); 2140 } 2141 } 2142 } 2143 } 2144 2145 deprecated("Use CLI!(Config, COMMAND) or CLI!(COMMAND)") 2146 template CLI(Config config) 2147 { 2148 mixin template main(COMMAND, alias newMain) 2149 { 2150 mixin CLI!(config, COMMAND).main!newMain; 2151 } 2152 } 2153 2154 alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS); 2155 2156 2157 unittest 2158 { 2159 struct T 2160 { 2161 struct cmd1 2162 { 2163 string foo; 2164 string bar; 2165 string baz; 2166 } 2167 struct cmd2 2168 { 2169 string cat,can,dog; 2170 } 2171 2172 @NamedArgument("apple","a") 2173 string a = "dummyA"; 2174 @NamedArgument 2175 string s = "dummyS"; 2176 @NamedArgument 2177 string b = "dummyB"; 2178 2179 @SubCommands 2180 SumType!(cmd1, cmd2) cmd; 2181 } 2182 2183 assert(CLI!T.completeArgs([]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 2184 assert(CLI!T.completeArgs([""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 2185 assert(CLI!T.completeArgs(["-a"]) == ["-a"]); 2186 assert(CLI!T.completeArgs(["c"]) == ["cmd1","cmd2"]); 2187 assert(CLI!T.completeArgs(["cmd1"]) == ["cmd1"]); 2188 assert(CLI!T.completeArgs(["cmd1",""]) == ["--apple","--bar","--baz","--foo","--help","-a","-b","-h","-s","cmd1","cmd2"]); 2189 assert(CLI!T.completeArgs(["-a","val-a",""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 2190 2191 assert(!CLI!T.complete(["init","--bash","--commandName","mytool"])); 2192 assert(!CLI!T.complete(["init","--zsh"])); 2193 assert(!CLI!T.complete(["init","--tcsh"])); 2194 assert(!CLI!T.complete(["init","--fish"])); 2195 2196 assert(CLI!T.complete(["init","--unknown"])); 2197 2198 import std.process: environment; 2199 { 2200 environment["COMP_LINE"] = "mytool "; 2201 assert(!CLI!T.complete(["--bash","--","---","foo","foo"])); 2202 2203 environment["COMP_LINE"] = "mytool c"; 2204 assert(!CLI!T.complete(["--bash","--","c","---"])); 2205 2206 environment.remove("COMP_LINE"); 2207 } 2208 { 2209 environment["COMMAND_LINE"] = "mytool "; 2210 assert(!CLI!T.complete(["--tcsh","--"])); 2211 2212 environment["COMMAND_LINE"] = "mytool c"; 2213 assert(!CLI!T.complete(["--fish","--","c"])); 2214 2215 environment.remove("COMMAND_LINE"); 2216 } 2217 } 2218 2219 2220 unittest 2221 { 2222 struct T 2223 { 2224 int a; 2225 } 2226 2227 static assert(__traits(compiles, { mixin CLI!T.main!((params) => 0); })); 2228 static assert(__traits(compiles, { mixin CLI!T.main!((params, args) => 0); })); 2229 } 2230 2231 2232 private struct Parsers 2233 { 2234 static auto Convert(T)(string value) 2235 { 2236 import std.conv: to; 2237 return value.length > 0 ? value.to!T : T.init; 2238 } 2239 2240 static auto PassThrough(string[] values) 2241 { 2242 return values; 2243 } 2244 } 2245 2246 unittest 2247 { 2248 static assert(Parsers.Convert!int("7") == 7); 2249 static assert(Parsers.Convert!string("7") == "7"); 2250 static assert(Parsers.Convert!char("7") == '7'); 2251 2252 static assert(Parsers.PassThrough(["7","8"]) == ["7","8"]); 2253 } 2254 2255 2256 private struct Actions 2257 { 2258 static auto Assign(DEST, SRC=DEST)(ref DEST param, SRC value) 2259 { 2260 param = value; 2261 } 2262 2263 static auto Append(T)(ref T param, T value) 2264 { 2265 param ~= value; 2266 } 2267 2268 static auto Extend(T)(ref T[] param, T value) 2269 { 2270 param ~= value; 2271 } 2272 2273 static auto CallFunction(F)(ref F func, RawParam param) 2274 { 2275 // ... func() 2276 static if(__traits(compiles, { func(); })) 2277 { 2278 func(); 2279 } 2280 // ... func(string value) 2281 else static if(__traits(compiles, { func(param.value[0]); })) 2282 { 2283 foreach(value; param.value) 2284 func(value); 2285 } 2286 // ... func(string[] value) 2287 else static if(__traits(compiles, { func(param.value); })) 2288 { 2289 func(param.value); 2290 } 2291 // ... func(RawParam param) 2292 else static if(__traits(compiles, { func(param); })) 2293 { 2294 func(param); 2295 } 2296 else 2297 static assert(false, "Unsupported callback: " ~ F.stringof); 2298 } 2299 2300 static auto CallFunctionNoParam(F)(ref F func, Param!void param) 2301 { 2302 // ... func() 2303 static if(__traits(compiles, { func(); })) 2304 { 2305 func(); 2306 } 2307 // ... func(string value) 2308 else static if(__traits(compiles, { func(string.init); })) 2309 { 2310 func(string.init); 2311 } 2312 // ... func(string[] value) 2313 else static if(__traits(compiles, { func([]); })) 2314 { 2315 func([]); 2316 } 2317 // ... func(Param!void param) 2318 else static if(__traits(compiles, { func(param); })) 2319 { 2320 func(param); 2321 } 2322 else 2323 static assert(false, "Unsupported callback: " ~ F.stringof); 2324 } 2325 } 2326 2327 unittest 2328 { 2329 int i; 2330 Actions.Assign!(int)(i,7); 2331 assert(i == 7); 2332 } 2333 2334 unittest 2335 { 2336 int[] i; 2337 Actions.Append!(int[])(i,[1,2,3]); 2338 Actions.Append!(int[])(i,[7,8,9]); 2339 assert(i == [1,2,3,7,8,9]); 2340 2341 alias test = (int[] v1, int[] v2) { 2342 int[] res; 2343 2344 Param!(int[]) param; 2345 2346 alias F = Actions.Append!(int[]); 2347 param.value = v1; ActionFunc!(F, int[], int[])(res, param); 2348 2349 param.value = v2; ActionFunc!(F, int[], int[])(res, param); 2350 2351 return res; 2352 }; 2353 static assert(test([1,2,3],[7,8,9]) == [1,2,3,7,8,9]); 2354 } 2355 2356 unittest 2357 { 2358 int[][] i; 2359 Actions.Extend!(int[])(i,[1,2,3]); 2360 Actions.Extend!(int[])(i,[7,8,9]); 2361 assert(i == [[1,2,3],[7,8,9]]); 2362 } 2363 2364 2365 private struct Validators 2366 { 2367 static auto ValueInList(alias values, TYPE)(in Param!TYPE param) 2368 { 2369 import std.array : assocArray, join; 2370 import std.range : repeat, front; 2371 import std.conv: to; 2372 2373 enum valuesAA = assocArray(values, false.repeat); 2374 enum allowedValues = values.to!(string[]).join(','); 2375 2376 static if(is(typeof(values.front) == TYPE)) 2377 auto paramValues = [param.value]; 2378 else 2379 auto paramValues = param.value; 2380 2381 foreach(value; paramValues) 2382 if(!(value in valuesAA)) 2383 { 2384 param.config.onError("Invalid value '", value, "' for argument '", param.name, "'.\nValid argument values are: ", allowedValues); 2385 return false; 2386 } 2387 2388 return true; 2389 } 2390 } 2391 2392 2393 // values => bool 2394 // bool validate(T value) 2395 // bool validate(T[i] value) 2396 // bool validate(Param!T param) 2397 private struct ValidateFunc(alias F, T, string funcName="Validation") 2398 { 2399 static bool opCall(Param!T param) 2400 { 2401 static if(is(F == void)) 2402 { 2403 return true; 2404 } 2405 else static if(__traits(compiles, { F(param); })) 2406 { 2407 // bool validate(Param!T param) 2408 return cast(bool) F(param); 2409 } 2410 else static if(__traits(compiles, { F(param.value); })) 2411 { 2412 // bool validate(T values) 2413 return cast(bool) F(param.value); 2414 } 2415 else static if(/*isArray!T &&*/ __traits(compiles, { F(param.value[0]); })) 2416 { 2417 // bool validate(T[i] value) 2418 foreach(value; param.value) 2419 if(!F(value)) 2420 return false; 2421 return true; 2422 } 2423 else 2424 static assert(false, funcName~" function is not supported for type "~T.stringof~": "~typeof(F).stringof); 2425 } 2426 } 2427 2428 unittest 2429 { 2430 auto test(alias F, T)(T[] values) 2431 { 2432 Param!(T[]) param; 2433 param.value = values; 2434 return ValidateFunc!(F, T[])(param); 2435 } 2436 2437 // bool validate(T[] values) 2438 static assert(test!((string[] a) => true, string)(["1","2","3"])); 2439 static assert(test!((int[] a) => true, int)([1,2,3])); 2440 2441 // bool validate(T value) 2442 static assert(test!((string a) => true, string)(["1","2","3"])); 2443 static assert(test!((int a) => true, int)([1,2,3])); 2444 2445 // bool validate(Param!T param) 2446 static assert(test!((RawParam p) => true, string)(["1","2","3"])); 2447 static assert(test!((Param!(int[]) p) => true, int)([1,2,3])); 2448 } 2449 2450 unittest 2451 { 2452 static assert(ValidateFunc!(void, string[])(RawParam(Config.init, "", ["1","2","3"]))); 2453 2454 static assert(!__traits(compiles, { ValidateFunc!(() {}, string[])(config, "", ["1","2","3"]); })); 2455 static assert(!__traits(compiles, { ValidateFunc!((int,int) {}, string[])(config, "", ["1","2","3"]); })); 2456 } 2457 2458 2459 private template ParseType(alias F, T) 2460 { 2461 import std.traits : Unqual; 2462 2463 static if(is(F == void)) 2464 alias ParseType = Unqual!T; 2465 else static if(Parameters!F.length == 0) 2466 static assert(false, "Parse function should take at least one parameter"); 2467 else static if(Parameters!F.length == 1) 2468 { 2469 // T action(arg) 2470 alias ParseType = Unqual!(ReturnType!F); 2471 static assert(!is(ParseType == void), "Parse function should return value"); 2472 } 2473 else static if(Parameters!F.length == 2 && is(Parameters!F[0] == Config)) 2474 { 2475 // T action(Config config, arg) 2476 alias ParseType = Unqual!(ReturnType!F); 2477 static assert(!is(ParseType == void), "Parse function should return value"); 2478 } 2479 else static if(Parameters!F.length == 2) 2480 { 2481 // ... action(ref T param, arg) 2482 alias ParseType = Parameters!F[0]; 2483 } 2484 else static if(Parameters!F.length == 3) 2485 { 2486 // ... action(Config config, ref T param, arg) 2487 alias ParseType = Parameters!F[1]; 2488 } 2489 else static if(Parameters!F.length == 4) 2490 { 2491 // ... action(Config config, string argName, ref T param, arg) 2492 alias ParseType = Parameters!F[2]; 2493 } 2494 else 2495 static assert(false, "Parse function has too many parameters: "~Parameters!F.stringof); 2496 } 2497 2498 unittest 2499 { 2500 static assert(is(ParseType!(void, double) == double)); 2501 static assert(!__traits(compiles, { ParseType!((){}, double) p; })); 2502 static assert(!__traits(compiles, { ParseType!((int,int,int,int,int){}, double) p; })); 2503 2504 // T action(arg) 2505 static assert(is(ParseType!((int)=>3, double) == int)); 2506 static assert(!__traits(compiles, { ParseType!((int){}, double) p; })); 2507 // T action(Config config, arg) 2508 static assert(is(ParseType!((Config config, int)=>3, double) == int)); 2509 static assert(!__traits(compiles, { ParseType!((Config config, int){}, double) p; })); 2510 // ... action(ref T param, arg) 2511 static assert(is(ParseType!((ref int, string v) {}, double) == int)); 2512 // ... action(Config config, ref T param, arg) 2513 static assert(is(ParseType!((Config config, ref int, string v) {}, double) == int)); 2514 // ... action(Config config, string argName, ref T param, arg) 2515 //static assert(is(ParseType!((Config config, string argName, ref int, string v) {}, double) == int)); 2516 } 2517 2518 2519 // T parse(string[] values) 2520 // T parse(string value) 2521 // T parse(RawParam param) 2522 // bool parse(ref T receiver, RawParam param) 2523 // void parse(ref T receiver, RawParam param) 2524 private struct ParseFunc(alias F, T) 2525 { 2526 alias ParseType = .ParseType!(F, T); 2527 2528 static bool opCall(ref ParseType receiver, RawParam param) 2529 { 2530 static if(is(F == void)) 2531 { 2532 foreach(value; param.value) 2533 receiver = Parsers.Convert!T(value); 2534 return true; 2535 } 2536 // T parse(string[] values) 2537 else static if(__traits(compiles, { receiver = cast(ParseType) F(param.value); })) 2538 { 2539 receiver = cast(ParseType) F(param.value); 2540 return true; 2541 } 2542 // T parse(string value) 2543 else static if(__traits(compiles, { receiver = cast(ParseType) F(param.value[0]); })) 2544 { 2545 foreach(value; param.value) 2546 receiver = cast(ParseType) F(value); 2547 return true; 2548 } 2549 // T parse(RawParam param) 2550 else static if(__traits(compiles, { receiver = cast(ParseType) F(param); })) 2551 { 2552 receiver = cast(ParseType) F(param); 2553 return true; 2554 } 2555 // bool parse(ref T receiver, RawParam param) 2556 // void parse(ref T receiver, RawParam param) 2557 else static if(__traits(compiles, { F(receiver, param); })) 2558 { 2559 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 2560 { 2561 // bool parse(ref T receiver, RawParam param) 2562 return cast(bool) F(receiver, param); 2563 } 2564 else 2565 { 2566 // void parse(ref T receiver, RawParam param) 2567 F(receiver, param); 2568 return true; 2569 } 2570 } 2571 else 2572 static assert(false, "Parse function is not supported"); 2573 } 2574 } 2575 2576 unittest 2577 { 2578 int i; 2579 RawParam param; 2580 param.value = ["1","2","3"]; 2581 assert(ParseFunc!(void, int)(i, param)); 2582 assert(i == 3); 2583 } 2584 2585 unittest 2586 { 2587 auto test(alias F, T)(string[] values) 2588 { 2589 T value; 2590 RawParam param; 2591 param.value = values; 2592 assert(ParseFunc!(F, T)(value, param)); 2593 return value; 2594 } 2595 2596 // T parse(string value) 2597 static assert(test!((string a) => a, string)(["1","2","3"]) == "3"); 2598 2599 // T parse(string[] values) 2600 static assert(test!((string[] a) => a, string[])(["1","2","3"]) == ["1","2","3"]); 2601 2602 // T parse(RawParam param) 2603 static assert(test!((RawParam p) => p.value[0], string)(["1","2","3"]) == "1"); 2604 2605 // bool parse(ref T receiver, RawParam param) 2606 static assert(test!((ref string[] r, RawParam p) { r = p.value; return true; }, string[])(["1","2","3"]) == ["1","2","3"]); 2607 2608 // void parse(ref T receiver, RawParam param) 2609 static assert(test!((ref string[] r, RawParam p) { r = p.value; }, string[])(["1","2","3"]) == ["1","2","3"]); 2610 } 2611 2612 2613 // bool action(ref T receiver, ParseType value) 2614 // void action(ref T receiver, ParseType value) 2615 // bool action(ref T receiver, Param!ParseType param) 2616 // void action(ref T receiver, Param!ParseType param) 2617 private struct ActionFunc(alias F, T, ParseType) 2618 { 2619 static bool opCall(ref T receiver, Param!ParseType param) 2620 { 2621 static if(is(F == void)) 2622 { 2623 Actions.Assign!(T, ParseType)(receiver, param.value); 2624 return true; 2625 } 2626 // bool action(ref T receiver, ParseType value) 2627 // void action(ref T receiver, ParseType value) 2628 else static if(__traits(compiles, { F(receiver, param.value); })) 2629 { 2630 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param.value); })) 2631 { 2632 // bool action(ref T receiver, ParseType value) 2633 return cast(bool) F(receiver, param.value); 2634 } 2635 else 2636 { 2637 // void action(ref T receiver, ParseType value) 2638 F(receiver, param.value); 2639 return true; 2640 } 2641 } 2642 // bool action(ref T receiver, Param!ParseType param) 2643 // void action(ref T receiver, Param!ParseType param) 2644 else static if(__traits(compiles, { F(receiver, param); })) 2645 { 2646 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 2647 { 2648 // bool action(ref T receiver, Param!ParseType param) 2649 return cast(bool) F(receiver, param); 2650 } 2651 else 2652 { 2653 // void action(ref T receiver, Param!ParseType param) 2654 F(receiver, param); 2655 return true; 2656 } 2657 } 2658 else 2659 static assert(false, "Action function is not supported"); 2660 } 2661 } 2662 2663 unittest 2664 { 2665 auto param(T)(T values) 2666 { 2667 Param!T param; 2668 param.value = values; 2669 return param; 2670 } 2671 auto test(alias F, T)(T values) 2672 { 2673 T receiver; 2674 assert(ActionFunc!(F, T, T)(receiver, param(values))); 2675 return receiver; 2676 } 2677 2678 static assert(test!(void, string[])(["1","2","3"]) == ["1","2","3"]); 2679 2680 static assert(!__traits(compiles, { test!(() {}, string[])(["1","2","3"]); })); 2681 static assert(!__traits(compiles, { test!((int,int) {}, string[])(["1","2","3"]); })); 2682 2683 // bool action(ref T receiver, ParseType value) 2684 static assert(test!((ref string[] p, string[] a) { p=a; return true; }, string[])(["1","2","3"]) == ["1","2","3"]); 2685 2686 // void action(ref T receiver, ParseType value) 2687 static assert(test!((ref string[] p, string[] a) { p=a; }, string[])(["1","2","3"]) == ["1","2","3"]); 2688 2689 // bool action(ref T receiver, Param!ParseType param) 2690 static assert(test!((ref string[] p, Param!(string[]) a) { p=a.value; return true; }, string[]) (["1","2","3"]) == ["1","2","3"]); 2691 2692 // void action(ref T receiver, Param!ParseType param) 2693 static assert(test!((ref string[] p, Param!(string[]) a) { p=a.value; }, string[])(["1","2","3"]) == ["1","2","3"]); 2694 } 2695 2696 2697 // => receiver + bool 2698 // DEST action() 2699 // bool action(ref DEST receiver) 2700 // void action(ref DEST receiver) 2701 // bool action(ref DEST receiver, Param!void param) 2702 // void action(ref DEST receiver, Param!void param) 2703 private struct NoValueActionFunc(alias F, T) 2704 { 2705 static bool opCall(ref T receiver, Param!void param) 2706 { 2707 static if(is(F == void)) 2708 { 2709 assert(false, "No-value action function is not provided"); 2710 } 2711 else static if(__traits(compiles, { receiver = cast(T) F(); })) 2712 { 2713 // DEST action() 2714 receiver = cast(T) F(); 2715 return true; 2716 } 2717 else static if(__traits(compiles, { F(receiver); })) 2718 { 2719 static if(__traits(compiles, { auto res = cast(bool) F(receiver); })) 2720 { 2721 // bool action(ref DEST receiver) 2722 return cast(bool) F(receiver); 2723 } 2724 else 2725 { 2726 // void action(ref DEST receiver) 2727 F(receiver); 2728 return true; 2729 } 2730 } 2731 else static if(__traits(compiles, { F(receiver, param); })) 2732 { 2733 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 2734 { 2735 // bool action(ref DEST receiver, Param!void param) 2736 return cast(bool) F(receiver, param); 2737 } 2738 else 2739 { 2740 // void action(ref DEST receiver, Param!void param) 2741 F(receiver, param); 2742 return true; 2743 } 2744 } 2745 else 2746 static assert(false, "No-value action function has too many parameters: "~Parameters!F.stringof); 2747 } 2748 } 2749 2750 unittest 2751 { 2752 auto test(alias F, T)() 2753 { 2754 T receiver; 2755 assert(NoValueActionFunc!(F, T)(receiver, Param!void.init)); 2756 return receiver; 2757 } 2758 2759 static assert(!__traits(compiles, { NoValueActionFunc!(() {}, int); })); 2760 static assert(!__traits(compiles, { NoValueActionFunc!((int) {}, int); })); 2761 static assert(!__traits(compiles, { NoValueActionFunc!((int,int) {}, int); })); 2762 static assert(!__traits(compiles, { NoValueActionFunc!((int,int,int) {}, int); })); 2763 2764 // DEST action() 2765 static assert(test!(() => 7, int) == 7); 2766 2767 // bool action(ref DEST param) 2768 static assert(test!((ref int p) { p=7; return true; }, int) == 7); 2769 2770 // void action(ref DEST param) 2771 static assert(test!((ref int p) { p=7; }, int) == 7); 2772 2773 // bool action(ref DEST receiver, Param!void param) 2774 static assert(test!((ref int r, Param!void p) { r=7; return true; }, int) == 7); 2775 2776 // void action(ref DEST receiver, Param!void param) 2777 static assert(test!((ref int r, Param!void p) { r=7; }, int) == 7); 2778 } 2779 2780 2781 private void splitValues(ref RawParam param) 2782 { 2783 if(param.config.arraySep == char.init) 2784 return; 2785 2786 import std.array : array, split; 2787 import std.algorithm : map, joiner; 2788 2789 param.value = param.value.map!((string s) => s.split(param.config.arraySep)).joiner.array; 2790 } 2791 2792 unittest 2793 { 2794 alias test = (char arraySep, string[] values) 2795 { 2796 Config config; 2797 config.arraySep = arraySep; 2798 2799 auto param = RawParam(config, "", values); 2800 2801 splitValues(param); 2802 2803 return param.value; 2804 }; 2805 2806 static assert(test(',', []) == []); 2807 static assert(test(',', ["a","b","c"]) == ["a","b","c"]); 2808 static assert(test(',', ["a,b","c","d,e,f"]) == ["a","b","c","d","e","f"]); 2809 static assert(test(' ', ["a,b","c","d,e,f"]) == ["a,b","c","d,e,f"]); 2810 } 2811 2812 2813 private struct ValueParseFunctions(alias PreProcess, 2814 alias PreValidation, 2815 alias Parse, 2816 alias Validation, 2817 alias Action, 2818 alias NoValueAction) 2819 { 2820 alias changePreProcess (alias func) = ValueParseFunctions!( func, PreValidation, Parse, Validation, Action, NoValueAction); 2821 alias changePreValidation(alias func) = ValueParseFunctions!(PreProcess, func, Parse, Validation, Action, NoValueAction); 2822 alias changeParse (alias func) = ValueParseFunctions!(PreProcess, PreValidation, func, Validation, Action, NoValueAction); 2823 alias changeValidation (alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, func, Action, NoValueAction); 2824 alias changeAction (alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, Validation, func, NoValueAction); 2825 alias changeNoValueAction(alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, Validation, Action, func); 2826 2827 template addDefaults(T) 2828 { 2829 static if(is(PreProcess == void)) 2830 alias preProc = DefaultValueParseFunctions!T; 2831 else 2832 alias preProc = DefaultValueParseFunctions!T.changePreProcess!PreProcess; 2833 2834 static if(is(PreValidation == void)) 2835 alias preVal = preProc; 2836 else 2837 alias preVal = preProc.changePreValidation!PreValidation; 2838 2839 static if(is(Parse == void)) 2840 alias parse = preVal; 2841 else 2842 alias parse = preVal.changeParse!Parse; 2843 2844 static if(is(Validation == void)) 2845 alias val = parse; 2846 else 2847 alias val = parse.changeValidation!Validation; 2848 2849 static if(is(Action == void)) 2850 alias action = val; 2851 else 2852 alias action = val.changeAction!Action; 2853 2854 static if(is(NoValueAction == void)) 2855 alias addDefaults = action; 2856 else 2857 alias addDefaults = action.changeNoValueAction!NoValueAction; 2858 } 2859 2860 2861 // Procedure to process (parse) the values to an argument of type T 2862 // - if there is a value(s): 2863 // - pre validate raw strings 2864 // - parse raw strings 2865 // - validate parsed values 2866 // - action with values 2867 // - if there is no value: 2868 // - action if no value 2869 // Requirement: rawValues.length must be correct 2870 static bool parse(T)(ref T receiver, RawParam param) 2871 { 2872 return addDefaults!T.parseImpl(receiver, param); 2873 } 2874 static bool parseImpl(T)(ref T receiver, ref RawParam rawParam) 2875 { 2876 alias ParseType(T) = .ParseType!(Parse, T); 2877 2878 alias preValidation = ValidateFunc!(PreValidation, string[], "Pre validation"); 2879 alias parse(T) = ParseFunc!(Parse, T); 2880 alias validation(T) = ValidateFunc!(Validation, ParseType!T); 2881 alias action(T) = ActionFunc!(Action, T, ParseType!T); 2882 alias noValueAction(T) = NoValueActionFunc!(NoValueAction, T); 2883 2884 if(rawParam.value.length == 0) 2885 { 2886 return noValueAction!T(receiver, Param!void(rawParam.config, rawParam.name)); 2887 } 2888 else 2889 { 2890 static if(!is(PreProcess == void)) 2891 PreProcess(rawParam); 2892 2893 if(!preValidation(rawParam)) 2894 return false; 2895 2896 auto parsedParam = Param!(ParseType!T)(rawParam.config, rawParam.name); 2897 2898 if(!parse!T(parsedParam.value, rawParam)) 2899 return false; 2900 2901 if(!validation!T(parsedParam)) 2902 return false; 2903 2904 if(!action!T(receiver, parsedParam)) 2905 return false; 2906 2907 return true; 2908 } 2909 } 2910 } 2911 2912 2913 private template DefaultValueParseFunctions(T) 2914 if(!is(T == void)) 2915 { 2916 import std.traits; 2917 import std.conv: to; 2918 2919 static if(is(T == enum)) 2920 { 2921 alias DefaultValueParseFunctions = ValueParseFunctions!( 2922 void, // pre process 2923 Validators.ValueInList!(EnumMembersAsStrings!T, typeof(RawParam.value)), // pre validate 2924 void, // parse 2925 void, // validate 2926 void, // action 2927 void // no-value action 2928 ); 2929 } 2930 else static if(isSomeString!T || isNumeric!T) 2931 { 2932 alias DefaultValueParseFunctions = ValueParseFunctions!( 2933 void, // pre process 2934 void, // pre validate 2935 void, // parse 2936 void, // validate 2937 void, // action 2938 void // no-value action 2939 ); 2940 } 2941 else static if(isBoolean!T) 2942 { 2943 alias DefaultValueParseFunctions = ValueParseFunctions!( 2944 void, // pre process 2945 void, // pre validate 2946 (string value) // parse 2947 { 2948 switch(value) 2949 { 2950 case "": goto case; 2951 case "yes": goto case; 2952 case "y": return true; 2953 case "no": goto case; 2954 case "n": return false; 2955 default: return value.to!T; 2956 } 2957 }, 2958 void, // validate 2959 void, // action 2960 (ref T result) { result = true; } // no-value action 2961 ); 2962 } 2963 else static if(isSomeChar!T) 2964 { 2965 alias DefaultValueParseFunctions = ValueParseFunctions!( 2966 void, // pre process 2967 void, // pre validate 2968 (string value) // parse 2969 { 2970 return value.length > 0 ? value[0].to!T : T.init; 2971 }, 2972 void, // validate 2973 void, // action 2974 void // no-value action 2975 ); 2976 } 2977 else static if(isArray!T) 2978 { 2979 import std.traits: ForeachType; 2980 2981 alias TElement = ForeachType!T; 2982 2983 static if(!isArray!TElement || isSomeString!TElement) // 1D array 2984 { 2985 static if(!isStaticArray!T) 2986 alias action = Actions.Append!T; 2987 else 2988 alias action = Actions.Assign!T; 2989 2990 alias DefaultValueParseFunctions = DefaultValueParseFunctions!TElement 2991 .changePreProcess!splitValues 2992 .changeParse!((ref T receiver, RawParam param) 2993 { 2994 static if(!isStaticArray!T) 2995 { 2996 if(receiver.length < param.value.length) 2997 receiver.length = param.value.length; 2998 } 2999 3000 foreach(i, value; param.value) 3001 { 3002 if(!DefaultValueParseFunctions!TElement.parse(receiver[i], 3003 RawParam(param.config, param.name, [value]))) 3004 return false; 3005 } 3006 3007 return true; 3008 }) 3009 .changeAction!(action) 3010 .changeNoValueAction!((ref T param) {}); 3011 } 3012 else static if(!isArray!(ForeachType!TElement) || isSomeString!(ForeachType!TElement)) // 2D array 3013 { 3014 alias DefaultValueParseFunctions = DefaultValueParseFunctions!TElement 3015 .changeAction!(Actions.Extend!TElement) 3016 .changeNoValueAction!((ref T param) { param ~= TElement.init; }); 3017 } 3018 else 3019 { 3020 static assert(false, "Multi-dimentional arrays are not supported: " ~ T.stringof); 3021 } 3022 } 3023 else static if(isAssociativeArray!T) 3024 { 3025 import std.string : indexOf; 3026 alias DefaultValueParseFunctions = ValueParseFunctions!( 3027 splitValues, // pre process 3028 void, // pre validate 3029 Parsers.PassThrough, // parse 3030 void, // validate 3031 (ref T recepient, Param!(string[]) param) // action 3032 { 3033 alias K = KeyType!T; 3034 alias V = ValueType!T; 3035 3036 foreach(input; param.value) 3037 { 3038 auto j = indexOf(input, param.config.assignChar); 3039 if(j < 0) 3040 return false; 3041 3042 K key; 3043 if(!DefaultValueParseFunctions!K.parse(key, RawParam(param.config, param.name, [input[0 .. j]]))) 3044 return false; 3045 3046 V value; 3047 if(!DefaultValueParseFunctions!V.parse(value, RawParam(param.config, param.name, [input[j + 1 .. $]]))) 3048 return false; 3049 3050 recepient[key] = value; 3051 } 3052 return true; 3053 }, 3054 (ref T param) {} // no-value action 3055 ); 3056 } 3057 else static if(is(T == delegate)) 3058 { 3059 alias DefaultValueParseFunctions = ValueParseFunctions!( 3060 void, // pre process 3061 void, // pre validate 3062 Parsers.PassThrough, // parse 3063 void, // validate 3064 Actions.CallFunction!T, // action 3065 Actions.CallFunctionNoParam!T // no-value action 3066 ); 3067 } 3068 else 3069 static assert(false, "Type is not supported: " ~ T.stringof); 3070 } 3071 3072 unittest 3073 { 3074 enum MyEnum { foo, bar, } 3075 3076 import std.meta: AliasSeq; 3077 static foreach(T; AliasSeq!(string, bool, int, double, char, MyEnum)) 3078 static foreach(R; AliasSeq!(T, T[], T[][])) 3079 {{ 3080 // ensure that this compiles 3081 R receiver; 3082 RawParam param; 3083 param.value = [""]; 3084 DefaultValueParseFunctions!R.parse(receiver, param); 3085 }} 3086 } 3087 3088 unittest 3089 { 3090 alias test(R) = (string[][] values) 3091 { 3092 auto config = Config('=', ','); 3093 R receiver; 3094 foreach(value; values) 3095 { 3096 assert(DefaultValueParseFunctions!R.parse(receiver, RawParam(config, "", value))); 3097 } 3098 return receiver; 3099 }; 3100 3101 static assert(test!(string[])([["1","2","3"], [], ["4"]]) == ["1","2","3","4"]); 3102 static assert(test!(string[][])([["1","2","3"], [], ["4"]]) == [["1","2","3"],[],["4"]]); 3103 3104 static assert(test!(string[string])([["a=bar","b=foo"], [], ["b=baz","c=boo"]]) == ["a":"bar", "b":"baz", "c":"boo"]); 3105 3106 static assert(test!(string[])([["1,2,3"], [], ["4"]]) == ["1","2","3","4"]); 3107 static assert(test!(string[string])([["a=bar,b=foo"], [], ["b=baz,c=boo"]]) == ["a":"bar", "b":"baz", "c":"boo"]); 3108 3109 static assert(test!(int[])([["1","2","3"], [], ["4"]]) == [1,2,3,4]); 3110 static assert(test!(int[][])([["1","2","3"], [], ["4"]]) == [[1,2,3],[],[4]]); 3111 3112 } 3113 3114 unittest 3115 { 3116 import std.math: isNaN; 3117 enum MyEnum { foo, bar, } 3118 3119 alias test(T) = (string[] values) 3120 { 3121 T receiver; 3122 RawParam param; 3123 param.value = values; 3124 assert(DefaultValueParseFunctions!T.parse(receiver, param)); 3125 return receiver; 3126 }; 3127 3128 static assert(test!string([""]) == ""); 3129 static assert(test!string(["foo"]) == "foo"); 3130 static assert(isNaN(test!double([""]))); 3131 static assert(test!double(["-12.34"]) == -12.34); 3132 static assert(test!double(["12.34"]) == 12.34); 3133 static assert(test!uint(["1234"]) == 1234); 3134 static assert(test!int([""]) == int.init); 3135 static assert(test!int(["-1234"]) == -1234); 3136 static assert(test!char([""]) == char.init); 3137 static assert(test!char(["f"]) == 'f'); 3138 static assert(test!bool([]) == true); 3139 static assert(test!bool([""]) == true); 3140 static assert(test!bool(["yes"]) == true); 3141 static assert(test!bool(["y"]) == true); 3142 static assert(test!bool(["true"]) == true); 3143 static assert(test!bool(["no"]) == false); 3144 static assert(test!bool(["n"]) == false); 3145 static assert(test!bool(["false"]) == false); 3146 static assert(test!MyEnum(["foo"]) == MyEnum.foo); 3147 static assert(test!MyEnum(["bar"]) == MyEnum.bar); 3148 static assert(test!(MyEnum[])(["bar","foo"]) == [MyEnum.bar, MyEnum.foo]); 3149 static assert(test!(string[string])(["a=bar","b=foo"]) == ["a":"bar", "b":"foo"]); 3150 static assert(test!(MyEnum[string])(["a=bar","b=foo"]) == ["a":MyEnum.bar, "b":MyEnum.foo]); 3151 static assert(test!(int[MyEnum])(["bar=3","foo=5"]) == [MyEnum.bar:3, MyEnum.foo:5]); 3152 } 3153 3154 3155 private struct ArgumentInfo 3156 { 3157 string[] names; 3158 3159 string description; 3160 string placeholder; 3161 3162 private void setAllowedValues(alias names)() 3163 { 3164 if(placeholder.length == 0) 3165 { 3166 import std.conv: to; 3167 import std.array: join; 3168 import std.format: format; 3169 placeholder = "{%s}".format(names.to!(string[]).join(',')); 3170 } 3171 } 3172 3173 bool hideFromHelp = false; // if true then this argument is not printed on help page 3174 3175 bool required; 3176 3177 Nullable!uint position; 3178 3179 @property bool positional() const { return !position.isNull; } 3180 3181 Nullable!ulong minValuesCount; 3182 Nullable!ulong maxValuesCount; 3183 3184 private auto checkValuesCount(string argName, ulong count) const 3185 { 3186 immutable min = minValuesCount.get; 3187 immutable max = maxValuesCount.get; 3188 3189 // override for boolean flags 3190 if(allowBooleanNegation && count == 1) 3191 return Result.Success; 3192 3193 if(min == max && count != min) 3194 { 3195 return Result.Error("argument ",argName,": expected ",min,min == 1 ? " value" : " values"); 3196 } 3197 if(count < min) 3198 { 3199 return Result.Error("argument ",argName,": expected at least ",min,min == 1 ? " value" : " values"); 3200 } 3201 if(count > max) 3202 { 3203 return Result.Error("argument ",argName,": expected at most ",max,max == 1 ? " value" : " values"); 3204 } 3205 3206 return Result.Success; 3207 } 3208 3209 private bool allowBooleanNegation = true; 3210 private bool ignoreInDefaultCommand; 3211 } 3212 3213 unittest 3214 { 3215 ArgumentInfo info; 3216 info.allowBooleanNegation = false; 3217 info.minValuesCount = 2; 3218 info.maxValuesCount = 4; 3219 3220 alias isError = (Result res) => !res && res.errorMsg.length > 0; 3221 3222 assert( isError(info.checkValuesCount("", 1))); 3223 assert(!isError(info.checkValuesCount("", 2))); 3224 assert(!isError(info.checkValuesCount("", 3))); 3225 assert(!isError(info.checkValuesCount("", 4))); 3226 assert( isError(info.checkValuesCount("", 5))); 3227 } 3228 3229 unittest 3230 { 3231 ArgumentInfo info; 3232 info.allowBooleanNegation = false; 3233 info.minValuesCount = 2; 3234 info.maxValuesCount = 2; 3235 3236 alias isError = (Result res) => !res && res.errorMsg.length > 0; 3237 3238 assert( isError(info.checkValuesCount("", 1))); 3239 assert(!isError(info.checkValuesCount("", 2))); 3240 assert( isError(info.checkValuesCount("", 3))); 3241 } 3242 3243 3244 //////////////////////////////////////////////////////////////////////////////////////////////////// 3245 // User defined attributes 3246 //////////////////////////////////////////////////////////////////////////////////////////////////// 3247 private struct ArgumentUDA(alias ValueParseFunctions) 3248 { 3249 ArgumentInfo info; 3250 3251 alias parsingFunc = ValueParseFunctions; 3252 3253 3254 3255 auto ref Description(string text) 3256 { 3257 info.description = text; 3258 return this; 3259 } 3260 3261 auto ref HideFromHelp(bool hide = true) 3262 { 3263 info.hideFromHelp = hide; 3264 return this; 3265 } 3266 3267 auto ref Placeholder(string value) 3268 { 3269 info.placeholder = value; 3270 return this; 3271 } 3272 3273 auto ref Required() 3274 { 3275 info.required = true; 3276 return this; 3277 } 3278 3279 auto ref Optional() 3280 { 3281 info.required = false; 3282 return this; 3283 } 3284 3285 auto ref NumberOfValues(ulong num) 3286 { 3287 info.minValuesCount = num; 3288 info.maxValuesCount = num; 3289 return this; 3290 } 3291 3292 auto ref NumberOfValues(ulong min, ulong max) 3293 { 3294 info.minValuesCount = min; 3295 info.maxValuesCount = max; 3296 return this; 3297 } 3298 3299 auto ref MinNumberOfValues(ulong min) 3300 { 3301 assert(min <= info.maxValuesCount.get(ulong.max)); 3302 3303 info.minValuesCount = min; 3304 return this; 3305 } 3306 3307 auto ref MaxNumberOfValues(ulong max) 3308 { 3309 assert(max >= info.minValuesCount.get(0)); 3310 3311 info.maxValuesCount = max; 3312 return this; 3313 } 3314 3315 // ReverseSwitch 3316 } 3317 3318 unittest 3319 { 3320 auto arg = NamedArgument.Description("desc").Placeholder("text"); 3321 assert(arg.info.description == "desc"); 3322 assert(arg.info.placeholder == "text"); 3323 assert(!arg.info.hideFromHelp); 3324 assert(!arg.info.required); 3325 assert(arg.info.minValuesCount.isNull); 3326 assert(arg.info.maxValuesCount.isNull); 3327 3328 arg = arg.HideFromHelp().Required().NumberOfValues(10); 3329 assert(arg.info.hideFromHelp); 3330 assert(arg.info.required); 3331 assert(arg.info.minValuesCount.get == 10); 3332 assert(arg.info.maxValuesCount.get == 10); 3333 3334 arg = arg.Optional().NumberOfValues(20,30); 3335 assert(!arg.info.required); 3336 assert(arg.info.minValuesCount.get == 20); 3337 assert(arg.info.maxValuesCount.get == 30); 3338 3339 arg = arg.MinNumberOfValues(2).MaxNumberOfValues(3); 3340 assert(arg.info.minValuesCount.get == 2); 3341 assert(arg.info.maxValuesCount.get == 3); 3342 } 3343 3344 unittest 3345 { 3346 struct T 3347 { 3348 @(NamedArgument.NumberOfValues(1,3)) 3349 int[] a; 3350 @(NamedArgument.NumberOfValues(2)) 3351 int[] b; 3352 } 3353 3354 assert(CLI!T.parseArgs!((T t) { assert(t == T([1,2,3],[4,5])); })(["-a","1","2","3","-b","4","5"]) == 0); 3355 assert(CLI!T.parseArgs!((T t) { assert(t == T([1],[4,5])); })(["-a","1","-b","4","5"]) == 0); 3356 assert(["-a","1","2","3","-b","4","5"].parseCLIArgs!T.get == T([1,2,3],[4,5])); 3357 assert(["-a","1","-b","4","5"].parseCLIArgs!T.get == T([1],[4,5])); 3358 } 3359 3360 private enum bool isArgumentUDA(T) = (is(typeof(T.info) == ArgumentInfo) && is(T.parsingFunc)); 3361 3362 3363 auto PreValidation(alias func, ARG)(ARG arg) 3364 if(isArgumentUDA!ARG) 3365 { 3366 return ArgumentUDA!(arg.parsingFunc.changePreValidation!func)(arg.tupleof); 3367 } 3368 3369 auto Parse(alias func, ARG)(ARG arg) 3370 if(isArgumentUDA!ARG) 3371 { 3372 return ArgumentUDA!(arg.parsingFunc.changeParse!func)(arg.tupleof); 3373 } 3374 3375 auto Validation(alias func, ARG)(ARG arg) 3376 if(isArgumentUDA!ARG) 3377 { 3378 return ArgumentUDA!(arg.parsingFunc.changeValidation!func)(arg.tupleof); 3379 } 3380 3381 auto Action(alias func, ARG)(ARG arg) 3382 if(isArgumentUDA!ARG) 3383 { 3384 return ArgumentUDA!(arg.parsingFunc.changeAction!func)(arg.tupleof); 3385 } 3386 3387 auto AllowNoValue(alias valueToUse, ARG)(ARG arg) 3388 if(isArgumentUDA!ARG) 3389 { 3390 auto desc = ArgumentUDA!(arg.parsingFunc.changeNoValueAction!(() { return valueToUse; }))(arg.tupleof); 3391 desc.info.minValuesCount = 0; 3392 return desc; 3393 } 3394 3395 auto RequireNoValue(alias valueToUse, ARG)(ARG arg) 3396 if(isArgumentUDA!ARG) 3397 { 3398 auto desc = arg.AllowNoValue!valueToUse; 3399 desc.info.minValuesCount = 0; 3400 desc.info.maxValuesCount = 0; 3401 return desc; 3402 } 3403 3404 auto Counter(ARG)(ARG arg) 3405 if(isArgumentUDA!ARG) 3406 { 3407 struct CounterParsingFunction 3408 { 3409 static bool parse(T)(ref T receiver, const ref RawParam param) 3410 { 3411 assert(param.value.length == 0); 3412 3413 ++receiver; 3414 3415 return true; 3416 } 3417 } 3418 3419 auto desc = ArgumentUDA!(CounterParsingFunction)(arg.tupleof); 3420 desc.info.minValuesCount = 0; 3421 desc.info.maxValuesCount = 0; 3422 return desc; 3423 } 3424 3425 3426 unittest 3427 { 3428 struct T 3429 { 3430 @(NamedArgument.Counter()) int a; 3431 } 3432 3433 assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); })(["-a","-a","-a"]) == 0); 3434 static assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); 3435 assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); 3436 } 3437 3438 3439 auto AllowedValues(alias values, ARG)(ARG arg) 3440 { 3441 import std.array : assocArray; 3442 import std.range : repeat; 3443 3444 enum valuesAA = assocArray(values, false.repeat); 3445 3446 auto desc = arg.Validation!(Validators.ValueInList!(values, KeyType!(typeof(valuesAA)))); 3447 desc.info.setAllowedValues!values; 3448 return desc; 3449 } 3450 3451 3452 unittest 3453 { 3454 struct T 3455 { 3456 @(NamedArgument.AllowedValues!([1,3,5])) int a; 3457 } 3458 3459 static assert(["-a","3"].parseCLIArgs!T.get == T(3)); 3460 assert(["-a","2"].parseCLIArgs!T.isNull); 3461 assert(["-a","3"].parseCLIArgs!T.get == T(3)); 3462 } 3463 3464 unittest 3465 { 3466 struct T 3467 { 3468 @(NamedArgument.AllowedValues!(["apple","pear","banana"])) 3469 string fruit; 3470 } 3471 3472 assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); })(["--fruit", "apple"]) == 0); 3473 assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed 3474 static assert(["--fruit", "apple"].parseCLIArgs!T.get == T("apple")); 3475 assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); 3476 } 3477 3478 unittest 3479 { 3480 enum Fruit { apple, pear, banana } 3481 struct T 3482 { 3483 @NamedArgument 3484 Fruit fruit; 3485 } 3486 3487 static assert(["--fruit", "apple"].parseCLIArgs!T.get == T(Fruit.apple)); 3488 assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); 3489 } 3490 3491 3492 auto PositionalArgument(uint pos) 3493 { 3494 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo()).Required(); 3495 arg.info.position = pos; 3496 return arg; 3497 } 3498 3499 auto PositionalArgument(uint pos, string name) 3500 { 3501 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Required(); 3502 arg.info.position = pos; 3503 return arg; 3504 } 3505 3506 auto NamedArgument(string[] name...) 3507 { 3508 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo(name)).Optional(); 3509 } 3510 3511 auto NamedArgument(string name) 3512 { 3513 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Optional(); 3514 } 3515 3516 struct TrailingArguments {} 3517 3518 3519 unittest 3520 { 3521 struct T 3522 { 3523 string a; 3524 string b; 3525 3526 @TrailingArguments string[] args; 3527 } 3528 3529 assert(CLI!T.parseArgs!((T t) { assert(t == T("A","",["-b","B"])); })(["-a","A","--","-b","B"]) == 0); 3530 static assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); 3531 assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); 3532 } 3533 3534 unittest 3535 { 3536 struct T 3537 { 3538 @NamedArgument int i; 3539 @NamedArgument(["u","u1"]) uint u; 3540 @NamedArgument("d","d1") double d; 3541 } 3542 3543 assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u","8","-d","12.345"]) == 0); 3544 assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); })(["-i","-5","-u1","8","-d1","12.345"]) == 0); 3545 3546 static assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 3547 static assert(["-i","-5","-u1","8","-d1","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 3548 assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 3549 assert(["-i","-5","-u1","8","-d1","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 3550 } 3551 3552 unittest 3553 { 3554 struct T 3555 { 3556 @NamedArgument int[] a; 3557 @NamedArgument int[][] b; 3558 } 3559 3560 assert(CLI!T.parseArgs!((T t) { assert(t.a == [1,2,3,4,5]); })(["-a","1","2","3","-a","4","5"]) == 0); 3561 assert(CLI!T.parseArgs!((T t) { assert(t.b == [[1,2,3],[4,5]]); })(["-b","1","2","3","-b","4","5"]) == 0); 3562 3563 static assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); 3564 static assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); 3565 assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); 3566 assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); 3567 } 3568 3569 unittest 3570 { 3571 struct T 3572 { 3573 @NamedArgument int[] a; 3574 } 3575 3576 enum cfg = { 3577 Config cfg; 3578 cfg.arraySep = ','; 3579 return cfg; 3580 }(); 3581 3582 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T([1,2,3,4,5])); })(["-a","1,2,3","-a","4","5"]) == 0); 3583 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T([1,2,3,4,5])); 3584 } 3585 3586 unittest 3587 { 3588 struct T 3589 { 3590 @NamedArgument int[string] a; 3591 } 3592 3593 assert(CLI!T.parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3","-a","boo=7"]) == 0); 3594 static assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); 3595 assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); 3596 } 3597 3598 unittest 3599 { 3600 struct T 3601 { 3602 @NamedArgument int[string] a; 3603 } 3604 3605 enum cfg = { 3606 Config cfg; 3607 cfg.arraySep = ','; 3608 return cfg; 3609 }(); 3610 3611 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a=foo=3,boo=7"]) == 0); 3612 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); })(["-a","foo=3,boo=7"]) == 0); 3613 assert(["-a=foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); 3614 assert(["-a","foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); 3615 } 3616 3617 unittest 3618 { 3619 struct T 3620 { 3621 enum Fruit { apple, pear }; 3622 3623 @NamedArgument Fruit a; 3624 } 3625 3626 assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.apple)); })(["-a","apple"]) == 0); 3627 assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.pear)); })(["-a=pear"]) == 0); 3628 static assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); 3629 static assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); 3630 assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); 3631 assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); 3632 } 3633 3634 unittest 3635 { 3636 struct T 3637 { 3638 @NamedArgument string[] a; 3639 } 3640 3641 assert(CLI!T.parseArgs!((T t) { assert(t == T(["1,2,3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); 3642 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T.get == T(["1,2,3","4","5"])); 3643 3644 enum cfg = { 3645 Config cfg; 3646 cfg.arraySep = ','; 3647 return cfg; 3648 }(); 3649 3650 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["1","2","3","4","5"])); })(["-a","1,2,3","-a","4","5"]) == 0); 3651 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T(["1","2","3","4","5"])); 3652 } 3653 3654 unittest 3655 { 3656 struct T 3657 { 3658 @(NamedArgument.AllowNoValue !10) int a; 3659 @(NamedArgument.RequireNoValue!20) int b; 3660 } 3661 3662 assert(CLI!T.parseArgs!((T t) { assert(t.a == 10); })(["-a"]) == 0); 3663 assert(CLI!T.parseArgs!((T t) { assert(t.b == 20); })(["-b"]) == 0); 3664 assert(CLI!T.parseArgs!((T t) { assert(t.a == 30); })(["-a","30"]) == 0); 3665 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","30"]) != 0); 3666 static assert(["-a"].parseCLIArgs!T.get.a == 10); 3667 static assert(["-b"].parseCLIArgs!T.get.b == 20); 3668 static assert(["-a", "30"].parseCLIArgs!T.get.a == 30); 3669 assert(["-a"].parseCLIArgs!T.get.a == 10); 3670 assert(["-b"].parseCLIArgs!T.get.b == 20); 3671 assert(["-a", "30"].parseCLIArgs!T.get.a == 30); 3672 assert(["-b", "30"].parseCLIArgs!T.isNull); 3673 } 3674 3675 unittest 3676 { 3677 struct T 3678 { 3679 @(NamedArgument 3680 .PreValidation!((string s) { return s.length > 1 && s[0] == '!'; }) 3681 .Parse !((string s) { return s[1]; }) 3682 .Validation !((char v) { return v >= '0' && v <= '9'; }) 3683 .Action !((ref int a, char v) { a = v - '0'; }) 3684 ) 3685 int a; 3686 } 3687 3688 assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","!4"]) == 0); 3689 static assert(["-a","!4"].parseCLIArgs!T.get.a == 4); 3690 assert(["-a","!4"].parseCLIArgs!T.get.a == 4); 3691 } 3692 3693 unittest 3694 { 3695 static struct T 3696 { 3697 int a; 3698 3699 @(NamedArgument("a")) void foo() { a++; } 3700 } 3701 3702 assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); })(["-a","-a","-a","-a"]) == 0); 3703 static assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); 3704 assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); 3705 } 3706 3707 3708 private string getProgramName() 3709 { 3710 import core.runtime: Runtime; 3711 import std.path: baseName; 3712 return Runtime.args[0].baseName; 3713 } 3714 3715 unittest 3716 { 3717 assert(getProgramName().length > 0); 3718 } 3719 3720 3721 private struct CommandInfo 3722 { 3723 string[] names = [""]; 3724 string usage; 3725 string description; 3726 string shortDescription; 3727 string epilog; 3728 3729 auto ref Usage(string text) 3730 { 3731 usage = text; 3732 return this; 3733 } 3734 3735 auto ref Description(string text) 3736 { 3737 description = text; 3738 return this; 3739 } 3740 3741 auto ref ShortDescription(string text) 3742 { 3743 shortDescription = text; 3744 return this; 3745 } 3746 3747 auto ref Epilog(string text) 3748 { 3749 epilog = text; 3750 return this; 3751 } 3752 } 3753 3754 auto Command(string[] name...) 3755 { 3756 return CommandInfo(name.dup); 3757 } 3758 3759 unittest 3760 { 3761 auto a = Command("MYPROG").Usage("usg").Description("desc").ShortDescription("sum").Epilog("epi"); 3762 assert(a.names == ["MYPROG"]); 3763 assert(a.usage == "usg"); 3764 assert(a.description == "desc"); 3765 assert(a.shortDescription == "sum"); 3766 assert(a.epilog == "epi"); 3767 } 3768 3769 private mixin template ForwardMemberFunction(string dest) 3770 { 3771 import std.array: split; 3772 mixin("auto "~dest.split('.')[$-1]~"(Args...)(auto ref Args args) inout { import core.lifetime: forward; return "~dest~"(forward!args); }"); 3773 } 3774 3775 3776 private struct CommandArguments(RECEIVER) 3777 { 3778 static assert(getSymbolsByUDA!(RECEIVER, TrailingArguments).length <= 1, 3779 "Type "~RECEIVER.stringof~" must have at most one 'TrailingArguments' UDA"); 3780 3781 private enum _validate = checkArgumentNames!RECEIVER && 3782 checkPositionalIndexes!RECEIVER; 3783 3784 static assert(getUDAs!(RECEIVER, CommandInfo).length <= 1); 3785 3786 CommandInfo info; 3787 const(string)[] parentNames; 3788 3789 Arguments arguments; 3790 3791 ParseFunction!RECEIVER[] parseArguments; 3792 ParseFunction!RECEIVER[] completeArguments; 3793 3794 uint level; // (sub-)command level, 0 = top level 3795 3796 // sub commands 3797 size_t[string] subCommandsByName; 3798 CommandInfo[] subCommands; 3799 ParseSubCommandFunction!RECEIVER[] parseSubCommands; 3800 ParseSubCommandFunction!RECEIVER[] completeSubCommands; 3801 InitSubCommandFunction !RECEIVER[] initSubCommands; 3802 3803 // completion 3804 string[] completeSuggestion; 3805 3806 3807 mixin ForwardMemberFunction!"arguments.findPositionalArgument"; 3808 mixin ForwardMemberFunction!"arguments.findNamedArgument"; 3809 mixin ForwardMemberFunction!"arguments.checkRestrictions"; 3810 3811 3812 3813 private this(in Config config) 3814 { 3815 static if(getUDAs!(RECEIVER, CommandInfo).length > 0) 3816 CommandInfo info = getUDAs!(RECEIVER, CommandInfo)[0]; 3817 else 3818 CommandInfo info; 3819 3820 this(config, info); 3821 } 3822 3823 private this(PARENT = void)(in Config config, CommandInfo info, const PARENT* parentArguments = null) 3824 { 3825 this.info = info; 3826 3827 checkArgumentName!RECEIVER(config.namedArgChar); 3828 3829 static if(is(PARENT == void)) 3830 { 3831 level = 0; 3832 arguments = Arguments(config.caseSensitive); 3833 } 3834 else 3835 { 3836 parentNames = parentArguments.parentNames ~ parentArguments.info.names[0]; 3837 level = parentArguments.level + 1; 3838 arguments = Arguments(config.caseSensitive, &parentArguments.arguments); 3839 } 3840 3841 fillArguments(); 3842 3843 if(config.addHelp) 3844 { 3845 arguments.addArgument!helpArgument; 3846 parseArguments ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) 3847 { 3848 import std.stdio: stdout; 3849 3850 printHelp(stdout.lockingTextWriter(), this, config); 3851 3852 return Result(0); 3853 }; 3854 completeArguments ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) 3855 { 3856 return Result.Success; 3857 }; 3858 } 3859 3860 3861 import std.algorithm: sort, map; 3862 import std.range: join; 3863 import std.array: array; 3864 3865 completeSuggestion = arguments.argsNamed.keys.map!(_ => getArgumentName(_, config)).array; 3866 completeSuggestion ~= subCommandsByName.keys.array; 3867 completeSuggestion.sort; 3868 } 3869 3870 private void fillArguments() 3871 { 3872 enum hasNoUDAs = getSymbolsByUDA!(RECEIVER, ArgumentUDA ).length == 0 && 3873 getSymbolsByUDA!(RECEIVER, NamedArgument).length == 0 && 3874 getSymbolsByUDA!(RECEIVER, SubCommands ).length == 0; 3875 3876 static foreach(sym; __traits(allMembers, RECEIVER)) 3877 {{ 3878 alias mem = __traits(getMember, RECEIVER, sym); 3879 3880 static if(!is(mem)) // skip types 3881 { 3882 static if(hasUDA!(mem, ArgumentUDA) || hasUDA!(mem, NamedArgument)) 3883 addArgument!sym; 3884 else static if(hasUDA!(mem, SubCommands)) 3885 addSubCommands!sym; 3886 else static if(hasNoUDAs && 3887 // skip "op*" functions 3888 !(is(typeof(mem) == function) && sym.length > 2 && sym[0..2] == "op")) 3889 { 3890 import std.sumtype: isSumType; 3891 3892 static if(isSumType!(typeof(mem))) 3893 addSubCommands!sym; 3894 else 3895 addArgument!sym; 3896 } 3897 } 3898 }} 3899 } 3900 3901 private void addArgument(alias symbol)() 3902 { 3903 alias member = __traits(getMember, RECEIVER, symbol); 3904 3905 static assert(getUDAs!(member, ArgumentUDA).length <= 1, 3906 "Member "~RECEIVER.stringof~"."~symbol~" has multiple '*Argument' UDAs"); 3907 3908 static assert(getUDAs!(member, Group).length <= 1, 3909 "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'Group' UDAs"); 3910 3911 static if(getUDAs!(member, ArgumentUDA).length > 0) 3912 enum uda = getUDAs!(member, ArgumentUDA)[0]; 3913 else 3914 enum uda = NamedArgument(); 3915 3916 enum info = setDefaults!(uda.info, typeof(member), symbol); 3917 3918 enum restrictions = { 3919 RestrictionGroup[] restrictions; 3920 static foreach(gr; getUDAs!(member, RestrictionGroup)) 3921 restrictions ~= gr; 3922 return restrictions; 3923 }(); 3924 3925 static if(getUDAs!(member, Group).length > 0) 3926 arguments.addArgument!(info, restrictions, getUDAs!(member, Group)[0]); 3927 else 3928 arguments.addArgument!(info, restrictions); 3929 3930 parseArguments ~= ParsingArgument!(symbol, uda, info, RECEIVER, false); 3931 completeArguments ~= ParsingArgument!(symbol, uda, info, RECEIVER, true); 3932 } 3933 3934 private void addSubCommands(alias symbol)() 3935 { 3936 import std.sumtype: isSumType; 3937 3938 alias member = __traits(getMember, RECEIVER, symbol); 3939 3940 static assert(isSumType!(typeof(member)), RECEIVER.stringof~"."~symbol~" must have 'SumType' type"); 3941 3942 static assert(getUDAs!(member, SubCommands).length <= 1, 3943 "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'SubCommands' UDAs"); 3944 3945 static foreach(TYPE; typeof(member).Types) 3946 {{ 3947 enum defaultCommand = is(TYPE == Default!COMMAND_TYPE, COMMAND_TYPE); 3948 static if(!defaultCommand) 3949 alias COMMAND_TYPE = TYPE; 3950 3951 static assert(getUDAs!(COMMAND_TYPE, CommandInfo).length <= 1); 3952 3953 //static assert(getUDAs!(member, Group).length <= 1, 3954 // "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'Group' UDAs"); 3955 3956 static if(getUDAs!(COMMAND_TYPE, CommandInfo).length > 0) 3957 enum info = getUDAs!(COMMAND_TYPE, CommandInfo)[0]; 3958 else 3959 enum info = CommandInfo([COMMAND_TYPE.stringof]); 3960 3961 static assert(info.names.length > 0 && info.names[0].length > 0); 3962 3963 //static if(getUDAs!(member, Group).length > 0) 3964 // args.addArgument!(info, restrictions, getUDAs!(member, Group)[0])(ParsingArgument!(symbol, uda, info, RECEIVER)); 3965 //else 3966 //arguments.addSubCommand!(info); 3967 3968 immutable index = subCommands.length; 3969 3970 static foreach(name; info.names) 3971 { 3972 assert(!(name in subCommandsByName), "Duplicated name of subcommand: "~name); 3973 subCommandsByName[arguments.convertCase(name)] = index; 3974 } 3975 3976 static if(defaultCommand) 3977 { 3978 assert(!(DEFAULT_COMMAND in subCommandsByName), "Multiple default subcommands: "~RECEIVER.stringof~"."~symbol); 3979 subCommandsByName[DEFAULT_COMMAND] = index; 3980 } 3981 3982 subCommands ~= info; 3983 //group.arguments ~= index; 3984 parseSubCommands ~= ParsingSubCommandArgument!(TYPE, info, RECEIVER, symbol, false)(&this); 3985 completeSubCommands ~= ParsingSubCommandArgument!(TYPE, info, RECEIVER, symbol, true)(&this); 3986 initSubCommands ~= ParsingSubCommandInit!(TYPE, RECEIVER, symbol); 3987 }} 3988 } 3989 3990 auto getParseFunction(bool completionMode)(size_t index) const 3991 { 3992 static if(completionMode) 3993 return completeArguments[index]; 3994 else 3995 return parseArguments[index]; 3996 } 3997 3998 auto findSubCommand(string name) const 3999 { 4000 struct Result 4001 { 4002 uint level = uint.max; 4003 ParseSubCommandFunction!RECEIVER parse; 4004 ParseSubCommandFunction!RECEIVER complete; 4005 InitSubCommandFunction !RECEIVER initialize; 4006 } 4007 4008 auto p = arguments.convertCase(name) in subCommandsByName; 4009 return !p ? Result.init : Result(level+1, parseSubCommands[*p], completeSubCommands[*p], initSubCommands[*p]); 4010 } 4011 4012 static if(getSymbolsByUDA!(RECEIVER, TrailingArguments).length == 1) 4013 { 4014 private void setTrailingArgs(ref RECEIVER receiver, string[] rawValues) const 4015 { 4016 enum symbol = __traits(identifier, getSymbolsByUDA!(RECEIVER, TrailingArguments)[0]); 4017 auto target = &__traits(getMember, receiver, symbol); 4018 4019 static if(__traits(compiles, { *target = rawValues; })) 4020 *target = rawValues; 4021 else 4022 static assert(false, "Type '"~typeof(*target).stringof~"' of `"~ 4023 RECEIVER.stringof~"."~symbol~"` is not supported for 'TrailingArguments' UDA"); 4024 } 4025 } 4026 } 4027 4028 //////////////////////////////////////////////////////////////////////////////////////////////////// 4029 // Help-printing functions 4030 //////////////////////////////////////////////////////////////////////////////////////////////////// 4031 private void printValue(Output)(auto ref Output output, in ArgumentInfo info) 4032 { 4033 if(info.maxValuesCount.get == 0) 4034 return; 4035 4036 if(info.minValuesCount.get == 0) 4037 output.put('['); 4038 4039 output.put(info.placeholder); 4040 if(info.maxValuesCount.get > 1) 4041 output.put(" ..."); 4042 4043 if(info.minValuesCount.get == 0) 4044 output.put(']'); 4045 } 4046 4047 unittest 4048 { 4049 auto test(int min, int max) 4050 { 4051 ArgumentInfo info; 4052 info.placeholder = "v"; 4053 info.minValuesCount = min; 4054 info.maxValuesCount = max; 4055 4056 import std.array: appender; 4057 auto a = appender!string; 4058 a.printValue(info); 4059 return a[]; 4060 } 4061 4062 assert(test(0,0) == ""); 4063 assert(test(0,1) == "[v]"); 4064 assert(test(0,5) == "[v ...]"); 4065 assert(test(1,1) == "v"); 4066 assert(test(1,5) == "v ..."); 4067 assert(test(3,3) == "v ..."); 4068 assert(test(3,5) == "v ..."); 4069 } 4070 4071 4072 private string getArgumentName(string name, in Config config) 4073 { 4074 import std.conv: to; 4075 4076 immutable dash = config.namedArgChar.to!string; 4077 4078 name = dash ~ name; 4079 return name.length > 2 ? dash ~ name : name; 4080 } 4081 4082 unittest 4083 { 4084 assert(getArgumentName("f", Config.init) == "-f"); 4085 assert(getArgumentName("foo", Config.init) == "--foo"); 4086 } 4087 4088 4089 private void printInvocation(Output)(auto ref Output output, in ArgumentInfo info, in string[] names, in Config config) 4090 { 4091 if(info.positional) 4092 output.printValue(info); 4093 else 4094 { 4095 import std.algorithm: each; 4096 4097 names.each!((i, name) 4098 { 4099 if(i > 0) 4100 output.put(", "); 4101 4102 output.put(getArgumentName(name, config)); 4103 4104 if(info.maxValuesCount.get > 0) 4105 { 4106 output.put(' '); 4107 output.printValue(info); 4108 } 4109 }); 4110 } 4111 } 4112 4113 unittest 4114 { 4115 auto test(bool positional)() 4116 { 4117 enum info = { 4118 ArgumentInfo info; 4119 info.placeholder = "v"; 4120 static if (positional) 4121 info.position = 0; 4122 return info; 4123 }(); 4124 4125 import std.array: appender; 4126 auto a = appender!string; 4127 a.printInvocation(setDefaults!(info, int, "foo"), ["f","foo"], Config.init); 4128 return a[]; 4129 } 4130 4131 assert(test!false == "-f v, --foo v"); 4132 assert(test!true == "v"); 4133 } 4134 4135 4136 private void printUsage(Output)(auto ref Output output, in ArgumentInfo info, in Config config) 4137 { 4138 if(!info.required) 4139 output.put('['); 4140 4141 output.printInvocation(info, [info.names[0]], config); 4142 4143 if(!info.required) 4144 output.put(']'); 4145 } 4146 4147 unittest 4148 { 4149 auto test(bool required, bool positional)() 4150 { 4151 enum info = { 4152 ArgumentInfo info; 4153 info.names ~= "foo"; 4154 info.placeholder = "v"; 4155 info.required = required; 4156 static if (positional) 4157 info.position = 0; 4158 return info; 4159 }(); 4160 4161 import std.array: appender; 4162 auto a = appender!string; 4163 a.printUsage(setDefaults!(info, int, "foo"), Config.init); 4164 return a[]; 4165 } 4166 4167 assert(test!(false, false) == "[--foo v]"); 4168 assert(test!(false, true) == "[v]"); 4169 assert(test!(true, false) == "--foo v"); 4170 assert(test!(true, true) == "v"); 4171 } 4172 4173 4174 private void substituteProg(Output)(auto ref Output output, string text, string prog) 4175 { 4176 import std.array: replaceInto; 4177 output.replaceInto(text, "%(PROG)", prog); 4178 } 4179 4180 unittest 4181 { 4182 import std.array: appender; 4183 auto a = appender!string; 4184 a.substituteProg("this is some text where %(PROG) is substituted but PROG and prog are not", "-myprog-"); 4185 assert(a[] == "this is some text where -myprog- is substituted but PROG and prog are not"); 4186 } 4187 4188 4189 private string spaces(ulong num) 4190 { 4191 import std.range: repeat; 4192 import std.array: array; 4193 return ' '.repeat(num).array; 4194 } 4195 4196 unittest 4197 { 4198 assert(spaces(0) == ""); 4199 assert(spaces(1) == " "); 4200 assert(spaces(5) == " "); 4201 } 4202 4203 private void printUsage(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 4204 { 4205 import std.algorithm: map; 4206 import std.array: join; 4207 4208 string progName = (cmd.parentNames ~ cmd.info.names[0]).map!(_ => _.length > 0 ? _ : getProgramName()).join(" "); 4209 4210 output.put("Usage: "); 4211 4212 if(cmd.info.usage.length > 0) 4213 substituteProg(output, cmd.info.usage, progName); 4214 else 4215 { 4216 import std.algorithm: filter, each, map; 4217 4218 alias print = (r) => r 4219 .filter!((ref _) => !_.hideFromHelp) 4220 .each!((ref _) 4221 { 4222 output.put(' '); 4223 output.printUsage(_, config); 4224 }); 4225 4226 output.put(progName); 4227 4228 // named args 4229 print(cmd.arguments.arguments.filter!((ref _) => !_.positional)); 4230 // positional args 4231 print(cmd.arguments.positionalArguments.map!(ref (_) => cmd.arguments.arguments[_])); 4232 // sub commands 4233 if(cmd.subCommands.length > 0) 4234 output.put(" <command> [<args>]"); 4235 } 4236 4237 output.put('\n'); 4238 } 4239 4240 void printUsage(T, Output)(auto ref Output output, in Config config) 4241 { 4242 printUsage(output, CommandArguments!T(config), config); 4243 } 4244 4245 unittest 4246 { 4247 @(Command("MYPROG").Usage("custom usage of %(PROG)")) 4248 struct T 4249 { 4250 string s; 4251 } 4252 4253 auto test(string usage) 4254 { 4255 import std.array: appender; 4256 4257 auto a = appender!string; 4258 a.printUsage!T(Config.init); 4259 return a[]; 4260 } 4261 4262 enum expected = "Usage: custom usage of MYPROG\n"; 4263 static assert(test("custom usage of %(PROG)") == expected); 4264 assert(test("custom usage of %(PROG)") == expected); 4265 } 4266 4267 4268 private void printHelp(Output, ARGS)(auto ref Output output, in Group group, ARGS args, int helpPosition) 4269 { 4270 import std.string: leftJustify; 4271 4272 if(group.arguments.length == 0 || group.name.length == 0) 4273 return; 4274 4275 alias printDescription = { 4276 output.put(group.name); 4277 output.put(":\n"); 4278 4279 if (group.description.length > 0) 4280 { 4281 output.put(" "); 4282 output.put(group.description); 4283 output.put("\n\n"); 4284 } 4285 }; 4286 bool descriptionIsPrinted = false; 4287 4288 immutable ident = spaces(helpPosition + 2); 4289 4290 foreach(idx; group.arguments) 4291 { 4292 auto arg = &args[idx]; 4293 4294 if(arg.invocation.length == 0) 4295 continue; 4296 4297 if(!descriptionIsPrinted) 4298 { 4299 printDescription(); 4300 descriptionIsPrinted = true; 4301 } 4302 4303 if(arg.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 4304 { 4305 import std.array: appender; 4306 4307 auto invocation = appender!string; 4308 invocation ~= " "; 4309 invocation ~= arg.invocation.leftJustify(helpPosition); 4310 output.wrapMutiLine(arg.help, 80-2, invocation[], ident); 4311 } 4312 else 4313 { 4314 // long action name; start on the next line 4315 output.put(" "); 4316 output.put(arg.invocation); 4317 output.put("\n"); 4318 output.wrapMutiLine(arg.help, 80-2, ident, ident); 4319 } 4320 } 4321 4322 output.put('\n'); 4323 } 4324 4325 4326 private void printHelp(Output)(auto ref Output output, in Arguments arguments, in Config config, bool helpArgIsPrinted = false) 4327 { 4328 import std.algorithm: map, maxElement, min; 4329 import std.array: appender, array; 4330 4331 // pre-compute the output 4332 auto args = 4333 arguments.arguments 4334 .map!((ref _) 4335 { 4336 struct Result 4337 { 4338 string invocation, help; 4339 } 4340 4341 if(_.hideFromHelp) 4342 return Result.init; 4343 4344 if(isHelpArgument(_.names[0])) 4345 { 4346 if(helpArgIsPrinted) 4347 return Result.init; 4348 4349 helpArgIsPrinted = true; 4350 } 4351 4352 auto invocation = appender!string; 4353 invocation.printInvocation(_, _.names, config); 4354 4355 return Result(invocation[], _.description); 4356 }).array; 4357 4358 immutable maxInvocationWidth = args.map!(_ => _.invocation.length).maxElement; 4359 immutable helpPosition = min(maxInvocationWidth + 4, 24); 4360 4361 //user-defined groups 4362 foreach(ref group; arguments.groups[2..$]) 4363 output.printHelp(group, args, helpPosition); 4364 4365 //required args 4366 output.printHelp(arguments.requiredGroup, args, helpPosition); 4367 4368 //optionals args 4369 output.printHelp(arguments.optionalGroup, args, helpPosition); 4370 4371 if(arguments.parentArguments) 4372 output.printHelp(*arguments.parentArguments, config, helpArgIsPrinted); 4373 } 4374 4375 private void printHelp(Output)(auto ref Output output, in CommandInfo[] commands, in Config config) 4376 { 4377 import std.algorithm: map, maxElement, min; 4378 import std.array: appender, array, join; 4379 4380 if(commands.length == 0) 4381 return; 4382 4383 output.put("Available commands:\n"); 4384 4385 // pre-compute the output 4386 auto cmds = commands 4387 .map!((ref _) 4388 { 4389 struct Result 4390 { 4391 string invocation, help; 4392 } 4393 4394 //if(_.hideFromHelp) 4395 // return Result.init; 4396 4397 return Result(_.names.join(","), _.shortDescription.length > 0 ? _.shortDescription : _.description); 4398 }).array; 4399 4400 immutable maxInvocationWidth = cmds.map!(_ => _.invocation.length).maxElement; 4401 immutable helpPosition = min(maxInvocationWidth + 4, 24); 4402 4403 4404 immutable ident = spaces(helpPosition + 2); 4405 4406 foreach(const ref cmd; cmds) 4407 { 4408 if(cmd.invocation.length == 0) 4409 continue; 4410 4411 if(cmd.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 4412 { 4413 import std.array: appender; 4414 import std.string: leftJustify; 4415 4416 auto invocation = appender!string; 4417 invocation ~= " "; 4418 invocation ~= cmd.invocation.leftJustify(helpPosition); 4419 output.wrapMutiLine(cmd.help, 80-2, invocation[], ident); 4420 } 4421 else 4422 { 4423 // long action name; start on the next line 4424 output.put(" "); 4425 output.put(cmd.invocation); 4426 output.put("\n"); 4427 output.wrapMutiLine(cmd.help, 80-2, ident, ident); 4428 } 4429 } 4430 4431 output.put('\n'); 4432 } 4433 4434 4435 private void printHelp(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 4436 { 4437 printUsage(output, cmd, config); 4438 output.put('\n'); 4439 4440 if(cmd.info.description.length > 0) 4441 { 4442 output.put(cmd.info.description); 4443 output.put("\n\n"); 4444 } 4445 4446 // sub commands 4447 output.printHelp(cmd.subCommands, config); 4448 4449 output.printHelp(cmd.arguments, config); 4450 4451 if(cmd.info.epilog.length > 0) 4452 { 4453 output.put(cmd.info.epilog); 4454 output.put('\n'); 4455 } 4456 } 4457 4458 void printHelp(T, Output)(auto ref Output output, in Config config) 4459 { 4460 printHelp(output, CommandArguments!T(config), config); 4461 } 4462 4463 unittest 4464 { 4465 @(Command("MYPROG") 4466 .Description("custom description") 4467 .Epilog("custom epilog") 4468 ) 4469 struct T 4470 { 4471 @NamedArgument string s; 4472 @(NamedArgument.Placeholder("VALUE")) string p; 4473 4474 @(NamedArgument.HideFromHelp()) string hidden; 4475 4476 enum Fruit { apple, pear }; 4477 @(NamedArgument(["f","fruit"]).Required().Description("This is a help text for fruit. Very very very very very very very very very very very very very very very very very very very long text")) Fruit f; 4478 4479 @(NamedArgument.AllowedValues!([1,4,16,8])) int i; 4480 4481 @(PositionalArgument(0).Description("This is a help text for param0. Very very very very very very very very very very very very very very very very very very very long text")) string param0; 4482 @(PositionalArgument(1).AllowedValues!(["q","a"])) string param1; 4483 4484 @TrailingArguments string[] args; 4485 } 4486 4487 auto test(alias func)() 4488 { 4489 import std.array: appender; 4490 4491 auto a = appender!string; 4492 func!T(a, Config.init); 4493 return a[]; 4494 } 4495 static assert(test!printUsage.length > 0); // ensure that it works at compile time 4496 static assert(test!printHelp .length > 0); // ensure that it works at compile time 4497 4498 assert(test!printUsage == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n"); 4499 assert(test!printHelp == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~ 4500 "custom description\n\n"~ 4501 "Required arguments:\n"~ 4502 " -f {apple,pear}, --fruit {apple,pear}\n"~ 4503 " This is a help text for fruit. Very very very very\n"~ 4504 " very very very very very very very very very very\n"~ 4505 " very very very very very long text\n"~ 4506 " param0 This is a help text for param0. Very very very very\n"~ 4507 " very very very very very very very very very very\n"~ 4508 " very very very very very long text\n"~ 4509 " {q,a} \n\n"~ 4510 "Optional arguments:\n"~ 4511 " -s S \n"~ 4512 " -p VALUE \n"~ 4513 " -i {1,4,16,8} \n"~ 4514 " -h, --help Show this help message and exit\n\n"~ 4515 "custom epilog\n"); 4516 } 4517 4518 unittest 4519 { 4520 @Command("MYPROG") 4521 struct T 4522 { 4523 @(ArgumentGroup("group1").Description("group1 description")) 4524 { 4525 @NamedArgument 4526 { 4527 string a; 4528 string b; 4529 } 4530 @PositionalArgument(0) string p; 4531 } 4532 4533 @(ArgumentGroup("group2").Description("group2 description")) 4534 @NamedArgument 4535 { 4536 string c; 4537 string d; 4538 } 4539 @PositionalArgument(1) string q; 4540 } 4541 4542 auto test(alias func)() 4543 { 4544 import std.array: appender; 4545 4546 auto a = appender!string; 4547 func!T(a, Config.init); 4548 return a[]; 4549 } 4550 4551 assert(test!printHelp == "Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q\n\n"~ 4552 "group1:\n"~ 4553 " group1 description\n\n"~ 4554 " -a A \n"~ 4555 " -b B \n"~ 4556 " p \n\n"~ 4557 "group2:\n"~ 4558 " group2 description\n\n"~ 4559 " -c C \n"~ 4560 " -d D \n\n"~ 4561 "Required arguments:\n"~ 4562 " q \n\n"~ 4563 "Optional arguments:\n"~ 4564 " -h, --help Show this help message and exit\n\n"); 4565 } 4566 4567 unittest 4568 { 4569 @Command("MYPROG") 4570 struct T 4571 { 4572 @(Command("cmd1").ShortDescription("Perform cmd 1")) 4573 struct CMD1 4574 { 4575 string a; 4576 } 4577 @(Command("very-long-command-name-2").ShortDescription("Perform cmd 2")) 4578 struct CMD2 4579 { 4580 string b; 4581 } 4582 4583 string c; 4584 string d; 4585 4586 SumType!(CMD1, CMD2) cmd; 4587 } 4588 4589 auto test(alias func)() 4590 { 4591 import std.array: appender; 4592 4593 auto a = appender!string; 4594 func!T(a, Config.init); 4595 return a[]; 4596 } 4597 4598 assert(test!printHelp == "Usage: MYPROG [-c C] [-d D] [-h] <command> [<args>]\n\n"~ 4599 "Available commands:\n"~ 4600 " cmd1 Perform cmd 1\n"~ 4601 " very-long-command-name-2\n"~ 4602 " Perform cmd 2\n\n"~ 4603 "Optional arguments:\n"~ 4604 " -c C \n"~ 4605 " -d D \n"~ 4606 " -h, --help Show this help message and exit\n\n"); 4607 } 4608 4609 unittest 4610 { 4611 @Command("MYPROG") 4612 struct T 4613 { 4614 @(NamedArgument.HideFromHelp()) string s; 4615 } 4616 4617 assert(parseCLIArgs!T(["-h","-s","asd"]).isNull()); 4618 assert(parseCLIArgs!T(["-h"], (T t) { assert(false); }) == 0); 4619 4620 auto args = ["-h","-s","asd"]; 4621 assert(parseCLIKnownArgs!T(args).isNull()); 4622 assert(args.length == 3); 4623 assert(parseCLIKnownArgs!T(["-h"], (T t, string[] args) { assert(false); }) == 0); 4624 } 4625 4626 unittest 4627 { 4628 @Command("MYPROG") 4629 struct T 4630 { 4631 @(NamedArgument.Required()) string s; 4632 } 4633 4634 assert(parseCLIArgs!T([], (T t) { assert(false); }) != 0); 4635 } 4636 4637 unittest 4638 { 4639 @Command("MYPROG") 4640 struct T 4641 { 4642 @MutuallyExclusive() 4643 { 4644 string a; 4645 string b; 4646 } 4647 } 4648 4649 // Either or no argument is allowed 4650 assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); 4651 assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); 4652 assert(CLI!T.parseArgs!((T t) {})([]) == 0); 4653 4654 // Both arguments are not allowed 4655 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); 4656 4657 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0); 4658 assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0); 4659 assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0); 4660 assert(parseCLIArgs!T([], (T t) {}) == 0); 4661 } 4662 4663 unittest 4664 { 4665 @Command("MYPROG") 4666 struct T 4667 { 4668 @(MutuallyExclusive().Required()) 4669 { 4670 string a; 4671 string b; 4672 } 4673 } 4674 4675 // Either argument is allowed 4676 assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); 4677 assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); 4678 4679 // Both arguments or no argument is not allowed 4680 assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); 4681 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); 4682 4683 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0); 4684 assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0); 4685 assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0); 4686 assert(parseCLIArgs!T([], (T t) { assert(false); }) != 0); 4687 } 4688 4689 unittest 4690 { 4691 @Command("MYPROG") 4692 struct T 4693 { 4694 @RequiredTogether() 4695 { 4696 string a; 4697 string b; 4698 } 4699 } 4700 4701 // Both or no argument is allowed 4702 assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); 4703 assert(CLI!T.parseArgs!((T t) {})([]) == 0); 4704 4705 // Single argument is not allowed 4706 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); 4707 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); 4708 4709 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0); 4710 assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0); 4711 assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0); 4712 assert(parseCLIArgs!T([], (T t) {}) == 0); 4713 } 4714 4715 unittest 4716 { 4717 @Command("MYPROG") 4718 struct T 4719 { 4720 @(RequiredTogether().Required()) 4721 { 4722 string a; 4723 string b; 4724 } 4725 } 4726 4727 // Both arguments are allowed 4728 assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); 4729 4730 // Single argument or no argument is not allowed 4731 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); 4732 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); 4733 assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); 4734 4735 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0); 4736 assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0); 4737 assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0); 4738 assert(parseCLIArgs!T([], (T t) { assert(false); }) != 0); 4739 } 4740 4741 4742 private void wrapMutiLine(Output, S)(auto ref Output output, 4743 S s, 4744 in size_t columns = 80, 4745 S firstindent = null, 4746 S indent = null, 4747 in size_t tabsize = 8) 4748 if (isSomeString!S) 4749 { 4750 import std.string: wrap, lineSplitter, join; 4751 import std.algorithm: map, copy; 4752 4753 auto lines = s.lineSplitter; 4754 if(lines.empty) 4755 { 4756 output.put(firstindent); 4757 output.put("\n"); 4758 return; 4759 } 4760 4761 output.put(lines.front.wrap(columns, firstindent, indent, tabsize)); 4762 lines.popFront; 4763 4764 lines.map!(s => s.wrap(columns, indent, indent, tabsize)).copy(output); 4765 } 4766 4767 unittest 4768 { 4769 string test(string s, size_t columns, string firstindent = null, string indent = null) 4770 { 4771 import std.array: appender; 4772 auto a = appender!string; 4773 a.wrapMutiLine(s, columns, firstindent, indent); 4774 return a[]; 4775 } 4776 assert(test("a short string", 7) == "a short\nstring\n"); 4777 assert(test("a\nshort string", 7) == "a\nshort\nstring\n"); 4778 4779 // wrap will not break inside of a word, but at the next space 4780 assert(test("a short string", 4) == "a\nshort\nstring\n"); 4781 4782 assert(test("a short string", 7, "\t") == "\ta\nshort\nstring\n"); 4783 assert(test("a short string", 7, "\t", " ") == "\ta\n short\n string\n"); 4784 }