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