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 ParsingFunction(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 if(!info.checkValuesCount(config, argName, rawValues.length)) 687 return Result.Failure; 688 689 auto param = RawParam(config, argName, rawValues); 690 691 auto target = &__traits(getMember, receiver, symbol); 692 693 static if(is(typeof(target) == function) || is(typeof(target) == delegate)) 694 return uda.parsingFunc.parse(target, param) ? Result.Success : Result.Failure; 695 else 696 return uda.parsingFunc.parse(*target, param) ? Result.Success : Result.Failure; 697 } 698 catch(Exception e) 699 { 700 config.onError(argName, ": ", e.msg); 701 return Result.Failure; 702 } 703 }; 704 705 private auto ParsingSubCommand(COMMAND_TYPE, CommandInfo info, RECEIVER, alias symbol)(const CommandArguments!RECEIVER* parentArguments) 706 { 707 return delegate(ref Parser parser, const ref Parser.Argument arg, ref RECEIVER receiver) 708 { 709 import std.sumtype: match; 710 711 auto target = &__traits(getMember, receiver, symbol); 712 713 alias parse = (ref COMMAND_TYPE cmdTarget) 714 { 715 static if(!is(COMMAND_TYPE == Default!TYPE, TYPE)) 716 alias TYPE = COMMAND_TYPE; 717 718 auto command = CommandArguments!TYPE(parser.config, info, parentArguments); 719 720 return arg.match!(_ => parser.parse(command, cmdTarget, _)); 721 }; 722 723 724 static if(typeof(*target).Types.length == 1) 725 return (*target).match!parse; 726 else 727 { 728 if((*target).match!((COMMAND_TYPE t) => false, _ => true)) 729 *target = COMMAND_TYPE.init; 730 731 return (*target).match!(parse, 732 (_) 733 { 734 assert(false, "This should never happen"); 735 return Result.Failure; 736 } 737 ); 738 } 739 }; 740 } 741 742 struct SubCommands {} 743 744 // Default subcommand 745 struct Default(COMMAND) 746 { 747 COMMAND command; 748 alias command this; 749 } 750 751 unittest 752 { 753 struct T 754 { 755 @(NamedArgument) 756 int a; 757 @(NamedArgument.Optional()) 758 int b; 759 @(NamedArgument.Required()) 760 int c; 761 @(NamedArgument) 762 int d; 763 @(NamedArgument.Required()) 764 int e; 765 @(NamedArgument) 766 int f; 767 } 768 769 enum config = { 770 Config config; 771 config.addHelp = false; 772 return config; 773 }(); 774 775 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 776 777 auto a = CommandArguments!T(config); 778 assert(a.arguments.requiredGroup.arguments == [2,4]); 779 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 780 assert(a.arguments.argsPositional == []); 781 } 782 783 unittest 784 { 785 struct T 786 { 787 int a,b,c,d,e,f; 788 } 789 790 enum config = { 791 Config config; 792 config.addHelp = false; 793 return config; 794 }(); 795 796 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 797 798 auto a = CommandArguments!T(config); 799 assert(a.arguments.requiredGroup.arguments == []); 800 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 801 assert(a.arguments.argsPositional == []); 802 } 803 804 unittest 805 { 806 struct T1 807 { 808 @(NamedArgument("1")) 809 @(NamedArgument("2")) 810 int a; 811 } 812 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 813 814 struct T2 815 { 816 @(NamedArgument("1")) 817 int a; 818 @(NamedArgument("1")) 819 int b; 820 } 821 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 822 823 struct T3 824 { 825 @(PositionalArgument(0)) int a; 826 @(PositionalArgument(0)) int b; 827 } 828 static assert(!__traits(compiles, { CommandArguments!T3(Config.init); })); 829 830 struct T4 831 { 832 @(PositionalArgument(0)) int a; 833 @(PositionalArgument(2)) int b; 834 } 835 static assert(!__traits(compiles, { CommandArguments!T4(Config.init); })); 836 } 837 838 private void checkArgumentName(T)(char namedArgChar) 839 { 840 import std.exception: enforce; 841 842 static foreach(sym; getSymbolsByUDA!(T, ArgumentUDA)) 843 static foreach(name; getUDAs!(__traits(getMember, T, __traits(identifier, sym)), ArgumentUDA)[0].info.names) 844 enforce(name[0] != namedArgChar, "Name of argument should not begin with '"~namedArgChar~"': "~name); 845 } 846 847 private auto consumeValuesFromCLI(ref string[] args, in ArgumentInfo argumentInfo, in Config config) 848 { 849 import std.range: empty, front, popFront; 850 851 immutable minValuesCount = argumentInfo.minValuesCount.get; 852 immutable maxValuesCount = argumentInfo.maxValuesCount.get; 853 854 string[] values; 855 856 if(minValuesCount > 0) 857 { 858 if(minValuesCount < args.length) 859 { 860 values = args[0..minValuesCount]; 861 args = args[minValuesCount..$]; 862 } 863 else 864 { 865 values = args; 866 args = []; 867 } 868 } 869 870 while(!args.empty && 871 values.length < maxValuesCount && 872 (args.front.length == 0 || args.front[0] != config.namedArgChar)) 873 { 874 values ~= args.front; 875 args.popFront(); 876 } 877 878 return values; 879 } 880 881 882 private enum helpArgument = { 883 ArgumentInfo arg; 884 arg.names = ["h","help"]; 885 arg.description = "Show this help message and exit"; 886 arg.minValuesCount = 0; 887 arg.maxValuesCount = 0; 888 arg.allowBooleanNegation = false; 889 return arg; 890 }(); 891 892 private bool isHelpArgument(string name) 893 { 894 static foreach(n; helpArgument.names) 895 if(n == name) 896 return true; 897 898 return false; 899 } 900 901 unittest 902 { 903 assert(isHelpArgument("h")); 904 assert(isHelpArgument("help")); 905 assert(!isHelpArgument("a")); 906 assert(!isHelpArgument("help1")); 907 } 908 909 struct Result 910 { 911 int resultCode; 912 913 private enum Status { failure, success, unknownArgument }; 914 private Status status; 915 916 bool opCast(type)() const if (is(type == bool)) 917 { 918 return status == Status.success; 919 } 920 921 private static enum Failure = Result(1, Status.failure); 922 private static enum Success = Result(0, Status.success); 923 private static enum UnknownArgument = Result(0, Status.unknownArgument); 924 } 925 926 private struct Parser 927 { 928 struct Unknown {} 929 struct Positional {} 930 struct NamedShort { 931 string name; 932 string nameWithDash; 933 string value = null; // null when there is no value 934 } 935 struct NamedLong { 936 string name; 937 string nameWithDash; 938 string value = null; // null when there is no value 939 } 940 941 import std.sumtype: SumType; 942 alias Argument = SumType!(Unknown, Positional, NamedShort, NamedLong); 943 944 immutable Config config; 945 946 string[] args; 947 string[] unrecognizedArgs; 948 949 bool[size_t] idxParsedArgs; 950 size_t idxNextPositional = 0; 951 952 private alias CmdParser = Result delegate(const ref Argument); 953 954 CmdParser[] cmdStack; 955 956 Argument splitArgumentNameValue(string arg) 957 { 958 import std.string : indexOf; 959 960 if(arg.length == 0) 961 return Argument.init; 962 963 if(arg[0] != config.namedArgChar) 964 return Argument(Positional.init); 965 966 if(arg.length == 1 || arg.length == 2 && arg[1] == config.namedArgChar) 967 return Argument.init; 968 969 auto idxAssignChar = config.assignChar == char.init ? -1 : arg.indexOf(config.assignChar); 970 971 immutable string nameWithDash = idxAssignChar < 0 ? arg : arg[0 .. idxAssignChar]; 972 immutable string value = idxAssignChar < 0 ? null : arg[idxAssignChar + 1 .. $]; 973 974 return arg[1] == config.namedArgChar 975 ? Argument(NamedLong (nameWithDash[2..$], nameWithDash, value)) 976 : Argument(NamedShort(nameWithDash[1..$], nameWithDash, value)); 977 } 978 979 void parseEndOfArgs(T)(const ref CommandArguments!T cmd, ref T receiver) 980 { 981 if(config.endOfArgs.length == 0) 982 return; 983 984 foreach(i, arg; args) 985 if(arg == config.endOfArgs) 986 { 987 static if(is(typeof(cmd.setTrailingArgs))) 988 cmd.setTrailingArgs(receiver, args[i+1..$]); 989 else 990 unrecognizedArgs ~= args[i+1..$]; 991 992 args = args[0..i]; 993 } 994 } 995 996 auto parseArgument(T, PARSE)(PARSE parse, ref T receiver, string value, string nameWithDash, size_t argIndex) 997 { 998 immutable res = parse(config, nameWithDash, receiver, value, args); 999 if(!res) 1000 return res; 1001 1002 idxParsedArgs[argIndex] = true; 1003 1004 return Result.Success; 1005 } 1006 1007 auto parseSubCommand(T)(const ref CommandArguments!T cmd, ref T receiver) 1008 { 1009 import std.range: front, popFront; 1010 1011 auto found = cmd.findSubCommand(args.front); 1012 if(found.parse is null) 1013 return Result.UnknownArgument; 1014 1015 if(found.level < cmdStack.length) 1016 cmdStack.length = found.level; 1017 1018 cmdStack ~= (const ref arg) => found.parse(this, arg, receiver); 1019 1020 args.popFront(); 1021 1022 return Result.Success; 1023 } 1024 1025 auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, Unknown) 1026 { 1027 return Result.UnknownArgument; 1028 } 1029 1030 auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, Positional) 1031 { 1032 auto foundArg = cmd.findPositionalArgument(idxNextPositional); 1033 if(foundArg.arg is null) 1034 return parseSubCommand(cmd, receiver); 1035 1036 immutable res = parseArgument(cmd.parseFunctions[foundArg.index], receiver, null, foundArg.arg.names[0], foundArg.index); 1037 if(!res) 1038 return res; 1039 1040 idxNextPositional++; 1041 1042 return Result.Success; 1043 } 1044 1045 auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, NamedLong arg) 1046 { 1047 import std.algorithm : startsWith; 1048 import std.range: popFront; 1049 1050 auto foundArg = cmd.findNamedArgument(arg.name); 1051 1052 if(foundArg.arg is null && arg.name.startsWith("no-")) 1053 { 1054 foundArg = cmd.findNamedArgument(arg.name[3..$]); 1055 if(foundArg.arg is null || !foundArg.arg.allowBooleanNegation) 1056 return Result.UnknownArgument; 1057 1058 arg.value = "false"; 1059 } 1060 1061 if(foundArg.arg is null) 1062 return Result.UnknownArgument; 1063 1064 args.popFront(); 1065 return parseArgument(cmd.parseFunctions[foundArg.index], receiver, arg.value, arg.nameWithDash, foundArg.index); 1066 } 1067 1068 auto parse(T)(const ref CommandArguments!T cmd, ref T receiver, NamedShort arg) 1069 { 1070 import std.range: popFront; 1071 1072 auto foundArg = cmd.findNamedArgument(arg.name); 1073 if(foundArg.arg !is null) 1074 { 1075 args.popFront(); 1076 return parseArgument(cmd.parseFunctions[foundArg.index], receiver, arg.value, arg.nameWithDash, foundArg.index); 1077 } 1078 1079 // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments 1080 do 1081 { 1082 auto name = [arg.name[0]]; 1083 foundArg = cmd.findNamedArgument(name); 1084 if(foundArg.arg is null) 1085 return Result.UnknownArgument; 1086 1087 // In case of bundling there can be no or one argument value 1088 if(config.bundling && foundArg.arg.minValuesCount.get > 1) 1089 return Result.UnknownArgument; 1090 1091 // In case of NO bundling there MUST be one argument value 1092 if(!config.bundling && foundArg.arg.minValuesCount.get != 1) 1093 return Result.UnknownArgument; 1094 1095 string value; 1096 if(foundArg.arg.minValuesCount == 0) 1097 arg.name = arg.name[1..$]; 1098 else 1099 { 1100 // Bundling case: try to parse "-ABvalue" where "A","B" are different single-letter arguments and "value" is a value for "B" 1101 // No bundling case: try to parse "-Avalue" where "A" is a single-letter argument and "value" is its value 1102 value = arg.name[1..$]; 1103 arg.name = ""; 1104 } 1105 1106 immutable res = parseArgument(cmd.parseFunctions[foundArg.index], receiver, value, "-"~name, foundArg.index); 1107 if(!res) 1108 return res; 1109 } 1110 while(arg.name.length > 0); 1111 1112 args.popFront(); 1113 return Result.Success; 1114 } 1115 1116 auto parse(Argument arg) 1117 { 1118 import std.range: front, popFront; 1119 1120 foreach_reverse(cmdParser; cmdStack) 1121 { 1122 immutable res = cmdParser(arg); 1123 if(res.status != Result.Status.unknownArgument) 1124 return res; 1125 } 1126 1127 unrecognizedArgs ~= args.front; 1128 args.popFront(); 1129 1130 return Result.Success; 1131 } 1132 1133 auto parseAll(T)(const ref CommandArguments!T cmd, ref T receiver) 1134 { 1135 import std.range: empty, front; 1136 1137 // Process trailing args first 1138 parseEndOfArgs(cmd, receiver); 1139 1140 cmdStack ~= (const ref arg) 1141 { 1142 import std.sumtype: match; 1143 1144 return arg.match!(_ => parse(cmd, receiver, _)); 1145 }; 1146 1147 auto found = cmd.findSubCommand(DEFAULT_COMMAND); 1148 if(found.parse !is null) 1149 cmdStack ~= (const ref arg) => found.parse(this, arg, receiver); 1150 1151 while(!args.empty) 1152 { 1153 immutable res = parse(splitArgumentNameValue(args.front)); 1154 if(!res) 1155 return res; 1156 } 1157 1158 if(!cmd.checkRestrictions(idxParsedArgs, config)) 1159 return Result.Failure; 1160 1161 return Result.Success; 1162 } 1163 } 1164 1165 unittest 1166 { 1167 assert(Parser.init.splitArgumentNameValue("") == Parser.Argument(Parser.Unknown.init)); 1168 assert(Parser.init.splitArgumentNameValue("-") == Parser.Argument(Parser.Unknown.init)); 1169 assert(Parser.init.splitArgumentNameValue("--") == Parser.Argument(Parser.Unknown.init)); 1170 assert(Parser.init.splitArgumentNameValue("abc=4") == Parser.Argument(Parser.Positional.init)); 1171 assert(Parser.init.splitArgumentNameValue("-abc") == Parser.Argument(Parser.NamedShort("abc", "-abc", null))); 1172 assert(Parser.init.splitArgumentNameValue("--abc") == Parser.Argument(Parser.NamedLong("abc", "--abc", null))); 1173 assert(Parser.init.splitArgumentNameValue("-abc=fd") == Parser.Argument(Parser.NamedShort("abc", "-abc", "fd"))); 1174 assert(Parser.init.splitArgumentNameValue("--abc=fd") == Parser.Argument(Parser.NamedLong("abc", "--abc", "fd"))); 1175 assert(Parser.init.splitArgumentNameValue("-abc=") == Parser.Argument(Parser.NamedShort("abc", "-abc", ""))); 1176 assert(Parser.init.splitArgumentNameValue("--abc=") == Parser.Argument(Parser.NamedLong("abc", "--abc", ""))); 1177 assert(Parser.init.splitArgumentNameValue("-=abc") == Parser.Argument(Parser.NamedShort("", "-", "abc"))); 1178 assert(Parser.init.splitArgumentNameValue("--=abc") == Parser.Argument(Parser.NamedLong("", "--", "abc"))); 1179 } 1180 1181 1182 private Result parseCLIKnownArgs(T)(ref T receiver, 1183 string[] args, 1184 out string[] unrecognizedArgs, 1185 const ref CommandArguments!T cmd, 1186 in Config config) 1187 { 1188 auto parser = Parser(config, args); 1189 1190 immutable res = parser.parseAll(cmd, receiver); 1191 if(!res) 1192 return res; 1193 1194 unrecognizedArgs = parser.unrecognizedArgs; 1195 1196 return Result.Success; 1197 } 1198 1199 Result parseCLIKnownArgs(T)(ref T receiver, 1200 string[] args, 1201 out string[] unrecognizedArgs, 1202 in Config config = Config.init) 1203 { 1204 auto command = CommandArguments!T(config); 1205 return parseCLIKnownArgs(receiver, args, unrecognizedArgs, command, config); 1206 } 1207 1208 auto parseCLIKnownArgs(T)(ref T receiver, ref string[] args, in Config config = Config.init) 1209 { 1210 string[] unrecognizedArgs; 1211 1212 auto res = parseCLIKnownArgs(receiver, args, unrecognizedArgs, config); 1213 if(res) 1214 args = unrecognizedArgs; 1215 1216 return res; 1217 } 1218 1219 Nullable!T parseCLIKnownArgs(T)(ref string[] args, in Config config = Config.init) 1220 { 1221 import std.typecons : nullable; 1222 1223 T receiver; 1224 1225 return parseCLIKnownArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; 1226 } 1227 1228 int parseCLIKnownArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init) 1229 if(__traits(compiles, { func(T.init, args); })) 1230 { 1231 alias value = initialValue; 1232 1233 auto res = parseCLIKnownArgs(value, args, config); 1234 if(!res) 1235 return res.resultCode; 1236 1237 static if(__traits(compiles, { int a = cast(int) func(value, args); })) 1238 return cast(int) func(value, args); 1239 else 1240 { 1241 func(value, args); 1242 return 0; 1243 } 1244 } 1245 1246 1247 auto parseCLIArgs(T)(ref T receiver, string[] args, in Config config = Config.init) 1248 { 1249 string[] unrecognizedArgs; 1250 1251 auto res = parseCLIKnownArgs(receiver, args, unrecognizedArgs, config); 1252 1253 if(res && unrecognizedArgs.length > 0) 1254 { 1255 config.onError("Unrecognized arguments: ", unrecognizedArgs); 1256 return Result.Failure; 1257 } 1258 1259 return res; 1260 } 1261 1262 Nullable!T parseCLIArgs(T)(string[] args, in Config config = Config.init) 1263 { 1264 import std.typecons : nullable; 1265 1266 T receiver; 1267 1268 return parseCLIArgs(receiver, args, config) ? receiver.nullable : Nullable!T.init; 1269 } 1270 1271 int parseCLIArgs(T, FUNC)(string[] args, FUNC func, in Config config = Config.init, T initialValue = T.init) 1272 if(__traits(compiles, { func(T.init); })) 1273 { 1274 alias value = initialValue; 1275 1276 auto res = parseCLIArgs(value, args, config); 1277 if(!res) 1278 return res.resultCode; 1279 1280 static if(__traits(compiles, { int a = cast(int) func(value); })) 1281 return cast(int) func(value); 1282 else 1283 { 1284 func(value); 1285 return 0; 1286 } 1287 } 1288 1289 unittest 1290 { 1291 import std.exception; 1292 1293 struct T 1294 { 1295 @(NamedArgument("--")) 1296 int a; 1297 } 1298 static assert(!__traits(compiles, { enum p = parseCLIArgs!T([]); })); 1299 assertThrown(parseCLIArgs!T([])); 1300 } 1301 1302 unittest 1303 { 1304 1305 import std.conv; 1306 import std.traits; 1307 1308 struct params 1309 { 1310 int no_a; 1311 1312 @(PositionalArgument(0, "a") 1313 .Description("Argument 'a'") 1314 .Validation!((int a) { return a > 3;}) 1315 .PreValidation!((string s) { return s.length > 0;}) 1316 .Validation!((int a) { return a > 0;}) 1317 ) 1318 int a; 1319 1320 int no_b; 1321 1322 @(NamedArgument(["b", "boo"]).Description("Flag boo") 1323 .AllowNoValue!55 1324 ) 1325 int b; 1326 1327 int no_c; 1328 } 1329 1330 enum p = CommandArguments!params(Config.init); 1331 static assert(p.findNamedArgument("a").arg is null); 1332 static assert(p.findNamedArgument("b").arg !is null); 1333 static assert(p.findNamedArgument("boo").arg !is null); 1334 static assert(p.findPositionalArgument(0).arg !is null); 1335 static assert(p.findPositionalArgument(1).arg is null); 1336 } 1337 1338 unittest 1339 { 1340 import std.typecons : tuple; 1341 1342 struct T 1343 { 1344 string a; 1345 string b; 1346 } 1347 1348 auto test(string[] args) 1349 { 1350 return tuple(args.parseCLIKnownArgs!T.get, args); 1351 } 1352 1353 assert(test(["-a","A","--"]) == tuple(T("A"), [])); 1354 static assert(test(["-a","A","--","-b","B"]) == tuple(T("A"), ["-b","B"])); 1355 1356 { 1357 T args; 1358 1359 args.parseCLIArgs([ "-a", "A"]); 1360 args.parseCLIArgs([ "-b", "B"]); 1361 1362 assert(args == T("A","B")); 1363 } 1364 } 1365 1366 unittest 1367 { 1368 struct T 1369 { 1370 string a; 1371 } 1372 1373 { 1374 auto test_called(string[] args) 1375 { 1376 bool called; 1377 auto dg = (T t) { 1378 called = true; 1379 }; 1380 assert(args.parseCLIArgs!T(dg) == 0 || !called); 1381 return called; 1382 } 1383 1384 static assert(test_called([])); 1385 assert(test_called([])); 1386 assert(!test_called(["-g"])); 1387 } 1388 { 1389 auto test_called(string[] args) 1390 { 1391 bool called; 1392 auto dg = (T t, string[] args) { 1393 assert(args.length == 0 || args == ["-g"]); 1394 called = true; 1395 }; 1396 assert(args.parseCLIKnownArgs!T(dg) == 0); 1397 return called; 1398 } 1399 1400 assert(test_called([])); 1401 static assert(test_called(["-g"])); 1402 } 1403 } 1404 1405 unittest 1406 { 1407 struct T 1408 { 1409 string a; 1410 } 1411 1412 int my_main(T command) 1413 { 1414 // do something 1415 return 0; 1416 } 1417 1418 static assert(["-a","aa"].parseCLIArgs!T(&my_main) == 0); 1419 assert(["-a","aa"].parseCLIArgs!T(&my_main) == 0); 1420 } 1421 1422 unittest 1423 { 1424 struct T 1425 { 1426 string a; 1427 } 1428 1429 auto args = [ "-a", "A", "-c", "C" ]; 1430 1431 assert(parseCLIKnownArgs!T(args).get == T("A")); 1432 assert(args == ["-c", "C"]); 1433 } 1434 1435 unittest 1436 { 1437 1438 struct T 1439 { 1440 @NamedArgument string x; 1441 @NamedArgument string foo; 1442 @(PositionalArgument(0, "a").Optional()) string a; 1443 @(PositionalArgument(1, "b").Optional()) string[] b; 1444 } 1445 static assert(["--foo","FOO","-x","X"].parseCLIArgs!T.get == T("X", "FOO")); 1446 static assert(["--foo=FOO","-x=X"].parseCLIArgs!T.get == T("X", "FOO")); 1447 static assert(["--foo=FOO","1","-x=X"].parseCLIArgs!T.get == T("X", "FOO", "1")); 1448 static assert(["--foo=FOO","1","2","3","4"].parseCLIArgs!T.get == T(string.init, "FOO", "1",["2","3","4"])); 1449 static assert(["-xX"].parseCLIArgs!T.get == T("X")); 1450 assert(["--foo","FOO","-x","X"].parseCLIArgs!T.get == T("X", "FOO")); 1451 assert(["--foo=FOO","-x=X"].parseCLIArgs!T.get == T("X", "FOO")); 1452 assert(["--foo=FOO","1","-x=X"].parseCLIArgs!T.get == T("X", "FOO", "1")); 1453 assert(["--foo=FOO","1","2","3","4"].parseCLIArgs!T.get == T(string.init, "FOO", "1",["2","3","4"])); 1454 assert(["-xX"].parseCLIArgs!T.get == T("X")); 1455 1456 struct T1 1457 { 1458 @(PositionalArgument(0, "a")) string[3] a; 1459 @(PositionalArgument(1, "b")) string[] b; 1460 } 1461 static assert(["1","2","3","4","5","6"].parseCLIArgs!T1.get == T1(["1","2","3"],["4","5","6"])); 1462 assert(["1","2","3","4","5","6"].parseCLIArgs!T1.get == T1(["1","2","3"],["4","5","6"])); 1463 1464 struct T2 1465 { 1466 bool foo = true; 1467 } 1468 static assert(["--no-foo"].parseCLIArgs!T2.get == T2(false)); 1469 assert(["--no-foo"].parseCLIArgs!T2.get == T2(false)); 1470 } 1471 1472 unittest 1473 { 1474 struct T 1475 { 1476 @(PositionalArgument(0, "a").Optional()) 1477 string a = "not set"; 1478 1479 @(NamedArgument.Required()) 1480 int b; 1481 } 1482 1483 static assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); 1484 assert(["-b", "4"].parseCLIArgs!T.get == T("not set", 4)); 1485 } 1486 1487 unittest 1488 { 1489 struct T 1490 { 1491 string x; 1492 string foo; 1493 } 1494 1495 auto test(T)(string[] args) 1496 { 1497 Config config; 1498 config.caseSensitive = false; 1499 1500 return args.parseCLIArgs!T(config).get; 1501 } 1502 1503 static assert(test!T(["--Foo","FOO","-X","X"]) == T("X", "FOO")); 1504 static assert(test!T(["--FOo=FOO","-X=X"]) == T("X", "FOO")); 1505 assert(test!T(["--Foo","FOO","-X","X"]) == T("X", "FOO")); 1506 assert(test!T(["--FOo=FOO","-X=X"]) == T("X", "FOO")); 1507 } 1508 1509 unittest 1510 { 1511 auto test(T)(string[] args) 1512 { 1513 Config config; 1514 config.bundling = true; 1515 1516 return args.parseCLIArgs!T(config).get; 1517 } 1518 1519 struct T 1520 { 1521 bool a; 1522 bool b; 1523 } 1524 static assert(test!T(["-a","-b"]) == T(true, true)); 1525 static assert(test!T(["-ab"]) == T(true, true)); 1526 assert(test!T(["-a","-b"]) == T(true, true)); 1527 assert(test!T(["-ab"]) == T(true, true)); 1528 } 1529 1530 unittest 1531 { 1532 struct T 1533 { 1534 bool b; 1535 } 1536 1537 static assert(["-b"] .parseCLIArgs!T.get == T(true)); 1538 static assert(["-b=true"] .parseCLIArgs!T.get == T(true)); 1539 static assert(["-b=false"] .parseCLIArgs!T.get == T(false)); 1540 assert(["-b"] .parseCLIArgs!T.get == T(true)); 1541 assert(["-b=true"] .parseCLIArgs!T.get == T(true)); 1542 assert(["-b=false"] .parseCLIArgs!T.get == T(false)); 1543 } 1544 1545 unittest 1546 { 1547 import std.sumtype: SumType, match; 1548 1549 struct T 1550 { 1551 struct cmd1 { string a; } 1552 struct cmd2 { string b; } 1553 1554 string c; 1555 string d; 1556 1557 SumType!(cmd1, cmd2) cmd; 1558 } 1559 1560 assert(["-c","C","cmd2","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(T.cmd2("B")))); 1561 } 1562 1563 unittest 1564 { 1565 import std.sumtype: SumType, match; 1566 1567 struct T 1568 { 1569 struct cmd1 { string a; } 1570 struct cmd2 { string b; } 1571 1572 string c; 1573 string d; 1574 1575 SumType!(cmd1, Default!cmd2) cmd; 1576 } 1577 1578 assert(["-c","C","-b","B"].parseCLIArgs!T.get == T("C",null,typeof(T.cmd)(Default!(T.cmd2)(T.cmd2("B"))))); 1579 } 1580 1581 struct Main 1582 { 1583 mixin template parseCLIKnownArgs(TYPE, alias newMain, Config config = Config.init) 1584 { 1585 int main(string[] argv) 1586 { 1587 return parseCLIKnownArgs!TYPE(argv[1..$], (TYPE values, string[] args) => newMain(values, args), config); 1588 } 1589 } 1590 1591 mixin template parseCLIArgs(TYPE, alias newMain, Config config = Config.init) 1592 { 1593 int main(string[] argv) 1594 { 1595 return parseCLIArgs!TYPE(argv[1..$], (TYPE values) => newMain(values), config); 1596 } 1597 } 1598 } 1599 1600 template CLI(Config config, COMMANDS...) 1601 { 1602 mixin template main(alias newMain) 1603 { 1604 int main(string[] argv) 1605 { 1606 import std.sumtype: SumType, match; 1607 1608 struct Program 1609 { 1610 SumType!COMMANDS cmd; // Sub-commands 1611 } 1612 1613 return parseCLIArgs!Program(argv[1..$], (Program prog) => prog.cmd.match!newMain, config); 1614 } 1615 } 1616 } 1617 1618 template CLI(Config config, COMMAND) 1619 { 1620 mixin template main(alias newMain) 1621 { 1622 int main(string[] argv) 1623 { 1624 return parseCLIArgs!COMMAND(argv[1..$], (COMMAND cmd) => newMain(cmd), config); 1625 } 1626 } 1627 } 1628 1629 template CLI(Config config) 1630 { 1631 mixin template main(COMMAND, alias newMain) 1632 { 1633 int main(string[] argv) 1634 { 1635 return parseCLIArgs!COMMAND(argv[1..$], (COMMAND cmd) => newMain(cmd), config); 1636 } 1637 } 1638 } 1639 1640 alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS); 1641 1642 1643 unittest 1644 { 1645 struct T 1646 { 1647 int a; 1648 } 1649 1650 static assert(__traits(compiles, { mixin Main.parseCLIArgs!(T, (params) => 0); })); 1651 static assert(__traits(compiles, { mixin Main.parseCLIKnownArgs!(T, (params, args) => 0); })); 1652 } 1653 1654 1655 private struct Parsers 1656 { 1657 static auto Convert(T)(string value) 1658 { 1659 import std.conv: to; 1660 return value.length > 0 ? value.to!T : T.init; 1661 } 1662 1663 static auto PassThrough(string[] values) 1664 { 1665 return values; 1666 } 1667 } 1668 1669 unittest 1670 { 1671 static assert(Parsers.Convert!int("7") == 7); 1672 static assert(Parsers.Convert!string("7") == "7"); 1673 static assert(Parsers.Convert!char("7") == '7'); 1674 1675 static assert(Parsers.PassThrough(["7","8"]) == ["7","8"]); 1676 } 1677 1678 1679 private struct Actions 1680 { 1681 static auto Assign(DEST, SRC=DEST)(ref DEST param, SRC value) 1682 { 1683 param = value; 1684 } 1685 1686 static auto Append(T)(ref T param, T value) 1687 { 1688 param ~= value; 1689 } 1690 1691 static auto Extend(T)(ref T[] param, T value) 1692 { 1693 param ~= value; 1694 } 1695 1696 static auto CallFunction(F)(ref F func, RawParam param) 1697 { 1698 // ... func() 1699 static if(__traits(compiles, { func(); })) 1700 { 1701 func(); 1702 } 1703 // ... func(string value) 1704 else static if(__traits(compiles, { func(param.value[0]); })) 1705 { 1706 foreach(value; param.value) 1707 func(value); 1708 } 1709 // ... func(string[] value) 1710 else static if(__traits(compiles, { func(param.value); })) 1711 { 1712 func(param.value); 1713 } 1714 // ... func(RawParam param) 1715 else static if(__traits(compiles, { func(param); })) 1716 { 1717 func(param); 1718 } 1719 else 1720 static assert(false, "Unsupported callback: " ~ F.stringof); 1721 } 1722 1723 static auto CallFunctionNoParam(F)(ref F func, Param!void param) 1724 { 1725 // ... func() 1726 static if(__traits(compiles, { func(); })) 1727 { 1728 func(); 1729 } 1730 // ... func(string value) 1731 else static if(__traits(compiles, { func(string.init); })) 1732 { 1733 func(string.init); 1734 } 1735 // ... func(string[] value) 1736 else static if(__traits(compiles, { func([]); })) 1737 { 1738 func([]); 1739 } 1740 // ... func(Param!void param) 1741 else static if(__traits(compiles, { func(param); })) 1742 { 1743 func(param); 1744 } 1745 else 1746 static assert(false, "Unsupported callback: " ~ F.stringof); 1747 } 1748 } 1749 1750 unittest 1751 { 1752 int i; 1753 Actions.Assign!(int)(i,7); 1754 assert(i == 7); 1755 } 1756 1757 unittest 1758 { 1759 int[] i; 1760 Actions.Append!(int[])(i,[1,2,3]); 1761 Actions.Append!(int[])(i,[7,8,9]); 1762 assert(i == [1,2,3,7,8,9]); 1763 1764 alias test = (int[] v1, int[] v2) { 1765 int[] res; 1766 1767 Param!(int[]) param; 1768 1769 alias F = Actions.Append!(int[]); 1770 param.value = v1; ActionFunc!(F, int[], int[])(res, param); 1771 1772 param.value = v2; ActionFunc!(F, int[], int[])(res, param); 1773 1774 return res; 1775 }; 1776 static assert(test([1,2,3],[7,8,9]) == [1,2,3,7,8,9]); 1777 } 1778 1779 unittest 1780 { 1781 int[][] i; 1782 Actions.Extend!(int[])(i,[1,2,3]); 1783 Actions.Extend!(int[])(i,[7,8,9]); 1784 assert(i == [[1,2,3],[7,8,9]]); 1785 } 1786 1787 1788 private struct Validators 1789 { 1790 static auto ValueInList(alias values, TYPE)(in Param!TYPE param) 1791 { 1792 import std.array : assocArray, join; 1793 import std.range : repeat, front; 1794 import std.conv: to; 1795 1796 enum valuesAA = assocArray(values, false.repeat); 1797 enum allowedValues = values.to!(string[]).join(','); 1798 1799 static if(is(typeof(values.front) == TYPE)) 1800 auto paramValues = [param.value]; 1801 else 1802 auto paramValues = param.value; 1803 1804 foreach(value; paramValues) 1805 if(!(value in valuesAA)) 1806 { 1807 param.config.onError("Invalid value '", value, "' for argument '", param.name, "'.\nValid argument values are: ", allowedValues); 1808 return false; 1809 } 1810 1811 return true; 1812 } 1813 } 1814 1815 1816 // values => bool 1817 // bool validate(T value) 1818 // bool validate(T[i] value) 1819 // bool validate(Param!T param) 1820 private struct ValidateFunc(alias F, T, string funcName="Validation") 1821 { 1822 static bool opCall(Param!T param) 1823 { 1824 static if(is(F == void)) 1825 { 1826 return true; 1827 } 1828 else static if(__traits(compiles, { F(param); })) 1829 { 1830 // bool validate(Param!T param) 1831 return cast(bool) F(param); 1832 } 1833 else static if(__traits(compiles, { F(param.value); })) 1834 { 1835 // bool validate(T values) 1836 return cast(bool) F(param.value); 1837 } 1838 else static if(/*isArray!T &&*/ __traits(compiles, { F(param.value[0]); })) 1839 { 1840 // bool validate(T[i] value) 1841 foreach(value; param.value) 1842 if(!F(value)) 1843 return false; 1844 return true; 1845 } 1846 else 1847 static assert(false, funcName~" function is not supported for type "~T.stringof~": "~typeof(F).stringof); 1848 } 1849 } 1850 1851 unittest 1852 { 1853 auto test(alias F, T)(T[] values) 1854 { 1855 Param!(T[]) param; 1856 param.value = values; 1857 return ValidateFunc!(F, T[])(param); 1858 } 1859 1860 // bool validate(T[] values) 1861 static assert(test!((string[] a) => true, string)(["1","2","3"])); 1862 static assert(test!((int[] a) => true, int)([1,2,3])); 1863 1864 // bool validate(T value) 1865 static assert(test!((string a) => true, string)(["1","2","3"])); 1866 static assert(test!((int a) => true, int)([1,2,3])); 1867 1868 // bool validate(Param!T param) 1869 static assert(test!((RawParam p) => true, string)(["1","2","3"])); 1870 static assert(test!((Param!(int[]) p) => true, int)([1,2,3])); 1871 } 1872 1873 unittest 1874 { 1875 static assert(ValidateFunc!(void, string[])(RawParam(Config.init, "", ["1","2","3"]))); 1876 1877 static assert(!__traits(compiles, { ValidateFunc!(() {}, string[])(config, "", ["1","2","3"]); })); 1878 static assert(!__traits(compiles, { ValidateFunc!((int,int) {}, string[])(config, "", ["1","2","3"]); })); 1879 } 1880 1881 1882 private template ParseType(alias F, T) 1883 { 1884 import std.traits : Unqual; 1885 1886 static if(is(F == void)) 1887 alias ParseType = Unqual!T; 1888 else static if(Parameters!F.length == 0) 1889 static assert(false, "Parse function should take at least one parameter"); 1890 else static if(Parameters!F.length == 1) 1891 { 1892 // T action(arg) 1893 alias ParseType = Unqual!(ReturnType!F); 1894 static assert(!is(ParseType == void), "Parse function should return value"); 1895 } 1896 else static if(Parameters!F.length == 2 && is(Parameters!F[0] == Config)) 1897 { 1898 // T action(Config config, arg) 1899 alias ParseType = Unqual!(ReturnType!F); 1900 static assert(!is(ParseType == void), "Parse function should return value"); 1901 } 1902 else static if(Parameters!F.length == 2) 1903 { 1904 // ... action(ref T param, arg) 1905 alias ParseType = Parameters!F[0]; 1906 } 1907 else static if(Parameters!F.length == 3) 1908 { 1909 // ... action(Config config, ref T param, arg) 1910 alias ParseType = Parameters!F[1]; 1911 } 1912 else static if(Parameters!F.length == 4) 1913 { 1914 // ... action(Config config, string argName, ref T param, arg) 1915 alias ParseType = Parameters!F[2]; 1916 } 1917 else 1918 static assert(false, "Parse function has too many parameters: "~Parameters!F.stringof); 1919 } 1920 1921 unittest 1922 { 1923 static assert(is(ParseType!(void, double) == double)); 1924 static assert(!__traits(compiles, { ParseType!((){}, double) p; })); 1925 static assert(!__traits(compiles, { ParseType!((int,int,int,int,int){}, double) p; })); 1926 1927 // T action(arg) 1928 static assert(is(ParseType!((int)=>3, double) == int)); 1929 static assert(!__traits(compiles, { ParseType!((int){}, double) p; })); 1930 // T action(Config config, arg) 1931 static assert(is(ParseType!((Config config, int)=>3, double) == int)); 1932 static assert(!__traits(compiles, { ParseType!((Config config, int){}, double) p; })); 1933 // ... action(ref T param, arg) 1934 static assert(is(ParseType!((ref int, string v) {}, double) == int)); 1935 // ... action(Config config, ref T param, arg) 1936 static assert(is(ParseType!((Config config, ref int, string v) {}, double) == int)); 1937 // ... action(Config config, string argName, ref T param, arg) 1938 //static assert(is(ParseType!((Config config, string argName, ref int, string v) {}, double) == int)); 1939 } 1940 1941 1942 // T parse(string[] values) 1943 // T parse(string value) 1944 // T parse(RawParam param) 1945 // bool parse(ref T receiver, RawParam param) 1946 // void parse(ref T receiver, RawParam param) 1947 private struct ParseFunc(alias F, T) 1948 { 1949 alias ParseType = .ParseType!(F, T); 1950 1951 static bool opCall(ref ParseType receiver, RawParam param) 1952 { 1953 static if(is(F == void)) 1954 { 1955 foreach(value; param.value) 1956 receiver = Parsers.Convert!T(value); 1957 return true; 1958 } 1959 // T parse(string[] values) 1960 else static if(__traits(compiles, { receiver = cast(ParseType) F(param.value); })) 1961 { 1962 receiver = cast(ParseType) F(param.value); 1963 return true; 1964 } 1965 // T parse(string value) 1966 else static if(__traits(compiles, { receiver = cast(ParseType) F(param.value[0]); })) 1967 { 1968 foreach(value; param.value) 1969 receiver = cast(ParseType) F(value); 1970 return true; 1971 } 1972 // T parse(RawParam param) 1973 else static if(__traits(compiles, { receiver = cast(ParseType) F(param); })) 1974 { 1975 receiver = cast(ParseType) F(param); 1976 return true; 1977 } 1978 // bool parse(ref T receiver, RawParam param) 1979 // void parse(ref T receiver, RawParam param) 1980 else static if(__traits(compiles, { F(receiver, param); })) 1981 { 1982 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 1983 { 1984 // bool parse(ref T receiver, RawParam param) 1985 return cast(bool) F(receiver, param); 1986 } 1987 else 1988 { 1989 // void parse(ref T receiver, RawParam param) 1990 F(receiver, param); 1991 return true; 1992 } 1993 } 1994 else 1995 static assert(false, "Parse function is not supported"); 1996 } 1997 } 1998 1999 unittest 2000 { 2001 int i; 2002 RawParam param; 2003 param.value = ["1","2","3"]; 2004 assert(ParseFunc!(void, int)(i, param)); 2005 assert(i == 3); 2006 } 2007 2008 unittest 2009 { 2010 auto test(alias F, T)(string[] values) 2011 { 2012 T value; 2013 RawParam param; 2014 param.value = values; 2015 assert(ParseFunc!(F, T)(value, param)); 2016 return value; 2017 } 2018 2019 // T parse(string value) 2020 static assert(test!((string a) => a, string)(["1","2","3"]) == "3"); 2021 2022 // T parse(string[] values) 2023 static assert(test!((string[] a) => a, string[])(["1","2","3"]) == ["1","2","3"]); 2024 2025 // T parse(RawParam param) 2026 static assert(test!((RawParam p) => p.value[0], string)(["1","2","3"]) == "1"); 2027 2028 // bool parse(ref T receiver, RawParam param) 2029 static assert(test!((ref string[] r, RawParam p) { r = p.value; return true; }, string[])(["1","2","3"]) == ["1","2","3"]); 2030 2031 // void parse(ref T receiver, RawParam param) 2032 static assert(test!((ref string[] r, RawParam p) { r = p.value; }, string[])(["1","2","3"]) == ["1","2","3"]); 2033 } 2034 2035 2036 // bool action(ref T receiver, ParseType value) 2037 // void action(ref T receiver, ParseType value) 2038 // bool action(ref T receiver, Param!ParseType param) 2039 // void action(ref T receiver, Param!ParseType param) 2040 private struct ActionFunc(alias F, T, ParseType) 2041 { 2042 static bool opCall(ref T receiver, Param!ParseType param) 2043 { 2044 static if(is(F == void)) 2045 { 2046 Actions.Assign!(T, ParseType)(receiver, param.value); 2047 return true; 2048 } 2049 // bool action(ref T receiver, ParseType value) 2050 // void action(ref T receiver, ParseType value) 2051 else static if(__traits(compiles, { F(receiver, param.value); })) 2052 { 2053 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param.value); })) 2054 { 2055 // bool action(ref T receiver, ParseType value) 2056 return cast(bool) F(receiver, param.value); 2057 } 2058 else 2059 { 2060 // void action(ref T receiver, ParseType value) 2061 F(receiver, param.value); 2062 return true; 2063 } 2064 } 2065 // bool action(ref T receiver, Param!ParseType param) 2066 // void action(ref T receiver, Param!ParseType param) 2067 else static if(__traits(compiles, { F(receiver, param); })) 2068 { 2069 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 2070 { 2071 // bool action(ref T receiver, Param!ParseType param) 2072 return cast(bool) F(receiver, param); 2073 } 2074 else 2075 { 2076 // void action(ref T receiver, Param!ParseType param) 2077 F(receiver, param); 2078 return true; 2079 } 2080 } 2081 else 2082 static assert(false, "Action function is not supported"); 2083 } 2084 } 2085 2086 unittest 2087 { 2088 auto param(T)(T values) 2089 { 2090 Param!T param; 2091 param.value = values; 2092 return param; 2093 } 2094 auto test(alias F, T)(T values) 2095 { 2096 T receiver; 2097 assert(ActionFunc!(F, T, T)(receiver, param(values))); 2098 return receiver; 2099 } 2100 2101 static assert(test!(void, string[])(["1","2","3"]) == ["1","2","3"]); 2102 2103 static assert(!__traits(compiles, { test!(() {}, string[])(["1","2","3"]); })); 2104 static assert(!__traits(compiles, { test!((int,int) {}, string[])(["1","2","3"]); })); 2105 2106 // bool action(ref T receiver, ParseType value) 2107 static assert(test!((ref string[] p, string[] a) { p=a; return true; }, string[])(["1","2","3"]) == ["1","2","3"]); 2108 2109 // void action(ref T receiver, ParseType value) 2110 static assert(test!((ref string[] p, string[] a) { p=a; }, string[])(["1","2","3"]) == ["1","2","3"]); 2111 2112 // bool action(ref T receiver, Param!ParseType param) 2113 static assert(test!((ref string[] p, Param!(string[]) a) { p=a.value; return true; }, string[]) (["1","2","3"]) == ["1","2","3"]); 2114 2115 // void action(ref T receiver, Param!ParseType param) 2116 static assert(test!((ref string[] p, Param!(string[]) a) { p=a.value; }, string[])(["1","2","3"]) == ["1","2","3"]); 2117 } 2118 2119 2120 // => receiver + bool 2121 // DEST action() 2122 // bool action(ref DEST receiver) 2123 // void action(ref DEST receiver) 2124 // bool action(ref DEST receiver, Param!void param) 2125 // void action(ref DEST receiver, Param!void param) 2126 private struct NoValueActionFunc(alias F, T) 2127 { 2128 static bool opCall(ref T receiver, Param!void param) 2129 { 2130 static if(is(F == void)) 2131 { 2132 assert(false, "No-value action function is not provided"); 2133 } 2134 else static if(__traits(compiles, { receiver = cast(T) F(); })) 2135 { 2136 // DEST action() 2137 receiver = cast(T) F(); 2138 return true; 2139 } 2140 else static if(__traits(compiles, { F(receiver); })) 2141 { 2142 static if(__traits(compiles, { auto res = cast(bool) F(receiver); })) 2143 { 2144 // bool action(ref DEST receiver) 2145 return cast(bool) F(receiver); 2146 } 2147 else 2148 { 2149 // void action(ref DEST receiver) 2150 F(receiver); 2151 return true; 2152 } 2153 } 2154 else static if(__traits(compiles, { F(receiver, param); })) 2155 { 2156 static if(__traits(compiles, { auto res = cast(bool) F(receiver, param); })) 2157 { 2158 // bool action(ref DEST receiver, Param!void param) 2159 return cast(bool) F(receiver, param); 2160 } 2161 else 2162 { 2163 // void action(ref DEST receiver, Param!void param) 2164 F(receiver, param); 2165 return true; 2166 } 2167 } 2168 else 2169 static assert(false, "No-value action function has too many parameters: "~Parameters!F.stringof); 2170 } 2171 } 2172 2173 unittest 2174 { 2175 auto test(alias F, T)() 2176 { 2177 T receiver; 2178 assert(NoValueActionFunc!(F, T)(receiver, Param!void.init)); 2179 return receiver; 2180 } 2181 2182 static assert(!__traits(compiles, { NoValueActionFunc!(() {}, int); })); 2183 static assert(!__traits(compiles, { NoValueActionFunc!((int) {}, int); })); 2184 static assert(!__traits(compiles, { NoValueActionFunc!((int,int) {}, int); })); 2185 static assert(!__traits(compiles, { NoValueActionFunc!((int,int,int) {}, int); })); 2186 2187 // DEST action() 2188 static assert(test!(() => 7, int) == 7); 2189 2190 // bool action(ref DEST param) 2191 static assert(test!((ref int p) { p=7; return true; }, int) == 7); 2192 2193 // void action(ref DEST param) 2194 static assert(test!((ref int p) { p=7; }, int) == 7); 2195 2196 // bool action(ref DEST receiver, Param!void param) 2197 static assert(test!((ref int r, Param!void p) { r=7; return true; }, int) == 7); 2198 2199 // void action(ref DEST receiver, Param!void param) 2200 static assert(test!((ref int r, Param!void p) { r=7; }, int) == 7); 2201 } 2202 2203 2204 private void splitValues(ref RawParam param) 2205 { 2206 if(param.config.arraySep == char.init) 2207 return; 2208 2209 import std.array : array, split; 2210 import std.algorithm : map, joiner; 2211 2212 param.value = param.value.map!((string s) => s.split(param.config.arraySep)).joiner.array; 2213 } 2214 2215 unittest 2216 { 2217 alias test = (char arraySep, string[] values) 2218 { 2219 Config config; 2220 config.arraySep = arraySep; 2221 2222 auto param = RawParam(config, "", values); 2223 2224 splitValues(param); 2225 2226 return param.value; 2227 }; 2228 2229 static assert(test(',', []) == []); 2230 static assert(test(',', ["a","b","c"]) == ["a","b","c"]); 2231 static assert(test(',', ["a,b","c","d,e,f"]) == ["a","b","c","d","e","f"]); 2232 static assert(test(' ', ["a,b","c","d,e,f"]) == ["a,b","c","d,e,f"]); 2233 } 2234 2235 2236 private struct ValueParseFunctions(alias PreProcess, 2237 alias PreValidation, 2238 alias Parse, 2239 alias Validation, 2240 alias Action, 2241 alias NoValueAction) 2242 { 2243 alias changePreProcess (alias func) = ValueParseFunctions!( func, PreValidation, Parse, Validation, Action, NoValueAction); 2244 alias changePreValidation(alias func) = ValueParseFunctions!(PreProcess, func, Parse, Validation, Action, NoValueAction); 2245 alias changeParse (alias func) = ValueParseFunctions!(PreProcess, PreValidation, func, Validation, Action, NoValueAction); 2246 alias changeValidation (alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, func, Action, NoValueAction); 2247 alias changeAction (alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, Validation, func, NoValueAction); 2248 alias changeNoValueAction(alias func) = ValueParseFunctions!(PreProcess, PreValidation, Parse, Validation, Action, func); 2249 2250 template addDefaults(T) 2251 { 2252 static if(is(PreProcess == void)) 2253 alias preProc = DefaultValueParseFunctions!T; 2254 else 2255 alias preProc = DefaultValueParseFunctions!T.changePreProcess!PreProcess; 2256 2257 static if(is(PreValidation == void)) 2258 alias preVal = preProc; 2259 else 2260 alias preVal = preProc.changePreValidation!PreValidation; 2261 2262 static if(is(Parse == void)) 2263 alias parse = preVal; 2264 else 2265 alias parse = preVal.changeParse!Parse; 2266 2267 static if(is(Validation == void)) 2268 alias val = parse; 2269 else 2270 alias val = parse.changeValidation!Validation; 2271 2272 static if(is(Action == void)) 2273 alias action = val; 2274 else 2275 alias action = val.changeAction!Action; 2276 2277 static if(is(NoValueAction == void)) 2278 alias addDefaults = action; 2279 else 2280 alias addDefaults = action.changeNoValueAction!NoValueAction; 2281 } 2282 2283 2284 // Procedure to process (parse) the values to an argument of type T 2285 // - if there is a value(s): 2286 // - pre validate raw strings 2287 // - parse raw strings 2288 // - validate parsed values 2289 // - action with values 2290 // - if there is no value: 2291 // - action if no value 2292 // Requirement: rawValues.length must be correct 2293 static bool parse(T)(ref T receiver, RawParam param) 2294 { 2295 return addDefaults!T.parseImpl(receiver, param); 2296 } 2297 static bool parseImpl(T)(ref T receiver, ref RawParam rawParam) 2298 { 2299 alias ParseType(T) = .ParseType!(Parse, T); 2300 2301 alias preValidation = ValidateFunc!(PreValidation, string[], "Pre validation"); 2302 alias parse(T) = ParseFunc!(Parse, T); 2303 alias validation(T) = ValidateFunc!(Validation, ParseType!T); 2304 alias action(T) = ActionFunc!(Action, T, ParseType!T); 2305 alias noValueAction(T) = NoValueActionFunc!(NoValueAction, T); 2306 2307 if(rawParam.value.length == 0) 2308 { 2309 return noValueAction!T(receiver, Param!void(rawParam.config, rawParam.name)); 2310 } 2311 else 2312 { 2313 static if(!is(PreProcess == void)) 2314 PreProcess(rawParam); 2315 2316 if(!preValidation(rawParam)) 2317 return false; 2318 2319 auto parsedParam = Param!(ParseType!T)(rawParam.config, rawParam.name); 2320 2321 if(!parse!T(parsedParam.value, rawParam)) 2322 return false; 2323 2324 if(!validation!T(parsedParam)) 2325 return false; 2326 2327 if(!action!T(receiver, parsedParam)) 2328 return false; 2329 2330 return true; 2331 } 2332 } 2333 } 2334 2335 2336 private template DefaultValueParseFunctions(T) 2337 if(!is(T == void)) 2338 { 2339 import std.traits; 2340 import std.conv: to; 2341 2342 static if(is(T == enum)) 2343 { 2344 alias DefaultValueParseFunctions = ValueParseFunctions!( 2345 void, // pre process 2346 Validators.ValueInList!(EnumMembersAsStrings!T, typeof(RawParam.value)), // pre validate 2347 void, // parse 2348 void, // validate 2349 void, // action 2350 void // no-value action 2351 ); 2352 } 2353 else static if(isSomeString!T || isNumeric!T) 2354 { 2355 alias DefaultValueParseFunctions = ValueParseFunctions!( 2356 void, // pre process 2357 void, // pre validate 2358 void, // parse 2359 void, // validate 2360 void, // action 2361 void // no-value action 2362 ); 2363 } 2364 else static if(isBoolean!T) 2365 { 2366 alias DefaultValueParseFunctions = ValueParseFunctions!( 2367 void, // pre process 2368 void, // pre validate 2369 (string value) // parse 2370 { 2371 switch(value) 2372 { 2373 case "": goto case; 2374 case "yes": goto case; 2375 case "y": return true; 2376 case "no": goto case; 2377 case "n": return false; 2378 default: return value.to!T; 2379 } 2380 }, 2381 void, // validate 2382 void, // action 2383 (ref T result) { result = true; } // no-value action 2384 ); 2385 } 2386 else static if(isSomeChar!T) 2387 { 2388 alias DefaultValueParseFunctions = ValueParseFunctions!( 2389 void, // pre process 2390 void, // pre validate 2391 (string value) // parse 2392 { 2393 return value.length > 0 ? value[0].to!T : T.init; 2394 }, 2395 void, // validate 2396 void, // action 2397 void // no-value action 2398 ); 2399 } 2400 else static if(isArray!T) 2401 { 2402 import std.traits: ForeachType; 2403 2404 alias TElement = ForeachType!T; 2405 2406 static if(!isArray!TElement || isSomeString!TElement) // 1D array 2407 { 2408 static if(!isStaticArray!T) 2409 alias action = Actions.Append!T; 2410 else 2411 alias action = Actions.Assign!T; 2412 2413 alias DefaultValueParseFunctions = DefaultValueParseFunctions!TElement 2414 .changePreProcess!splitValues 2415 .changeParse!((ref T receiver, RawParam param) 2416 { 2417 static if(!isStaticArray!T) 2418 { 2419 if(receiver.length < param.value.length) 2420 receiver.length = param.value.length; 2421 } 2422 2423 foreach(i, value; param.value) 2424 { 2425 if(!DefaultValueParseFunctions!TElement.parse(receiver[i], 2426 RawParam(param.config, param.name, [value]))) 2427 return false; 2428 } 2429 2430 return true; 2431 }) 2432 .changeAction!(action) 2433 .changeNoValueAction!((ref T param) {}); 2434 } 2435 else static if(!isArray!(ForeachType!TElement) || isSomeString!(ForeachType!TElement)) // 2D array 2436 { 2437 alias DefaultValueParseFunctions = DefaultValueParseFunctions!TElement 2438 .changeAction!(Actions.Extend!TElement) 2439 .changeNoValueAction!((ref T param) { param ~= TElement.init; }); 2440 } 2441 else 2442 { 2443 static assert(false, "Multi-dimentional arrays are not supported: " ~ T.stringof); 2444 } 2445 } 2446 else static if(isAssociativeArray!T) 2447 { 2448 import std.string : indexOf; 2449 alias DefaultValueParseFunctions = ValueParseFunctions!( 2450 splitValues, // pre process 2451 void, // pre validate 2452 Parsers.PassThrough, // parse 2453 void, // validate 2454 (ref T recepient, Param!(string[]) param) // action 2455 { 2456 alias K = KeyType!T; 2457 alias V = ValueType!T; 2458 2459 foreach(input; param.value) 2460 { 2461 auto j = indexOf(input, param.config.assignChar); 2462 if(j < 0) 2463 return false; 2464 2465 K key; 2466 if(!DefaultValueParseFunctions!K.parse(key, RawParam(param.config, param.name, [input[0 .. j]]))) 2467 return false; 2468 2469 V value; 2470 if(!DefaultValueParseFunctions!V.parse(value, RawParam(param.config, param.name, [input[j + 1 .. $]]))) 2471 return false; 2472 2473 recepient[key] = value; 2474 } 2475 return true; 2476 }, 2477 (ref T param) {} // no-value action 2478 ); 2479 } 2480 else static if(is(T == delegate)) 2481 { 2482 alias DefaultValueParseFunctions = ValueParseFunctions!( 2483 void, // pre process 2484 void, // pre validate 2485 Parsers.PassThrough, // parse 2486 void, // validate 2487 Actions.CallFunction!T, // action 2488 Actions.CallFunctionNoParam!T // no-value action 2489 ); 2490 } 2491 else 2492 static assert(false, "Type is not supported: " ~ T.stringof); 2493 } 2494 2495 unittest 2496 { 2497 enum MyEnum { foo, bar, } 2498 2499 import std.meta: AliasSeq; 2500 static foreach(T; AliasSeq!(string, bool, int, double, char, MyEnum)) 2501 static foreach(R; AliasSeq!(T, T[], T[][])) 2502 {{ 2503 // ensure that this compiles 2504 R receiver; 2505 RawParam param; 2506 param.value = [""]; 2507 DefaultValueParseFunctions!R.parse(receiver, param); 2508 }} 2509 } 2510 2511 unittest 2512 { 2513 alias test(R) = (string[][] values) 2514 { 2515 auto config = Config('=', ','); 2516 R receiver; 2517 foreach(value; values) 2518 { 2519 assert(DefaultValueParseFunctions!R.parse(receiver, RawParam(config, "", value))); 2520 } 2521 return receiver; 2522 }; 2523 2524 static assert(test!(string[])([["1","2","3"], [], ["4"]]) == ["1","2","3","4"]); 2525 static assert(test!(string[][])([["1","2","3"], [], ["4"]]) == [["1","2","3"],[],["4"]]); 2526 2527 static assert(test!(string[string])([["a=bar","b=foo"], [], ["b=baz","c=boo"]]) == ["a":"bar", "b":"baz", "c":"boo"]); 2528 2529 static assert(test!(string[])([["1,2,3"], [], ["4"]]) == ["1","2","3","4"]); 2530 static assert(test!(string[string])([["a=bar,b=foo"], [], ["b=baz,c=boo"]]) == ["a":"bar", "b":"baz", "c":"boo"]); 2531 2532 static assert(test!(int[])([["1","2","3"], [], ["4"]]) == [1,2,3,4]); 2533 static assert(test!(int[][])([["1","2","3"], [], ["4"]]) == [[1,2,3],[],[4]]); 2534 2535 } 2536 2537 unittest 2538 { 2539 import std.math: isNaN; 2540 enum MyEnum { foo, bar, } 2541 2542 alias test(T) = (string[] values) 2543 { 2544 T receiver; 2545 RawParam param; 2546 param.value = values; 2547 assert(DefaultValueParseFunctions!T.parse(receiver, param)); 2548 return receiver; 2549 }; 2550 2551 static assert(test!string([""]) == ""); 2552 static assert(test!string(["foo"]) == "foo"); 2553 static assert(isNaN(test!double([""]))); 2554 static assert(test!double(["-12.34"]) == -12.34); 2555 static assert(test!double(["12.34"]) == 12.34); 2556 static assert(test!uint(["1234"]) == 1234); 2557 static assert(test!int([""]) == int.init); 2558 static assert(test!int(["-1234"]) == -1234); 2559 static assert(test!char([""]) == char.init); 2560 static assert(test!char(["f"]) == 'f'); 2561 static assert(test!bool([]) == true); 2562 static assert(test!bool([""]) == true); 2563 static assert(test!bool(["yes"]) == true); 2564 static assert(test!bool(["y"]) == true); 2565 static assert(test!bool(["true"]) == true); 2566 static assert(test!bool(["no"]) == false); 2567 static assert(test!bool(["n"]) == false); 2568 static assert(test!bool(["false"]) == false); 2569 static assert(test!MyEnum(["foo"]) == MyEnum.foo); 2570 static assert(test!MyEnum(["bar"]) == MyEnum.bar); 2571 static assert(test!(MyEnum[])(["bar","foo"]) == [MyEnum.bar, MyEnum.foo]); 2572 static assert(test!(string[string])(["a=bar","b=foo"]) == ["a":"bar", "b":"foo"]); 2573 static assert(test!(MyEnum[string])(["a=bar","b=foo"]) == ["a":MyEnum.bar, "b":MyEnum.foo]); 2574 static assert(test!(int[MyEnum])(["bar=3","foo=5"]) == [MyEnum.bar:3, MyEnum.foo:5]); 2575 } 2576 2577 2578 private struct ArgumentInfo 2579 { 2580 string[] names; 2581 2582 string description; 2583 string placeholder; 2584 2585 private void setAllowedValues(alias names)() 2586 { 2587 if(placeholder.length == 0) 2588 { 2589 import std.conv: to; 2590 import std.array: join; 2591 import std.format: format; 2592 placeholder = "{%s}".format(names.to!(string[]).join(',')); 2593 } 2594 } 2595 2596 bool hideFromHelp = false; // if true then this argument is not printed on help page 2597 2598 bool required; 2599 2600 Nullable!uint position; 2601 2602 @property bool positional() const { return !position.isNull; } 2603 2604 Nullable!ulong minValuesCount; 2605 Nullable!ulong maxValuesCount; 2606 2607 private bool checkValuesCount(in Config config, string argName, ulong count) const 2608 { 2609 immutable min = minValuesCount.get; 2610 immutable max = maxValuesCount.get; 2611 2612 // override for boolean flags 2613 if(allowBooleanNegation && count == 1) 2614 return true; 2615 2616 if(min == max && count != min) 2617 { 2618 config.onError("argument ",argName,": expected ",min,min == 1 ? " value" : " values"); 2619 return false; 2620 } 2621 if(count < min) 2622 { 2623 config.onError("argument ",argName,": expected at least ",min,min == 1 ? " value" : " values"); 2624 return false; 2625 } 2626 if(count > max) 2627 { 2628 config.onError("argument ",argName,": expected at most ",max,max == 1 ? " value" : " values"); 2629 return false; 2630 } 2631 2632 return true; 2633 } 2634 2635 private bool allowBooleanNegation = true; 2636 } 2637 2638 2639 2640 //////////////////////////////////////////////////////////////////////////////////////////////////// 2641 // User defined attributes 2642 //////////////////////////////////////////////////////////////////////////////////////////////////// 2643 private struct ArgumentUDA(alias ValueParseFunctions) 2644 { 2645 ArgumentInfo info; 2646 2647 alias parsingFunc = ValueParseFunctions; 2648 2649 2650 2651 auto ref Description(string text) 2652 { 2653 info.description = text; 2654 return this; 2655 } 2656 2657 auto ref HideFromHelp(bool hide = true) 2658 { 2659 info.hideFromHelp = hide; 2660 return this; 2661 } 2662 2663 auto ref Placeholder(string value) 2664 { 2665 info.placeholder = value; 2666 return this; 2667 } 2668 2669 auto ref Required() 2670 { 2671 info.required = true; 2672 return this; 2673 } 2674 2675 auto ref Optional() 2676 { 2677 info.required = false; 2678 return this; 2679 } 2680 2681 auto ref NumberOfValues(ulong num) 2682 { 2683 info.minValuesCount = num; 2684 info.maxValuesCount = num; 2685 return this; 2686 } 2687 2688 auto ref NumberOfValues(ulong min, ulong max) 2689 { 2690 info.minValuesCount = min; 2691 info.maxValuesCount = max; 2692 return this; 2693 } 2694 2695 auto ref MinNumberOfValues(ulong min) 2696 { 2697 assert(min <= info.maxValuesCount.get(ulong.max)); 2698 2699 info.minValuesCount = min; 2700 return this; 2701 } 2702 2703 auto ref MaxNumberOfValues(ulong max) 2704 { 2705 assert(max >= info.minValuesCount.get(0)); 2706 2707 info.maxValuesCount = max; 2708 return this; 2709 } 2710 2711 // ReverseSwitch 2712 } 2713 2714 unittest 2715 { 2716 auto arg = NamedArgument.Description("desc").Placeholder("text"); 2717 assert(arg.info.description == "desc"); 2718 assert(arg.info.placeholder == "text"); 2719 assert(!arg.info.hideFromHelp); 2720 assert(!arg.info.required); 2721 assert(arg.info.minValuesCount.isNull); 2722 assert(arg.info.maxValuesCount.isNull); 2723 2724 arg = arg.HideFromHelp().Required().NumberOfValues(10); 2725 assert(arg.info.hideFromHelp); 2726 assert(arg.info.required); 2727 assert(arg.info.minValuesCount.get == 10); 2728 assert(arg.info.maxValuesCount.get == 10); 2729 2730 arg = arg.Optional().NumberOfValues(20,30); 2731 assert(!arg.info.required); 2732 assert(arg.info.minValuesCount.get == 20); 2733 assert(arg.info.maxValuesCount.get == 30); 2734 2735 arg = arg.MinNumberOfValues(2).MaxNumberOfValues(3); 2736 assert(arg.info.minValuesCount.get == 2); 2737 assert(arg.info.maxValuesCount.get == 3); 2738 } 2739 2740 unittest 2741 { 2742 struct T 2743 { 2744 @(NamedArgument.NumberOfValues(1,3)) 2745 int[] a; 2746 @(NamedArgument.NumberOfValues(2)) 2747 int[] b; 2748 } 2749 2750 assert(["-a","1","2","3","-b","4","5"].parseCLIArgs!T.get == T([1,2,3],[4,5])); 2751 assert(["-a","1","-b","4","5"].parseCLIArgs!T.get == T([1],[4,5])); 2752 } 2753 2754 private enum bool isArgumentUDA(T) = (is(typeof(T.info) == ArgumentInfo) && is(T.parsingFunc)); 2755 2756 2757 auto PreValidation(alias func, ARG)(ARG arg) 2758 if(isArgumentUDA!ARG) 2759 { 2760 return ArgumentUDA!(arg.parsingFunc.changePreValidation!func)(arg.tupleof); 2761 } 2762 2763 auto Parse(alias func, ARG)(ARG arg) 2764 if(isArgumentUDA!ARG) 2765 { 2766 return ArgumentUDA!(arg.parsingFunc.changeParse!func)(arg.tupleof); 2767 } 2768 2769 auto Validation(alias func, ARG)(ARG arg) 2770 if(isArgumentUDA!ARG) 2771 { 2772 return ArgumentUDA!(arg.parsingFunc.changeValidation!func)(arg.tupleof); 2773 } 2774 2775 auto Action(alias func, ARG)(ARG arg) 2776 if(isArgumentUDA!ARG) 2777 { 2778 return ArgumentUDA!(arg.parsingFunc.changeAction!func)(arg.tupleof); 2779 } 2780 2781 auto AllowNoValue(alias valueToUse, ARG)(ARG arg) 2782 if(isArgumentUDA!ARG) 2783 { 2784 auto desc = ArgumentUDA!(arg.parsingFunc.changeNoValueAction!(() { return valueToUse; }))(arg.tupleof); 2785 desc.info.minValuesCount = 0; 2786 return desc; 2787 } 2788 2789 auto RequireNoValue(alias valueToUse, ARG)(ARG arg) 2790 if(isArgumentUDA!ARG) 2791 { 2792 auto desc = arg.AllowNoValue!valueToUse; 2793 desc.info.minValuesCount = 0; 2794 desc.info.maxValuesCount = 0; 2795 return desc; 2796 } 2797 2798 auto Counter(ARG)(ARG arg) 2799 if(isArgumentUDA!ARG) 2800 { 2801 struct CounterParsingFunction 2802 { 2803 static bool parse(T)(ref T receiver, const ref RawParam param) 2804 { 2805 assert(param.value.length == 0); 2806 2807 ++receiver; 2808 2809 return true; 2810 } 2811 } 2812 2813 auto desc = ArgumentUDA!(CounterParsingFunction)(arg.tupleof); 2814 desc.info.minValuesCount = 0; 2815 desc.info.maxValuesCount = 0; 2816 return desc; 2817 } 2818 2819 2820 unittest 2821 { 2822 struct T 2823 { 2824 @(NamedArgument.Counter()) int a; 2825 } 2826 2827 static assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); 2828 assert(["-a","-a","-a"].parseCLIArgs!T.get == T(3)); 2829 } 2830 2831 2832 auto AllowedValues(alias values, ARG)(ARG arg) 2833 { 2834 import std.array : assocArray; 2835 import std.range : repeat; 2836 2837 enum valuesAA = assocArray(values, false.repeat); 2838 2839 auto desc = arg.Validation!(Validators.ValueInList!(values, KeyType!(typeof(valuesAA)))); 2840 desc.info.setAllowedValues!values; 2841 return desc; 2842 } 2843 2844 2845 unittest 2846 { 2847 struct T 2848 { 2849 @(NamedArgument.AllowedValues!([1,3,5])) int a; 2850 } 2851 2852 static assert(["-a","3"].parseCLIArgs!T.get == T(3)); 2853 assert(["-a","2"].parseCLIArgs!T.isNull); 2854 assert(["-a","3"].parseCLIArgs!T.get == T(3)); 2855 } 2856 2857 unittest 2858 { 2859 struct T 2860 { 2861 @(NamedArgument.AllowedValues!(["apple","pear","banana"])) 2862 string fruit; 2863 } 2864 2865 static assert(["--fruit", "apple"].parseCLIArgs!T.get == T("apple")); 2866 assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); 2867 } 2868 2869 unittest 2870 { 2871 enum Fruit { apple, pear, banana } 2872 struct T 2873 { 2874 @NamedArgument 2875 Fruit fruit; 2876 } 2877 2878 static assert(["--fruit", "apple"].parseCLIArgs!T.get == T(Fruit.apple)); 2879 assert(["--fruit", "kiwi"].parseCLIArgs!T.isNull); 2880 } 2881 2882 2883 auto PositionalArgument(uint pos) 2884 { 2885 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo()).Required(); 2886 arg.info.position = pos; 2887 return arg; 2888 } 2889 2890 auto PositionalArgument(uint pos, string name) 2891 { 2892 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Required(); 2893 arg.info.position = pos; 2894 return arg; 2895 } 2896 2897 auto NamedArgument(string[] name...) 2898 { 2899 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo(name)).Optional(); 2900 } 2901 2902 auto NamedArgument(string name) 2903 { 2904 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Optional(); 2905 } 2906 2907 struct TrailingArguments {} 2908 2909 2910 unittest 2911 { 2912 struct T 2913 { 2914 string a; 2915 string b; 2916 2917 @TrailingArguments string[] args; 2918 } 2919 2920 static assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); 2921 assert(["-a","A","--","-b","B"].parseCLIArgs!T.get == T("A","",["-b","B"])); 2922 } 2923 2924 unittest 2925 { 2926 struct T 2927 { 2928 @NamedArgument int i; 2929 @NamedArgument(["u","u1"]) uint u; 2930 @NamedArgument("d","d1") double d; 2931 } 2932 2933 static assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 2934 static assert(["-i","-5","-u1","8","-d1","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 2935 assert(["-i","-5","-u","8","-d","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 2936 assert(["-i","-5","-u1","8","-d1","12.345"].parseCLIArgs!T.get == T(-5,8,12.345)); 2937 } 2938 2939 unittest 2940 { 2941 struct T 2942 { 2943 @(NamedArgument) int[] a; 2944 @(NamedArgument) int[][] b; 2945 } 2946 2947 static assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); 2948 static assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); 2949 assert(["-a","1","2","3","-a","4","5"].parseCLIArgs!T.get.a == [1,2,3,4,5]); 2950 assert(["-b","1","2","3","-b","4","5"].parseCLIArgs!T.get.b == [[1,2,3],[4,5]]); 2951 } 2952 2953 unittest 2954 { 2955 struct T 2956 { 2957 @NamedArgument int[] a; 2958 } 2959 2960 Config cfg; 2961 cfg.arraySep = ','; 2962 2963 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T([1,2,3,4,5])); 2964 } 2965 2966 unittest 2967 { 2968 struct T 2969 { 2970 @NamedArgument int[string] a; 2971 } 2972 2973 static assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); 2974 assert(["-a=foo=3","-a","boo=7"].parseCLIArgs!T.get.a == ["foo":3,"boo":7]); 2975 } 2976 2977 unittest 2978 { 2979 struct T 2980 { 2981 @NamedArgument int[string] a; 2982 } 2983 2984 Config cfg; 2985 cfg.arraySep = ','; 2986 2987 assert(["-a=foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); 2988 assert(["-a","foo=3,boo=7"].parseCLIArgs!T(cfg).get.a == ["foo":3,"boo":7]); 2989 } 2990 2991 unittest 2992 { 2993 struct T 2994 { 2995 enum Fruit { apple, pear }; 2996 2997 @NamedArgument Fruit a; 2998 } 2999 3000 static assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); 3001 static assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); 3002 assert(["-a","apple"].parseCLIArgs!T.get == T(T.Fruit.apple)); 3003 assert(["-a=pear"].parseCLIArgs!T.get == T(T.Fruit.pear)); 3004 } 3005 3006 unittest 3007 { 3008 struct T 3009 { 3010 @NamedArgument string[] a; 3011 } 3012 3013 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T.get == T(["1,2,3","4","5"])); 3014 3015 Config cfg; 3016 cfg.arraySep = ','; 3017 3018 assert(["-a","1,2,3","-a","4","5"].parseCLIArgs!T(cfg).get == T(["1","2","3","4","5"])); 3019 } 3020 3021 unittest 3022 { 3023 struct T 3024 { 3025 @(NamedArgument.AllowNoValue !10) int a; 3026 @(NamedArgument.RequireNoValue!20) int b; 3027 } 3028 3029 static assert(["-a"].parseCLIArgs!T.get.a == 10); 3030 static assert(["-b"].parseCLIArgs!T.get.b == 20); 3031 static assert(["-a", "30"].parseCLIArgs!T.get.a == 30); 3032 assert(["-a"].parseCLIArgs!T.get.a == 10); 3033 assert(["-b"].parseCLIArgs!T.get.b == 20); 3034 assert(["-a", "30"].parseCLIArgs!T.get.a == 30); 3035 assert(["-b", "30"].parseCLIArgs!T.isNull); 3036 } 3037 3038 unittest 3039 { 3040 struct T 3041 { 3042 @(NamedArgument 3043 .PreValidation!((string s) { return s.length > 1 && s[0] == '!'; }) 3044 .Parse !((string s) { return s[1]; }) 3045 .Validation !((char v) { return v >= '0' && v <= '9'; }) 3046 .Action !((ref int a, char v) { a = v - '0'; }) 3047 ) 3048 int a; 3049 } 3050 3051 static assert(["-a","!4"].parseCLIArgs!T.get.a == 4); 3052 assert(["-a","!4"].parseCLIArgs!T.get.a == 4); 3053 } 3054 3055 unittest 3056 { 3057 static struct T 3058 { 3059 int a; 3060 3061 @(NamedArgument("a")) void foo() { a++; } 3062 } 3063 3064 static assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); 3065 assert(["-a","-a","-a","-a"].parseCLIArgs!T.get.a == 4); 3066 } 3067 3068 3069 private string getProgramName() 3070 { 3071 import core.runtime: Runtime; 3072 import std.path: baseName; 3073 return Runtime.args[0].baseName; 3074 } 3075 3076 unittest 3077 { 3078 assert(getProgramName().length > 0); 3079 } 3080 3081 3082 private struct CommandInfo 3083 { 3084 string[] names = [""]; 3085 string usage; 3086 string description; 3087 string shortDescription; 3088 string epilog; 3089 3090 auto ref Usage(string text) 3091 { 3092 usage = text; 3093 return this; 3094 } 3095 3096 auto ref Description(string text) 3097 { 3098 description = text; 3099 return this; 3100 } 3101 3102 auto ref ShortDescription(string text) 3103 { 3104 shortDescription = text; 3105 return this; 3106 } 3107 3108 auto ref Epilog(string text) 3109 { 3110 epilog = text; 3111 return this; 3112 } 3113 } 3114 3115 auto Command(string[] name...) 3116 { 3117 return CommandInfo(name); 3118 } 3119 3120 unittest 3121 { 3122 auto a = Command("MYPROG").Usage("usg").Description("desc").ShortDescription("sum").Epilog("epi"); 3123 assert(a.names == ["MYPROG"]); 3124 assert(a.usage == "usg"); 3125 assert(a.description == "desc"); 3126 assert(a.shortDescription == "sum"); 3127 assert(a.epilog == "epi"); 3128 } 3129 3130 private mixin template ForwardMemberFunction(string dest) 3131 { 3132 import std.array: split; 3133 mixin("auto "~dest.split('.')[$-1]~"(Args...)(auto ref Args args) inout { import core.lifetime: forward; return "~dest~"(forward!args); }"); 3134 } 3135 3136 3137 private struct CommandArguments(RECEIVER) 3138 { 3139 static assert(getSymbolsByUDA!(RECEIVER, TrailingArguments).length <= 1, 3140 "Type "~RECEIVER.stringof~" must have at most one 'TrailingArguments' UDA"); 3141 3142 private enum _validate = checkArgumentNames!RECEIVER && 3143 checkPositionalIndexes!RECEIVER; 3144 3145 static assert(getUDAs!(RECEIVER, CommandInfo).length <= 1); 3146 3147 CommandInfo info; 3148 const(string)[] parentNames; 3149 3150 Arguments arguments; 3151 3152 ParseFunction!RECEIVER[] parseFunctions; 3153 3154 uint level; // (sub-)command level, 0 = top level 3155 3156 // sub commands 3157 size_t[string] subCommandsByName; 3158 CommandInfo[] subCommands; 3159 ParseSubCommandFunction!RECEIVER[] parseSubCommands; 3160 3161 mixin ForwardMemberFunction!"arguments.findPositionalArgument"; 3162 mixin ForwardMemberFunction!"arguments.findNamedArgument"; 3163 mixin ForwardMemberFunction!"arguments.checkRestrictions"; 3164 3165 3166 3167 private this(in Config config) 3168 { 3169 static if(getUDAs!(RECEIVER, CommandInfo).length > 0) 3170 CommandInfo info = getUDAs!(RECEIVER, CommandInfo)[0]; 3171 else 3172 CommandInfo info; 3173 3174 this(config, info); 3175 } 3176 3177 private this(PARENT = void)(in Config config, CommandInfo info, const PARENT* parentArguments = null) 3178 { 3179 this.info = info; 3180 3181 checkArgumentName!RECEIVER(config.namedArgChar); 3182 3183 static if(is(PARENT == void)) 3184 { 3185 level = 0; 3186 arguments = Arguments(config.caseSensitive); 3187 } 3188 else 3189 { 3190 parentNames = parentArguments.parentNames ~ parentArguments.info.names[0]; 3191 level = parentArguments.level + 1; 3192 arguments = Arguments(config.caseSensitive, &parentArguments.arguments); 3193 } 3194 3195 fillArguments(); 3196 3197 if(config.addHelp) 3198 { 3199 arguments.addArgument!helpArgument; 3200 parseFunctions ~= delegate (in Config config, string argName, ref RECEIVER receiver, string rawValue, ref string[] rawArgs) 3201 { 3202 import std.stdio: stdout; 3203 3204 printHelp(stdout.lockingTextWriter(), this, config); 3205 3206 return Result(0); 3207 }; 3208 } 3209 } 3210 3211 private void fillArguments() 3212 { 3213 enum hasNoUDAs = getSymbolsByUDA!(RECEIVER, ArgumentUDA ).length == 0 && 3214 getSymbolsByUDA!(RECEIVER, NamedArgument).length == 0 && 3215 getSymbolsByUDA!(RECEIVER, SubCommands ).length == 0; 3216 3217 static foreach(sym; __traits(allMembers, RECEIVER)) 3218 {{ 3219 alias mem = __traits(getMember, RECEIVER, sym); 3220 3221 static if(!is(mem)) // skip types 3222 { 3223 static if(hasUDA!(mem, ArgumentUDA) || hasUDA!(mem, NamedArgument)) 3224 addArgument!sym; 3225 else static if(hasUDA!(mem, SubCommands)) 3226 addSubCommands!sym; 3227 else static if(hasNoUDAs && 3228 // skip "op*" functions 3229 !(is(typeof(mem) == function) && sym.length > 2 && sym[0..2] == "op")) 3230 { 3231 import std.sumtype: isSumType; 3232 3233 static if(isSumType!(typeof(mem))) 3234 addSubCommands!sym; 3235 else 3236 addArgument!sym; 3237 } 3238 } 3239 }} 3240 } 3241 3242 private void addArgument(alias symbol)() 3243 { 3244 alias member = __traits(getMember, RECEIVER, symbol); 3245 3246 static assert(getUDAs!(member, ArgumentUDA).length <= 1, 3247 "Member "~RECEIVER.stringof~"."~symbol~" has multiple '*Argument' UDAs"); 3248 3249 static assert(getUDAs!(member, Group).length <= 1, 3250 "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'Group' UDAs"); 3251 3252 static if(getUDAs!(member, ArgumentUDA).length > 0) 3253 enum uda = getUDAs!(member, ArgumentUDA)[0]; 3254 else 3255 enum uda = NamedArgument(); 3256 3257 enum info = setDefaults!(uda.info, typeof(member), symbol); 3258 3259 enum restrictions = { 3260 RestrictionGroup[] restrictions; 3261 static foreach(gr; getUDAs!(member, RestrictionGroup)) 3262 restrictions ~= gr; 3263 return restrictions; 3264 }(); 3265 3266 static if(getUDAs!(member, Group).length > 0) 3267 arguments.addArgument!(info, restrictions, getUDAs!(member, Group)[0]); 3268 else 3269 arguments.addArgument!(info, restrictions); 3270 3271 parseFunctions ~= ParsingFunction!(symbol, uda, info, RECEIVER); 3272 } 3273 3274 private void addSubCommands(alias symbol)() 3275 { 3276 import std.sumtype: isSumType; 3277 3278 alias member = __traits(getMember, RECEIVER, symbol); 3279 3280 static assert(isSumType!(typeof(member)), RECEIVER.stringof~"."~symbol~" must have 'SumType' type"); 3281 3282 static assert(getUDAs!(member, SubCommands).length <= 1, 3283 "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'SubCommands' UDAs"); 3284 3285 static foreach(TYPE; typeof(member).Types) 3286 {{ 3287 enum defaultCommand = is(TYPE == Default!COMMAND_TYPE, COMMAND_TYPE); 3288 static if(!defaultCommand) 3289 alias COMMAND_TYPE = TYPE; 3290 3291 static assert(getUDAs!(COMMAND_TYPE, CommandInfo).length <= 1); 3292 3293 //static assert(getUDAs!(member, Group).length <= 1, 3294 // "Member "~RECEIVER.stringof~"."~symbol~" has multiple 'Group' UDAs"); 3295 3296 static if(getUDAs!(COMMAND_TYPE, CommandInfo).length > 0) 3297 enum info = getUDAs!(COMMAND_TYPE, CommandInfo)[0]; 3298 else 3299 enum info = CommandInfo([COMMAND_TYPE.stringof]); 3300 3301 static assert(info.names.length > 0 && info.names[0].length > 0); 3302 3303 //static if(getUDAs!(member, Group).length > 0) 3304 // args.addArgument!(info, restrictions, getUDAs!(member, Group)[0])(ParsingFunction!(symbol, uda, info, RECEIVER)); 3305 //else 3306 //arguments.addSubCommand!(info); 3307 3308 immutable index = subCommands.length; 3309 3310 static foreach(name; info.names) 3311 { 3312 assert(!(name in subCommandsByName), "Duplicated name of subcommand: "~name); 3313 subCommandsByName[arguments.convertCase(name)] = index; 3314 } 3315 3316 static if(defaultCommand) 3317 { 3318 assert(!(DEFAULT_COMMAND in subCommandsByName), "Multiple default subcommands: "~RECEIVER.stringof~"."~symbol); 3319 subCommandsByName[DEFAULT_COMMAND] = index; 3320 } 3321 3322 subCommands ~= info; 3323 //group.arguments ~= index; 3324 parseSubCommands ~= ParsingSubCommand!(TYPE, info, RECEIVER, symbol)(&this); 3325 }} 3326 } 3327 3328 auto findSubCommand(string name) const 3329 { 3330 struct Result 3331 { 3332 uint level = uint.max; 3333 ParseSubCommandFunction!RECEIVER parse; 3334 } 3335 3336 auto p = arguments.convertCase(name) in subCommandsByName; 3337 return !p ? Result.init : Result(level+1, parseSubCommands[*p]); 3338 } 3339 3340 static if(getSymbolsByUDA!(RECEIVER, TrailingArguments).length == 1) 3341 { 3342 private void setTrailingArgs(ref RECEIVER receiver, string[] rawValues) const 3343 { 3344 enum symbol = __traits(identifier, getSymbolsByUDA!(RECEIVER, TrailingArguments)[0]); 3345 auto target = &__traits(getMember, receiver, symbol); 3346 3347 static if(__traits(compiles, { *target = rawValues; })) 3348 *target = rawValues; 3349 else 3350 static assert(false, "Type '"~typeof(*target).stringof~"' of `"~ 3351 RECEIVER.stringof~"."~symbol~"` is not supported for 'TrailingArguments' UDA"); 3352 } 3353 } 3354 } 3355 3356 //////////////////////////////////////////////////////////////////////////////////////////////////// 3357 // Help-printing functions 3358 //////////////////////////////////////////////////////////////////////////////////////////////////// 3359 private void printValue(Output)(auto ref Output output, in ArgumentInfo info) 3360 { 3361 if(info.maxValuesCount.get == 0) 3362 return; 3363 3364 if(info.minValuesCount.get == 0) 3365 output.put('['); 3366 3367 output.put(info.placeholder); 3368 if(info.maxValuesCount.get > 1) 3369 output.put(" ..."); 3370 3371 if(info.minValuesCount.get == 0) 3372 output.put(']'); 3373 } 3374 3375 unittest 3376 { 3377 auto test(int min, int max) 3378 { 3379 ArgumentInfo info; 3380 info.placeholder = "v"; 3381 info.minValuesCount = min; 3382 info.maxValuesCount = max; 3383 3384 import std.array: appender; 3385 auto a = appender!string; 3386 a.printValue(info); 3387 return a[]; 3388 } 3389 3390 assert(test(0,0) == ""); 3391 assert(test(0,1) == "[v]"); 3392 assert(test(0,5) == "[v ...]"); 3393 assert(test(1,1) == "v"); 3394 assert(test(1,5) == "v ..."); 3395 assert(test(3,3) == "v ..."); 3396 assert(test(3,5) == "v ..."); 3397 } 3398 3399 3400 private string getArgumentName(string name, in Config config) 3401 { 3402 name = config.namedArgChar ~ name; 3403 return name.length > 2 ? config.namedArgChar ~ name : name; 3404 } 3405 3406 unittest 3407 { 3408 assert(getArgumentName("f", Config.init) == "-f"); 3409 assert(getArgumentName("foo", Config.init) == "--foo"); 3410 } 3411 3412 3413 private void printInvocation(Output)(auto ref Output output, in ArgumentInfo info, in string[] names, in Config config) 3414 { 3415 if(info.positional) 3416 output.printValue(info); 3417 else 3418 { 3419 import std.algorithm: each; 3420 3421 names.each!((i, name) 3422 { 3423 if(i > 0) 3424 output.put(", "); 3425 3426 output.put(getArgumentName(name, config)); 3427 3428 if(info.maxValuesCount.get > 0) 3429 { 3430 output.put(' '); 3431 output.printValue(info); 3432 } 3433 }); 3434 } 3435 } 3436 3437 unittest 3438 { 3439 auto test(bool positional)() 3440 { 3441 enum info = { 3442 ArgumentInfo info; 3443 info.placeholder = "v"; 3444 static if (positional) 3445 info.position = 0; 3446 return info; 3447 }(); 3448 3449 import std.array: appender; 3450 auto a = appender!string; 3451 a.printInvocation(setDefaults!(info, int, "foo"), ["f","foo"], Config.init); 3452 return a[]; 3453 } 3454 3455 assert(test!false == "-f v, --foo v"); 3456 assert(test!true == "v"); 3457 } 3458 3459 3460 private void printUsage(Output)(auto ref Output output, in ArgumentInfo info, in Config config) 3461 { 3462 if(!info.required) 3463 output.put('['); 3464 3465 output.printInvocation(info, [info.names[0]], config); 3466 3467 if(!info.required) 3468 output.put(']'); 3469 } 3470 3471 unittest 3472 { 3473 auto test(bool required, bool positional)() 3474 { 3475 enum info = { 3476 ArgumentInfo info; 3477 info.names ~= "foo"; 3478 info.placeholder = "v"; 3479 info.required = required; 3480 static if (positional) 3481 info.position = 0; 3482 return info; 3483 }(); 3484 3485 import std.array: appender; 3486 auto a = appender!string; 3487 a.printUsage(setDefaults!(info, int, "foo"), Config.init); 3488 return a[]; 3489 } 3490 3491 assert(test!(false, false) == "[--foo v]"); 3492 assert(test!(false, true) == "[v]"); 3493 assert(test!(true, false) == "--foo v"); 3494 assert(test!(true, true) == "v"); 3495 } 3496 3497 3498 private void substituteProg(Output)(auto ref Output output, string text, string prog) 3499 { 3500 import std.array: replaceInto; 3501 output.replaceInto(text, "%(PROG)", prog); 3502 } 3503 3504 unittest 3505 { 3506 import std.array: appender; 3507 auto a = appender!string; 3508 a.substituteProg("this is some text where %(PROG) is substituted but PROG and prog are not", "-myprog-"); 3509 assert(a[] == "this is some text where -myprog- is substituted but PROG and prog are not"); 3510 } 3511 3512 3513 private string spaces(ulong num) 3514 { 3515 import std.range: repeat; 3516 import std.array: array; 3517 return ' '.repeat(num).array; 3518 } 3519 3520 unittest 3521 { 3522 assert(spaces(0) == ""); 3523 assert(spaces(1) == " "); 3524 assert(spaces(5) == " "); 3525 } 3526 3527 private void printUsage(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 3528 { 3529 import std.algorithm: map; 3530 import std.array: join; 3531 3532 string progName = (cmd.parentNames ~ cmd.info.names[0]).map!(_ => _.length > 0 ? _ : getProgramName()).join(" "); 3533 3534 output.put("Usage: "); 3535 3536 if(cmd.info.usage.length > 0) 3537 substituteProg(output, cmd.info.usage, progName); 3538 else 3539 { 3540 import std.algorithm: filter, each, map; 3541 3542 alias print = (r) => r 3543 .filter!((ref _) => !_.hideFromHelp) 3544 .each!((ref _) 3545 { 3546 output.put(' '); 3547 output.printUsage(_, config); 3548 }); 3549 3550 output.put(progName); 3551 3552 // named args 3553 print(cmd.arguments.arguments.filter!((ref _) => !_.positional)); 3554 // positional args 3555 print(cmd.arguments.positionalArguments.map!(ref (_) => cmd.arguments.arguments[_])); 3556 // sub commands 3557 if(cmd.subCommands.length > 0) 3558 output.put(" <command> [<args>]"); 3559 } 3560 3561 output.put('\n'); 3562 } 3563 3564 void printUsage(T, Output)(auto ref Output output, in Config config) 3565 { 3566 printUsage(output, CommandArguments!T(config), config); 3567 } 3568 3569 unittest 3570 { 3571 @(Command("MYPROG").Usage("custom usage of %(PROG)")) 3572 struct T 3573 { 3574 string s; 3575 } 3576 3577 auto test(string usage) 3578 { 3579 import std.array: appender; 3580 3581 auto a = appender!string; 3582 a.printUsage!T(Config.init); 3583 return a[]; 3584 } 3585 3586 enum expected = "Usage: custom usage of MYPROG\n"; 3587 static assert(test("custom usage of %(PROG)") == expected); 3588 assert(test("custom usage of %(PROG)") == expected); 3589 } 3590 3591 3592 private void printHelp(Output, ARGS)(auto ref Output output, in Group group, ARGS args, int helpPosition) 3593 { 3594 import std.string: leftJustify; 3595 3596 if(group.arguments.length == 0 || group.name.length == 0) 3597 return; 3598 3599 alias printDescription = { 3600 output.put(group.name); 3601 output.put(":\n"); 3602 3603 if (group.description.length > 0) 3604 { 3605 output.put(" "); 3606 output.put(group.description); 3607 output.put("\n\n"); 3608 } 3609 }; 3610 bool descriptionIsPrinted = false; 3611 3612 immutable ident = spaces(helpPosition + 2); 3613 3614 foreach(idx; group.arguments) 3615 { 3616 auto arg = &args[idx]; 3617 3618 if(arg.invocation.length == 0) 3619 continue; 3620 3621 if(!descriptionIsPrinted) 3622 { 3623 printDescription(); 3624 descriptionIsPrinted = true; 3625 } 3626 3627 if(arg.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 3628 { 3629 import std.array: appender; 3630 3631 auto invocation = appender!string; 3632 invocation ~= " "; 3633 invocation ~= arg.invocation.leftJustify(helpPosition); 3634 output.wrapMutiLine(arg.help, 80-2, invocation[], ident); 3635 } 3636 else 3637 { 3638 // long action name; start on the next line 3639 output.put(" "); 3640 output.put(arg.invocation); 3641 output.put("\n"); 3642 output.wrapMutiLine(arg.help, 80-2, ident, ident); 3643 } 3644 } 3645 3646 output.put('\n'); 3647 } 3648 3649 3650 private void printHelp(Output)(auto ref Output output, in Arguments arguments, in Config config, bool helpArgIsPrinted = false) 3651 { 3652 import std.algorithm: map, maxElement, min; 3653 import std.array: appender, array; 3654 3655 // pre-compute the output 3656 auto args = 3657 arguments.arguments 3658 .map!((ref _) 3659 { 3660 struct Result 3661 { 3662 string invocation, help; 3663 } 3664 3665 if(_.hideFromHelp) 3666 return Result.init; 3667 3668 if(isHelpArgument(_.names[0])) 3669 { 3670 if(helpArgIsPrinted) 3671 return Result.init; 3672 3673 helpArgIsPrinted = true; 3674 } 3675 3676 auto invocation = appender!string; 3677 invocation.printInvocation(_, _.names, config); 3678 3679 return Result(invocation[], _.description); 3680 }).array; 3681 3682 immutable maxInvocationWidth = args.map!(_ => _.invocation.length).maxElement; 3683 immutable helpPosition = min(maxInvocationWidth + 4, 24); 3684 3685 //user-defined groups 3686 foreach(ref group; arguments.groups[2..$]) 3687 output.printHelp(group, args, helpPosition); 3688 3689 //required args 3690 output.printHelp(arguments.requiredGroup, args, helpPosition); 3691 3692 //optionals args 3693 output.printHelp(arguments.optionalGroup, args, helpPosition); 3694 3695 if(arguments.parentArguments) 3696 output.printHelp(*arguments.parentArguments, config, helpArgIsPrinted); 3697 } 3698 3699 private void printHelp(Output)(auto ref Output output, in CommandInfo[] commands, in Config config) 3700 { 3701 import std.algorithm: map, maxElement, min; 3702 import std.array: appender, array, join; 3703 3704 if(commands.length == 0) 3705 return; 3706 3707 output.put("Available commands:\n"); 3708 3709 // pre-compute the output 3710 auto cmds = commands 3711 .map!((ref _) 3712 { 3713 struct Result 3714 { 3715 string invocation, help; 3716 } 3717 3718 //if(_.hideFromHelp) 3719 // return Result.init; 3720 3721 return Result(_.names.join(","), _.shortDescription); 3722 }).array; 3723 3724 immutable maxInvocationWidth = cmds.map!(_ => _.invocation.length).maxElement; 3725 immutable helpPosition = min(maxInvocationWidth + 4, 24); 3726 3727 3728 immutable ident = spaces(helpPosition + 2); 3729 3730 foreach(const ref cmd; cmds) 3731 { 3732 if(cmd.invocation.length == 0) 3733 continue; 3734 3735 if(cmd.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 3736 { 3737 import std.array: appender; 3738 import std.string: leftJustify; 3739 3740 auto invocation = appender!string; 3741 invocation ~= " "; 3742 invocation ~= cmd.invocation.leftJustify(helpPosition); 3743 output.wrapMutiLine(cmd.help, 80-2, invocation[], ident); 3744 } 3745 else 3746 { 3747 // long action name; start on the next line 3748 output.put(" "); 3749 output.put(cmd.invocation); 3750 output.put("\n"); 3751 output.wrapMutiLine(cmd.help, 80-2, ident, ident); 3752 } 3753 } 3754 3755 output.put('\n'); 3756 } 3757 3758 3759 private void printHelp(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 3760 { 3761 printUsage(output, cmd, config); 3762 output.put('\n'); 3763 3764 if(cmd.info.description.length > 0) 3765 { 3766 output.put(cmd.info.description); 3767 output.put("\n\n"); 3768 } 3769 3770 // sub commands 3771 output.printHelp(cmd.subCommands, config); 3772 3773 output.printHelp(cmd.arguments, config); 3774 3775 if(cmd.info.epilog.length > 0) 3776 { 3777 output.put(cmd.info.epilog); 3778 output.put('\n'); 3779 } 3780 } 3781 3782 void printHelp(T, Output)(auto ref Output output, in Config config) 3783 { 3784 printHelp(output, CommandArguments!T(config), config); 3785 } 3786 3787 unittest 3788 { 3789 @(Command("MYPROG") 3790 .Description("custom description") 3791 .Epilog("custom epilog") 3792 ) 3793 struct T 3794 { 3795 @NamedArgument string s; 3796 @(NamedArgument.Placeholder("VALUE")) string p; 3797 3798 @(NamedArgument.HideFromHelp()) string hidden; 3799 3800 enum Fruit { apple, pear }; 3801 @(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; 3802 3803 @(NamedArgument.AllowedValues!([1,4,16,8])) int i; 3804 3805 @(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; 3806 @(PositionalArgument(1).AllowedValues!(["q","a"])) string param1; 3807 3808 @TrailingArguments string[] args; 3809 } 3810 3811 auto test(alias func)() 3812 { 3813 import std.array: appender; 3814 3815 auto a = appender!string; 3816 func!T(a, Config.init); 3817 return a[]; 3818 } 3819 static assert(test!printUsage.length > 0); // ensure that it works at compile time 3820 static assert(test!printHelp .length > 0); // ensure that it works at compile time 3821 3822 assert(test!printUsage == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n"); 3823 assert(test!printHelp == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~ 3824 "custom description\n\n"~ 3825 "Required arguments:\n"~ 3826 " -f {apple,pear}, --fruit {apple,pear}\n"~ 3827 " This is a help text for fruit. Very very very very\n"~ 3828 " very very very very very very very very very very\n"~ 3829 " very very very very very long text\n"~ 3830 " param0 This is a help text for param0. Very very very very\n"~ 3831 " very very very very very very very very very very\n"~ 3832 " very very very very very long text\n"~ 3833 " {q,a} \n\n"~ 3834 "Optional arguments:\n"~ 3835 " -s S \n"~ 3836 " -p VALUE \n"~ 3837 " -i {1,4,16,8} \n"~ 3838 " -h, --help Show this help message and exit\n\n"~ 3839 "custom epilog\n"); 3840 } 3841 3842 unittest 3843 { 3844 @Command("MYPROG") 3845 struct T 3846 { 3847 @(ArgumentGroup("group1").Description("group1 description")) 3848 { 3849 @NamedArgument 3850 { 3851 string a; 3852 string b; 3853 } 3854 @PositionalArgument(0) string p; 3855 } 3856 3857 @(ArgumentGroup("group2").Description("group2 description")) 3858 @NamedArgument 3859 { 3860 string c; 3861 string d; 3862 } 3863 @PositionalArgument(1) string q; 3864 } 3865 3866 auto test(alias func)() 3867 { 3868 import std.array: appender; 3869 3870 auto a = appender!string; 3871 func!T(a, Config.init); 3872 return a[]; 3873 } 3874 3875 assert(test!printHelp == "Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q\n\n"~ 3876 "group1:\n"~ 3877 " group1 description\n\n"~ 3878 " -a A \n"~ 3879 " -b B \n"~ 3880 " p \n\n"~ 3881 "group2:\n"~ 3882 " group2 description\n\n"~ 3883 " -c C \n"~ 3884 " -d D \n\n"~ 3885 "Required arguments:\n"~ 3886 " q \n\n"~ 3887 "Optional arguments:\n"~ 3888 " -h, --help Show this help message and exit\n\n"); 3889 } 3890 3891 unittest 3892 { 3893 import std.sumtype: SumType, match; 3894 3895 @Command("MYPROG") 3896 struct T 3897 { 3898 @(Command("cmd1").ShortDescription("Perform cmd 1")) 3899 struct CMD1 3900 { 3901 string a; 3902 } 3903 @(Command("very-long-command-name-2").ShortDescription("Perform cmd 2")) 3904 struct CMD2 3905 { 3906 string b; 3907 } 3908 3909 string c; 3910 string d; 3911 3912 SumType!(CMD1, CMD2) cmd; 3913 } 3914 3915 auto test(alias func)() 3916 { 3917 import std.array: appender; 3918 3919 auto a = appender!string; 3920 func!T(a, Config.init); 3921 return a[]; 3922 } 3923 3924 assert(test!printHelp == "Usage: MYPROG [-c C] [-d D] [-h] <command> [<args>]\n\n"~ 3925 "Available commands:\n"~ 3926 " cmd1 Perform cmd 1\n"~ 3927 " very-long-command-name-2\n"~ 3928 " Perform cmd 2\n\n"~ 3929 "Optional arguments:\n"~ 3930 " -c C \n"~ 3931 " -d D \n"~ 3932 " -h, --help Show this help message and exit\n\n"); 3933 } 3934 3935 unittest 3936 { 3937 @Command("MYPROG") 3938 struct T 3939 { 3940 @(NamedArgument.HideFromHelp()) string s; 3941 } 3942 3943 assert(parseCLIArgs!T(["-h","-s","asd"]).isNull()); 3944 assert(parseCLIArgs!T(["-h"], (T t) { assert(false); }) == 0); 3945 3946 auto args = ["-h","-s","asd"]; 3947 assert(parseCLIKnownArgs!T(args).isNull()); 3948 assert(args.length == 3); 3949 assert(parseCLIKnownArgs!T(["-h"], (T t, string[] args) { assert(false); }) == 0); 3950 } 3951 3952 unittest 3953 { 3954 @Command("MYPROG") 3955 struct T 3956 { 3957 @(NamedArgument.Required()) string s; 3958 } 3959 3960 assert(parseCLIArgs!T([], (T t) { assert(false); }) != 0); 3961 } 3962 3963 unittest 3964 { 3965 @Command("MYPROG") 3966 struct T 3967 { 3968 @MutuallyExclusive() 3969 { 3970 string a; 3971 string b; 3972 } 3973 } 3974 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0); 3975 assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0); 3976 assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0); 3977 assert(parseCLIArgs!T([], (T t) {}) == 0); 3978 } 3979 3980 unittest 3981 { 3982 @Command("MYPROG") 3983 struct T 3984 { 3985 @RequiredTogether() 3986 { 3987 string a; 3988 string b; 3989 } 3990 } 3991 assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0); 3992 assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0); 3993 assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0); 3994 assert(parseCLIArgs!T([], (T t) {}) == 0); 3995 } 3996 3997 3998 private void wrapMutiLine(Output, S)(auto ref Output output, 3999 S s, 4000 in size_t columns = 80, 4001 S firstindent = null, 4002 S indent = null, 4003 in size_t tabsize = 8) 4004 if (isSomeString!S) 4005 { 4006 import std.string: wrap, lineSplitter, join; 4007 import std.algorithm: map, copy; 4008 4009 auto lines = s.lineSplitter; 4010 if(lines.empty) 4011 { 4012 output.put(firstindent); 4013 output.put("\n"); 4014 return; 4015 } 4016 4017 output.put(lines.front.wrap(columns, firstindent, indent, tabsize)); 4018 lines.popFront; 4019 4020 lines.map!(s => s.wrap(columns, indent, indent, tabsize)).copy(output); 4021 } 4022 4023 unittest 4024 { 4025 string test(string s, size_t columns, string firstindent = null, string indent = null) 4026 { 4027 import std.array: appender; 4028 auto a = appender!string; 4029 a.wrapMutiLine(s, columns, firstindent, indent); 4030 return a[]; 4031 } 4032 assert(test("a short string", 7) == "a short\nstring\n"); 4033 assert(test("a\nshort string", 7) == "a\nshort\nstring\n"); 4034 4035 // wrap will not break inside of a word, but at the next space 4036 assert(test("a short string", 4) == "a\nshort\nstring\n"); 4037 4038 assert(test("a short string", 7, "\t") == "\ta\nshort\nstring\n"); 4039 assert(test("a short string", 7, "\t", " ") == "\ta\n short\n string\n"); 4040 }