1 module argparse; 2 3 4 import argparse.internal; 5 import argparse.parser: callParser; 6 7 8 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 /// Public API 10 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 12 struct Config 13 { 14 /** 15 The assignment character used in options with parameters. 16 Defaults to '='. 17 */ 18 char assignChar = '='; 19 20 /** 21 When set to char.init, parameters to array and associative array receivers are 22 treated as an individual argument. That is, only one argument is appended or 23 inserted per appearance of the option switch. If `arraySep` is set to 24 something else, then each parameter is first split by the separator, and the 25 individual pieces are treated as arguments to the same option. 26 27 Defaults to char.init 28 */ 29 char arraySep = char.init; 30 31 /** 32 The option character. 33 Defaults to '-'. 34 */ 35 char namedArgChar = '-'; 36 37 /** 38 The string that conventionally marks the end of all options. 39 Assigning an empty string to `endOfArgs` effectively disables it. 40 Defaults to "--". 41 */ 42 string endOfArgs = "--"; 43 44 /** 45 If set then argument names are case-sensitive. 46 Defaults to true. 47 */ 48 bool caseSensitive = true; 49 50 /** 51 Single-letter arguments can be bundled together, i.e. "-abc" is the same as "-a -b -c". 52 Disabled by default. 53 */ 54 bool bundling = false; 55 56 /** 57 Add a -h/--help option to the parser. 58 Defaults to true. 59 */ 60 bool addHelp = true; 61 62 /** 63 Delegate that processes error messages if they happen during argument parsing. 64 By default all errors are printed to stderr. 65 */ 66 package void delegate(string s) nothrow errorHandlerFunc; 67 68 @property auto errorHandler(void function(string s) nothrow func) 69 { 70 return errorHandlerFunc = (string msg) { func(msg); }; 71 } 72 73 @property auto errorHandler(void delegate(string s) nothrow func) 74 { 75 return errorHandlerFunc = func; 76 } 77 78 79 package void onError(A...)(A args) const nothrow 80 { 81 import std.conv: text; 82 import std.stdio: stderr, writeln; 83 84 try 85 { 86 if(errorHandlerFunc) 87 errorHandlerFunc(text!A(args)); 88 else 89 stderr.writeln("Error: ", args); 90 } 91 catch(Exception e) 92 { 93 throw new Error(e.msg); 94 } 95 } 96 } 97 98 unittest 99 { 100 Config.init.onError("--just testing error func--",1,2.3,false); 101 Config c; 102 c.errorHandler = (string s){}; 103 c.onError("--just testing error func--",1,2.3,false); 104 } 105 106 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 107 108 struct Param(VALUE_TYPE) 109 { 110 const Config config; 111 string name; 112 113 static if(!is(VALUE_TYPE == void)) 114 VALUE_TYPE value; 115 } 116 117 alias RawParam = Param!(string[]); 118 119 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 120 121 struct Result 122 { 123 int resultCode; 124 125 package enum Status { failure, success, unknownArgument }; 126 package Status status; 127 128 package string errorMsg; 129 130 package const(string)[] suggestions; 131 132 package static enum Failure = Result(1, Status.failure); 133 package static enum Success = Result(0, Status.success); 134 package static enum UnknownArgument = Result(0, Status.unknownArgument); 135 136 bool opCast(type)() const if (is(type == bool)) 137 { 138 return status == Status.success; 139 } 140 141 package static auto Error(A...)(A args) 142 { 143 import std.conv: text; 144 import std.stdio: stderr, writeln; 145 146 return Result(1, Status.failure, text!A(args)); 147 } 148 149 version(unittest) 150 { 151 package bool isError(string text) 152 { 153 import std.algorithm: canFind; 154 return (!cast(bool) this) && errorMsg.canFind(text); 155 } 156 } 157 } 158 159 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 160 161 package struct ArgumentInfo 162 { 163 import std.typecons: Nullable; 164 165 package: 166 167 string[] names; 168 169 string description; 170 string placeholder; 171 172 void setAllowedValues(alias names)() 173 { 174 if(placeholder.length == 0) 175 { 176 import std.conv: to; 177 import std.array: join; 178 import std.format: format; 179 placeholder = "{%s}".format(names.to!(string[]).join(',')); 180 } 181 } 182 183 bool hideFromHelp = false; // if true then this argument is not printed on help page 184 185 bool required; 186 187 Nullable!uint position; 188 189 @property bool positional() const { return !position.isNull; } 190 191 Nullable!ulong minValuesCount; 192 Nullable!ulong maxValuesCount; 193 194 auto checkValuesCount(string argName, ulong count) const 195 { 196 immutable min = minValuesCount.get; 197 immutable max = maxValuesCount.get; 198 199 // override for boolean flags 200 if(allowBooleanNegation && count == 1) 201 return Result.Success; 202 203 if(min == max && count != min) 204 { 205 return Result.Error("argument ",argName,": expected ",min,min == 1 ? " value" : " values"); 206 } 207 if(count < min) 208 { 209 return Result.Error("argument ",argName,": expected at least ",min,min == 1 ? " value" : " values"); 210 } 211 if(count > max) 212 { 213 return Result.Error("argument ",argName,": expected at most ",max,max == 1 ? " value" : " values"); 214 } 215 216 return Result.Success; 217 } 218 219 bool allowBooleanNegation = true; 220 bool ignoreInDefaultCommand; 221 } 222 223 224 unittest 225 { 226 auto info(int min, int max) 227 { 228 ArgumentInfo info; 229 info.allowBooleanNegation = false; 230 info.minValuesCount = min; 231 info.maxValuesCount = max; 232 return info; 233 } 234 235 assert(info(2,4).checkValuesCount("", 1).isError("expected at least 2 values")); 236 assert(info(2,4).checkValuesCount("", 2)); 237 assert(info(2,4).checkValuesCount("", 3)); 238 assert(info(2,4).checkValuesCount("", 4)); 239 assert(info(2,4).checkValuesCount("", 5).isError("expected at most 4 values")); 240 241 assert(info(2,2).checkValuesCount("", 1).isError("expected 2 values")); 242 assert(info(2,2).checkValuesCount("", 2)); 243 assert(info(2,2).checkValuesCount("", 3).isError("expected 2 values")); 244 245 assert(info(1,1).checkValuesCount("", 0).isError("expected 1 value")); 246 assert(info(1,1).checkValuesCount("", 1)); 247 assert(info(1,1).checkValuesCount("", 2).isError("expected 1 value")); 248 249 assert(info(0,1).checkValuesCount("", 0)); 250 assert(info(0,1).checkValuesCount("", 1)); 251 assert(info(0,1).checkValuesCount("", 2).isError("expected at most 1 value")); 252 253 assert(info(1,2).checkValuesCount("", 0).isError("expected at least 1 value")); 254 assert(info(1,2).checkValuesCount("", 1)); 255 assert(info(1,2).checkValuesCount("", 2)); 256 assert(info(1,2).checkValuesCount("", 3).isError("expected at most 2 values")); 257 } 258 259 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 260 261 package struct ArgumentUDA(alias ValueParseFunctions) 262 { 263 package ArgumentInfo info; 264 265 package alias parsingFunc = ValueParseFunctions; 266 267 public auto ref Description(string text) 268 { 269 info.description = text; 270 return this; 271 } 272 273 public auto ref HideFromHelp(bool hide = true) 274 { 275 info.hideFromHelp = hide; 276 return this; 277 } 278 279 public auto ref Placeholder(string value) 280 { 281 info.placeholder = value; 282 return this; 283 } 284 285 public auto ref Required() 286 { 287 info.required = true; 288 return this; 289 } 290 291 public auto ref Optional() 292 { 293 info.required = false; 294 return this; 295 } 296 297 public auto ref NumberOfValues(ulong num) 298 { 299 info.minValuesCount = num; 300 info.maxValuesCount = num; 301 return this; 302 } 303 304 public auto ref NumberOfValues(ulong min, ulong max) 305 { 306 info.minValuesCount = min; 307 info.maxValuesCount = max; 308 return this; 309 } 310 311 public auto ref MinNumberOfValues(ulong min) 312 { 313 assert(min <= info.maxValuesCount.get(ulong.max)); 314 315 info.minValuesCount = min; 316 return this; 317 } 318 319 public auto ref MaxNumberOfValues(ulong max) 320 { 321 assert(max >= info.minValuesCount.get(0)); 322 323 info.maxValuesCount = max; 324 return this; 325 } 326 } 327 328 package enum bool isArgumentUDA(T) = (is(typeof(T.info) == ArgumentInfo) && is(T.parsingFunc)); 329 330 unittest 331 { 332 ArgumentUDA!void arg; 333 assert(!arg.info.hideFromHelp); 334 assert(!arg.info.required); 335 assert(arg.info.minValuesCount.isNull); 336 assert(arg.info.maxValuesCount.isNull); 337 338 arg = arg.Description("desc").Placeholder("text"); 339 assert(arg.info.description == "desc"); 340 assert(arg.info.placeholder == "text"); 341 342 arg = arg.HideFromHelp().Required().NumberOfValues(10); 343 assert(arg.info.hideFromHelp); 344 assert(arg.info.required); 345 assert(arg.info.minValuesCount.get == 10); 346 assert(arg.info.maxValuesCount.get == 10); 347 348 arg = arg.Optional().NumberOfValues(20,30); 349 assert(!arg.info.required); 350 assert(arg.info.minValuesCount.get == 20); 351 assert(arg.info.maxValuesCount.get == 30); 352 353 arg = arg.MinNumberOfValues(2).MaxNumberOfValues(3); 354 assert(arg.info.minValuesCount.get == 2); 355 assert(arg.info.maxValuesCount.get == 3); 356 } 357 358 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 359 360 auto PositionalArgument(uint pos) 361 { 362 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo()).Required(); 363 arg.info.position = pos; 364 return arg; 365 } 366 367 auto PositionalArgument(uint pos, string name) 368 { 369 auto arg = ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Required(); 370 arg.info.position = pos; 371 return arg; 372 } 373 374 auto NamedArgument(string[] name...) 375 { 376 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo(name)).Optional(); 377 } 378 379 auto NamedArgument(string name) 380 { 381 return ArgumentUDA!(ValueParseFunctions!(void, void, void, void, void, void))(ArgumentInfo([name])).Optional(); 382 } 383 384 struct TrailingArguments {} 385 386 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 387 388 package struct Group 389 { 390 package string name; 391 package string description; 392 package size_t[] arguments; 393 394 public auto ref Description(string text) 395 { 396 description = text; 397 return this; 398 } 399 400 } 401 402 auto ArgumentGroup(string name) 403 { 404 return Group(name); 405 } 406 407 unittest 408 { 409 auto g = ArgumentGroup("name").Description("description"); 410 assert(g.name == "name"); 411 assert(g.description == "description"); 412 } 413 414 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 415 416 package struct RestrictionGroup 417 { 418 package string location; 419 420 package enum Type { together, exclusive } 421 package Type type; 422 423 package size_t[] arguments; 424 425 package bool required; 426 427 public auto ref Required() 428 { 429 required = true; 430 return this; 431 } 432 } 433 434 auto RequiredTogether(string file=__FILE__, uint line = __LINE__)() 435 { 436 import std.conv: to; 437 return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.together); 438 } 439 440 auto MutuallyExclusive(string file=__FILE__, uint line = __LINE__)() 441 { 442 import std.conv: to; 443 return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.exclusive); 444 } 445 446 unittest 447 { 448 auto t = RequiredTogether(); 449 assert(t.location.length > 0); 450 assert(t.type == RestrictionGroup.Type.together); 451 452 auto e = MutuallyExclusive(); 453 assert(e.location.length > 0); 454 assert(e.type == RestrictionGroup.Type.exclusive); 455 } 456 457 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 458 459 struct SubCommands {} 460 461 // Default subcommand 462 struct Default(COMMAND) 463 { 464 COMMAND command; 465 alias command this; 466 } 467 468 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 469 470 package struct CommandInfo 471 { 472 package string[] names = [""]; 473 package string usage; 474 package string description; 475 package string shortDescription; 476 package string epilog; 477 478 public auto ref Usage(string text) 479 { 480 usage = text; 481 return this; 482 } 483 484 public auto ref Description(string text) 485 { 486 description = text; 487 return this; 488 } 489 490 public auto ref ShortDescription(string text) 491 { 492 shortDescription = text; 493 return this; 494 } 495 496 public auto ref Epilog(string text) 497 { 498 epilog = text; 499 return this; 500 } 501 } 502 503 unittest 504 { 505 CommandInfo c; 506 c = c.Usage("usg").Description("desc").ShortDescription("sum").Epilog("epi"); 507 assert(c.names == [""]); 508 assert(c.usage == "usg"); 509 assert(c.description == "desc"); 510 assert(c.shortDescription == "sum"); 511 assert(c.epilog == "epi"); 512 } 513 514 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 515 516 517 unittest 518 { 519 struct T 520 { 521 @NamedArgument 522 int a; 523 @(NamedArgument.Optional()) 524 int b; 525 @(NamedArgument.Required()) 526 int c; 527 @NamedArgument 528 int d; 529 @(NamedArgument.Required()) 530 int e; 531 @NamedArgument 532 int f; 533 } 534 535 enum config = { 536 Config config; 537 config.addHelp = false; 538 return config; 539 }(); 540 541 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 542 543 auto a = CommandArguments!T(config); 544 assert(a.arguments.requiredGroup.arguments == [2,4]); 545 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 546 assert(a.arguments.argsPositional == []); 547 } 548 549 unittest 550 { 551 struct T 552 { 553 int a,b,c,d,e,f; 554 } 555 556 enum config = { 557 Config config; 558 config.addHelp = false; 559 return config; 560 }(); 561 562 static assert(CommandArguments!T(config).arguments.arguments.length == 6); 563 564 auto a = CommandArguments!T(config); 565 assert(a.arguments.requiredGroup.arguments == []); 566 assert(a.arguments.argsNamed == ["a":0LU, "b":1LU, "c":2LU, "d":3LU, "e":4LU, "f":5LU]); 567 assert(a.arguments.argsPositional == []); 568 } 569 570 unittest 571 { 572 struct T1 573 { 574 @(NamedArgument("1")) 575 @(NamedArgument("2")) 576 int a; 577 } 578 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 579 580 struct T2 581 { 582 @(NamedArgument("1")) 583 int a; 584 @(NamedArgument("1")) 585 int b; 586 } 587 static assert(!__traits(compiles, { CommandArguments!T1(Config.init); })); 588 589 struct T3 590 { 591 @(PositionalArgument(0)) int a; 592 @(PositionalArgument(0)) int b; 593 } 594 static assert(!__traits(compiles, { CommandArguments!T3(Config.init); })); 595 596 struct T4 597 { 598 @(PositionalArgument(0)) int a; 599 @(PositionalArgument(2)) int b; 600 } 601 static assert(!__traits(compiles, { CommandArguments!T4(Config.init); })); 602 } 603 604 unittest 605 { 606 import std.exception; 607 608 struct T 609 { 610 @(NamedArgument("--")) 611 int a; 612 } 613 static assert(!__traits(compiles, { enum p = CLI!T.parseArgs!((T t){})([]); })); 614 assertThrown(CLI!T.parseArgs!((T t){})([])); 615 } 616 617 unittest 618 { 619 620 import std.conv; 621 import std.traits; 622 623 struct params 624 { 625 int no_a; 626 627 @(PositionalArgument(0, "a") 628 .Description("Argument 'a'") 629 .Validation!((int a) { return a > 3;}) 630 .PreValidation!((string s) { return s.length > 0;}) 631 .Validation!((int a) { return a > 0;}) 632 ) 633 int a; 634 635 int no_b; 636 637 @(NamedArgument(["b", "boo"]).Description("Flag boo") 638 .AllowNoValue!55 639 ) 640 int b; 641 642 int no_c; 643 } 644 645 enum p = CommandArguments!params(Config.init); 646 static assert(p.findNamedArgument("a").arg is null); 647 static assert(p.findNamedArgument("b").arg !is null); 648 static assert(p.findNamedArgument("boo").arg !is null); 649 static assert(p.findPositionalArgument(0).arg !is null); 650 static assert(p.findPositionalArgument(1).arg is null); 651 static assert(p.getParseFunction!false(p.findNamedArgument("b").index) !is null); 652 static assert(p.getParseFunction!true(p.findNamedArgument("b").index) !is null); 653 } 654 655 unittest 656 { 657 import std.typecons : tuple; 658 659 struct T 660 { 661 string a; 662 string b; 663 } 664 665 assert(CLI!T.parseArgs!((T t, string[] args) { 666 assert(t == T("A")); 667 assert(args == []); 668 return 12345; 669 })(["-a","A","--"]) == 12345); 670 assert(CLI!T.parseArgs!((T t, string[] args) { 671 assert(t == T("A")); 672 assert(args == []); 673 return 12345; 674 })(["-a","A","--"]) == 12345); 675 676 { 677 T args; 678 679 assert(CLI!T.parseArgs(args, [ "-a", "A"])); 680 assert(CLI!T.parseArgs(args, [ "-b", "B"])); 681 682 assert(args == T("A","B")); 683 } 684 } 685 686 unittest 687 { 688 struct T 689 { 690 string a; 691 } 692 693 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-g"]) != 0); 694 assert(CLI!T.parseArgs!((T t) { assert(t == T.init); return 12345; })([]) == 12345); 695 assert(CLI!T.parseArgs!((T t, string[] args) { 696 assert(t == T.init); 697 assert(args.length == 0); 698 return 12345; 699 })([]) == 12345); 700 assert(CLI!T.parseArgs!((T t, string[] args) { 701 assert(t == T("aa")); 702 assert(args == ["-g"]); 703 return 12345; 704 })(["-a","aa","-g"]) == 12345); 705 static assert(CLI!T.parseArgs!((T t, string[] args) { 706 assert(t == T.init); 707 assert(args.length == 0); 708 return 12345; 709 })([]) == 12345); 710 static assert(CLI!T.parseArgs!((T t, string[] args) { 711 assert(t == T("aa")); 712 assert(args == ["-g"]); 713 return 12345; 714 })(["-a","aa","-g"]) == 12345); 715 } 716 717 unittest 718 { 719 void test(string[] args, alias expected)() 720 { 721 assert(CLI!(typeof(expected)).parseArgs!((t) { 722 assert(t == expected); 723 return 12345; 724 })(args) == 12345); 725 } 726 727 struct T 728 { 729 @NamedArgument string x; 730 @NamedArgument string foo; 731 @(PositionalArgument(0, "a").Optional()) string a; 732 @(PositionalArgument(1, "b").Optional()) string[] b; 733 } 734 test!(["--foo","FOO","-x","X"], T("X", "FOO")); 735 test!(["--foo=FOO","-x=X"], T("X", "FOO")); 736 test!(["--foo=FOO","1","-x=X"], T("X", "FOO", "1")); 737 test!(["--foo=FOO","1","2","3","4"], T(string.init, "FOO", "1",["2","3","4"])); 738 test!(["-xX"], T("X")); 739 740 struct T1 741 { 742 @(PositionalArgument(0, "a")) string[3] a; 743 @(PositionalArgument(1, "b")) string[] b; 744 } 745 test!(["1","2","3","4","5","6"], T1(["1","2","3"],["4","5","6"])); 746 747 struct T2 748 { 749 bool foo = true; 750 } 751 test!(["--no-foo"], T2(false)); 752 753 struct T3 754 { 755 @(PositionalArgument(0, "a").Optional()) 756 string a = "not set"; 757 758 @(NamedArgument.Required()) 759 int b; 760 } 761 test!(["-b", "4"], T3("not set", 4)); 762 } 763 764 unittest 765 { 766 struct T 767 { 768 string x; 769 string foo; 770 } 771 772 enum config = { 773 Config config; 774 config.caseSensitive = false; 775 return config; 776 }(); 777 778 assert(CLI!(config, T).parseArgs!((T t) { assert(t == T("X", "FOO")); return 12345; })(["--Foo","FOO","-X","X"]) == 12345); 779 assert(CLI!(config, T).parseArgs!((T t) { assert(t == T("X", "FOO")); return 12345; })(["--FOo=FOO","-X=X"]) == 12345); 780 } 781 782 unittest 783 { 784 struct T 785 { 786 bool a; 787 bool b; 788 } 789 enum config = { 790 Config config; 791 config.bundling = true; 792 return config; 793 }(); 794 795 assert(CLI!(config, T).parseArgs!((T t) { assert(t == T(true, true)); return 12345; })(["-a","-b"]) == 12345); 796 assert(CLI!(config, T).parseArgs!((T t) { assert(t == T(true, true)); return 12345; })(["-ab"]) == 12345); 797 } 798 799 unittest 800 { 801 struct T 802 { 803 bool b; 804 } 805 806 assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); return 12345; })(["-b"]) == 12345); 807 assert(CLI!T.parseArgs!((T t) { assert(t == T(true)); return 12345; })(["-b=true"]) == 12345); 808 assert(CLI!T.parseArgs!((T t) { assert(t == T(false)); return 12345; })(["-b=false"]) == 12345); 809 } 810 811 unittest 812 { 813 struct T 814 { 815 import std.sumtype: SumType; 816 817 struct cmd1 { string a; } 818 struct cmd2 819 { 820 string b; 821 822 @TrailingArguments 823 string[] args; 824 } 825 826 string c; 827 string d; 828 829 SumType!(cmd1, cmd2) cmd; 830 } 831 832 assert(CLI!T.parseArgs!((T t) { assert(t == T("C",null,typeof(T.cmd)(T.cmd2("B")))); return 12345; })(["-c","C","cmd2","-b","B"]) == 12345); 833 assert(CLI!T.parseArgs!((T t) { assert(t == T("C",null,typeof(T.cmd)(T.cmd2("",["-b","B"])))); return 12345; })(["-c","C","cmd2","--","-b","B"]) == 12345); 834 } 835 836 unittest 837 { 838 struct T 839 { 840 import std.sumtype: SumType; 841 842 struct cmd1 {} 843 struct cmd2 {} 844 845 SumType!(cmd1, cmd2) cmd; 846 } 847 848 assert(CLI!T.parseArgs!((T t) { assert(t == T(typeof(T.cmd)(T.cmd1.init))); return 12345; })(["cmd1"]) == 12345); 849 assert(CLI!T.parseArgs!((T t) { assert(t == T(typeof(T.cmd)(T.cmd2.init))); return 12345; })(["cmd2"]) == 12345); 850 } 851 852 unittest 853 { 854 struct T 855 { 856 import std.sumtype: SumType; 857 858 struct cmd1 { string a; } 859 struct cmd2 { string b; } 860 861 string c; 862 string d; 863 864 SumType!(cmd1, Default!cmd2) cmd; 865 } 866 867 assert(CLI!T.parseArgs!((T t) { assert(t == T("C",null,typeof(T.cmd)(Default!(T.cmd2)(T.cmd2("B"))))); return 12345; })(["-c","C","-b","B"]) == 12345); 868 assert(CLI!T.parseArgs!((_) {assert(false);})(["-h"]) == 0); 869 assert(CLI!T.parseArgs!((_) {assert(false);})(["--help"]) == 0); 870 } 871 872 template CLI(Config config, COMMANDS...) 873 { 874 mixin template main(alias newMain) 875 { 876 import std.sumtype: SumType, match; 877 878 private struct Program 879 { 880 SumType!COMMANDS cmd; // Sub-commands 881 } 882 883 private auto forwardMain(Args...)(Program prog, auto ref Args args) 884 { 885 import core.lifetime: forward; 886 return prog.cmd.match!(_ => newMain(_, forward!args)); 887 } 888 889 mixin CLI!(config, Program).main!forwardMain; 890 } 891 } 892 893 template CLI(Config config, COMMAND) 894 { 895 static Result parseKnownArgs(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) 896 { 897 return callParser!(config, false)(receiver, args, unrecognizedArgs); 898 } 899 900 static Result parseKnownArgs(ref COMMAND receiver, ref string[] args) 901 { 902 string[] unrecognizedArgs; 903 904 auto res = parseKnownArgs(receiver, args, unrecognizedArgs); 905 if(res) 906 args = unrecognizedArgs; 907 908 return res; 909 } 910 911 static Result parseArgs(ref COMMAND receiver, string[] args) 912 { 913 auto res = parseKnownArgs(receiver, args); 914 if(res && args.length > 0) 915 { 916 res = Result.Error("Unrecognized arguments: ", args); 917 config.onError(res.errorMsg); 918 } 919 920 return res; 921 } 922 923 static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) 924 if(__traits(compiles, { newMain(COMMAND.init); })) 925 { 926 alias value = initialValue; 927 928 auto res = parseArgs(value, args); 929 if(!res) 930 return res.resultCode; 931 932 static if(__traits(compiles, { int a = cast(int) newMain(value); })) 933 return cast(int) newMain(value); 934 else 935 { 936 newMain(value); 937 return 0; 938 } 939 } 940 941 static int parseArgs(alias newMain)(string[] args, COMMAND initialValue = COMMAND.init) 942 if(__traits(compiles, { newMain(COMMAND.init, string[].init); })) 943 { 944 alias value = initialValue; 945 946 auto res = parseKnownArgs(value, args); 947 if(!res) 948 return res.resultCode; 949 950 static if(__traits(compiles, { int a = cast(int) newMain(value, args); })) 951 return cast(int) newMain(value, args); 952 else 953 { 954 newMain(value, args); 955 return 0; 956 } 957 } 958 959 string[] completeArgs(string[] args) 960 { 961 import std.algorithm: sort, uniq; 962 import std.array: array; 963 964 COMMAND dummy; 965 string[] unrecognizedArgs; 966 967 auto res = callParser!(config, true)(dummy, args.length == 0 ? [""] : args, unrecognizedArgs); 968 969 return res ? res.suggestions.dup.sort.uniq.array : []; 970 } 971 972 int complete(string[] args) 973 { 974 import argparse.completer; 975 import std.sumtype: match; 976 977 // dmd fails with core.exception.OutOfMemoryError@core\lifetime.d(137): Memory allocation failed 978 // if we call anything from CLI!(config, Complete!COMMAND) so we have to directly call parser here 979 980 Complete!COMMAND receiver; 981 string[] unrecognizedArgs; 982 983 auto res = callParser!(config, false)(receiver, args, unrecognizedArgs); 984 if(!res) 985 return 1; 986 987 if(res && unrecognizedArgs.length > 0) 988 { 989 config.onError("Unrecognized arguments: ", unrecognizedArgs); 990 return 1; 991 } 992 993 receiver.cmd.match!(_ => _.execute!config()); 994 995 return 0; 996 } 997 998 mixin template mainComplete() 999 { 1000 int main(string[] argv) 1001 { 1002 return CLI!(config, COMMAND).complete(argv[1..$]); 1003 } 1004 } 1005 1006 mixin template main(alias newMain) 1007 { 1008 version(argparse_completion) 1009 { 1010 mixin CLI!(config, COMMAND).mainComplete; 1011 } 1012 else 1013 { 1014 int main(string[] argv) 1015 { 1016 return CLI!(config, COMMAND).parseArgs!(newMain)(argv[1..$]); 1017 } 1018 } 1019 } 1020 } 1021 1022 alias CLI(COMMANDS...) = CLI!(Config.init, COMMANDS); 1023 1024 1025 unittest 1026 { 1027 struct T 1028 { 1029 import std.sumtype: SumType; 1030 1031 struct cmd1 1032 { 1033 string foo; 1034 string bar; 1035 string baz; 1036 } 1037 struct cmd2 1038 { 1039 string cat,can,dog; 1040 } 1041 1042 @NamedArgument("apple","a") 1043 string a = "dummyA"; 1044 @NamedArgument 1045 string s = "dummyS"; 1046 @NamedArgument 1047 string b = "dummyB"; 1048 1049 @SubCommands 1050 SumType!(cmd1, cmd2) cmd; 1051 } 1052 1053 assert(CLI!T.completeArgs([]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 1054 assert(CLI!T.completeArgs([""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 1055 assert(CLI!T.completeArgs(["-a"]) == ["-a"]); 1056 assert(CLI!T.completeArgs(["c"]) == ["cmd1","cmd2"]); 1057 assert(CLI!T.completeArgs(["cmd1"]) == ["cmd1"]); 1058 assert(CLI!T.completeArgs(["cmd1",""]) == ["--apple","--bar","--baz","--foo","--help","-a","-b","-h","-s","cmd1","cmd2"]); 1059 assert(CLI!T.completeArgs(["-a","val-a",""]) == ["--apple","--help","-a","-b","-h","-s","cmd1","cmd2"]); 1060 1061 assert(!CLI!T.complete(["init","--bash","--commandName","mytool"])); 1062 assert(!CLI!T.complete(["init","--zsh"])); 1063 assert(!CLI!T.complete(["init","--tcsh"])); 1064 assert(!CLI!T.complete(["init","--fish"])); 1065 1066 assert(CLI!T.complete(["init","--unknown"])); 1067 1068 import std.process: environment; 1069 { 1070 environment["COMP_LINE"] = "mytool "; 1071 assert(!CLI!T.complete(["--bash","--","---","foo","foo"])); 1072 1073 environment["COMP_LINE"] = "mytool c"; 1074 assert(!CLI!T.complete(["--bash","--","c","---"])); 1075 1076 environment.remove("COMP_LINE"); 1077 } 1078 { 1079 environment["COMMAND_LINE"] = "mytool "; 1080 assert(!CLI!T.complete(["--tcsh","--"])); 1081 1082 environment["COMMAND_LINE"] = "mytool c"; 1083 assert(!CLI!T.complete(["--fish","--","c"])); 1084 1085 environment.remove("COMMAND_LINE"); 1086 } 1087 } 1088 1089 1090 unittest 1091 { 1092 struct T 1093 { 1094 int a; 1095 } 1096 1097 static assert(__traits(compiles, { mixin CLI!T.main!((params) => 0); })); 1098 static assert(__traits(compiles, { mixin CLI!T.main!((params, args) => 0); })); 1099 } 1100 1101 1102 1103 1104 1105 1106 1107 1108 //////////////////////////////////////////////////////////////////////////////////////////////////// 1109 // User defined attributes 1110 //////////////////////////////////////////////////////////////////////////////////////////////////// 1111 1112 unittest 1113 { 1114 struct T 1115 { 1116 @(NamedArgument.NumberOfValues(1,3)) 1117 int[] a; 1118 @(NamedArgument.NumberOfValues(2)) 1119 int[] b; 1120 } 1121 1122 assert(CLI!T.parseArgs!((T t) { assert(t == T([1,2,3],[4,5])); return 12345; })(["-a","1","2","3","-b","4","5"]) == 12345); 1123 assert(CLI!T.parseArgs!((T t) { assert(t == T([1],[4,5])); return 12345; })(["-a","1","-b","4","5"]) == 12345); 1124 } 1125 1126 1127 auto PreValidation(alias func, ARG)(ARG arg) 1128 if(isArgumentUDA!ARG) 1129 { 1130 return ArgumentUDA!(arg.parsingFunc.changePreValidation!func)(arg.tupleof); 1131 } 1132 1133 auto Parse(alias func, ARG)(ARG arg) 1134 if(isArgumentUDA!ARG) 1135 { 1136 return ArgumentUDA!(arg.parsingFunc.changeParse!func)(arg.tupleof); 1137 } 1138 1139 auto Validation(alias func, ARG)(ARG arg) 1140 if(isArgumentUDA!ARG) 1141 { 1142 return ArgumentUDA!(arg.parsingFunc.changeValidation!func)(arg.tupleof); 1143 } 1144 1145 auto Action(alias func, ARG)(ARG arg) 1146 if(isArgumentUDA!ARG) 1147 { 1148 return ArgumentUDA!(arg.parsingFunc.changeAction!func)(arg.tupleof); 1149 } 1150 1151 auto AllowNoValue(alias valueToUse, ARG)(ARG arg) 1152 if(isArgumentUDA!ARG) 1153 { 1154 auto desc = ArgumentUDA!(arg.parsingFunc.changeNoValueAction!(() { return valueToUse; }))(arg.tupleof); 1155 desc.info.minValuesCount = 0; 1156 return desc; 1157 } 1158 1159 auto RequireNoValue(alias valueToUse, ARG)(ARG arg) 1160 if(isArgumentUDA!ARG) 1161 { 1162 auto desc = arg.AllowNoValue!valueToUse; 1163 desc.info.minValuesCount = 0; 1164 desc.info.maxValuesCount = 0; 1165 return desc; 1166 } 1167 1168 auto Counter(ARG)(ARG arg) 1169 if(isArgumentUDA!ARG) 1170 { 1171 struct CounterParsingFunction 1172 { 1173 static bool parse(T)(ref T receiver, const ref RawParam param) 1174 { 1175 assert(param.value.length == 0); 1176 1177 ++receiver; 1178 1179 return true; 1180 } 1181 } 1182 1183 auto desc = ArgumentUDA!(CounterParsingFunction)(arg.tupleof); 1184 desc.info.minValuesCount = 0; 1185 desc.info.maxValuesCount = 0; 1186 return desc; 1187 } 1188 1189 1190 unittest 1191 { 1192 struct T 1193 { 1194 @(NamedArgument.Counter()) int a; 1195 } 1196 1197 assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); return 12345; })(["-a","-a","-a"]) == 12345); 1198 } 1199 1200 1201 auto AllowedValues(alias values, ARG)(ARG arg) 1202 { 1203 import std.array : assocArray; 1204 import std.range : repeat; 1205 import std.traits: KeyType; 1206 1207 enum valuesAA = assocArray(values, false.repeat); 1208 1209 auto desc = arg.Validation!(Validators.ValueInList!(values, KeyType!(typeof(valuesAA)))); 1210 desc.info.setAllowedValues!values; 1211 return desc; 1212 } 1213 1214 1215 unittest 1216 { 1217 struct T 1218 { 1219 @(NamedArgument.AllowedValues!([1,3,5])) int a; 1220 } 1221 1222 assert(CLI!T.parseArgs!((T t) { assert(t == T(3)); return 12345; })(["-a", "3"]) == 12345); 1223 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a", "2"]) != 0); // "kiwi" is not allowed 1224 } 1225 1226 unittest 1227 { 1228 struct T 1229 { 1230 @(NamedArgument.AllowedValues!(["apple","pear","banana"])) 1231 string fruit; 1232 } 1233 1234 assert(CLI!T.parseArgs!((T t) { assert(t == T("apple")); return 12345; })(["--fruit", "apple"]) == 12345); 1235 assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed 1236 } 1237 1238 unittest 1239 { 1240 enum Fruit { apple, pear, banana } 1241 struct T 1242 { 1243 @NamedArgument 1244 Fruit fruit; 1245 } 1246 1247 assert(CLI!T.parseArgs!((T t) { assert(t == T(Fruit.apple)); return 12345; })(["--fruit", "apple"]) == 12345); 1248 assert(CLI!T.parseArgs!((T t) { assert(false); })(["--fruit", "kiwi"]) != 0); // "kiwi" is not allowed 1249 } 1250 1251 1252 1253 unittest 1254 { 1255 struct T 1256 { 1257 string a; 1258 string b; 1259 1260 @TrailingArguments string[] args; 1261 } 1262 1263 assert(CLI!T.parseArgs!((T t) { assert(t == T("A","",["-b","B"])); return 12345; })(["-a","A","--","-b","B"]) == 12345); 1264 } 1265 1266 unittest 1267 { 1268 struct T 1269 { 1270 @NamedArgument int i; 1271 @NamedArgument(["u","u1"]) uint u; 1272 @NamedArgument("d","d1") double d; 1273 } 1274 1275 assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); return 12345; })(["-i","-5","-u","8","-d","12.345"]) == 12345); 1276 assert(CLI!T.parseArgs!((T t) { assert(t == T(-5,8,12.345)); return 12345; })(["-i","-5","-u1","8","-d1","12.345"]) == 12345); 1277 } 1278 1279 unittest 1280 { 1281 struct T 1282 { 1283 @NamedArgument int[] a; 1284 @NamedArgument int[][] b; 1285 } 1286 1287 assert(CLI!T.parseArgs!((T t) { assert(t.a == [1,2,3,4,5]); return 12345; })(["-a","1","2","3","-a","4","5"]) == 12345); 1288 assert(CLI!T.parseArgs!((T t) { assert(t.b == [[1,2,3],[4,5]]); return 12345; })(["-b","1","2","3","-b","4","5"]) == 12345); 1289 } 1290 1291 unittest 1292 { 1293 struct T 1294 { 1295 @NamedArgument int[] a; 1296 } 1297 1298 enum cfg = { 1299 Config cfg; 1300 cfg.arraySep = ','; 1301 return cfg; 1302 }(); 1303 1304 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T([1,2,3,4,5])); return 12345; })(["-a","1,2,3","-a","4","5"]) == 12345); 1305 } 1306 1307 unittest 1308 { 1309 struct T 1310 { 1311 @NamedArgument int[string] a; 1312 } 1313 1314 assert(CLI!T.parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); return 12345; })(["-a=foo=3","-a","boo=7"]) == 12345); 1315 } 1316 1317 unittest 1318 { 1319 struct T 1320 { 1321 @NamedArgument int[string] a; 1322 } 1323 1324 enum cfg = { 1325 Config cfg; 1326 cfg.arraySep = ','; 1327 return cfg; 1328 }(); 1329 1330 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); return 12345; })(["-a=foo=3,boo=7"]) == 12345); 1331 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["foo":3,"boo":7])); return 12345; })(["-a","foo=3,boo=7"]) == 12345); 1332 } 1333 1334 unittest 1335 { 1336 struct T 1337 { 1338 enum Fruit { apple, pear }; 1339 1340 @NamedArgument Fruit a; 1341 } 1342 1343 assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.apple)); return 12345; })(["-a","apple"]) == 12345); 1344 assert(CLI!T.parseArgs!((T t) { assert(t == T(T.Fruit.pear)); return 12345; })(["-a=pear"]) == 12345); 1345 } 1346 1347 unittest 1348 { 1349 struct T 1350 { 1351 @NamedArgument string[] a; 1352 } 1353 1354 assert(CLI!T.parseArgs!((T t) { assert(t == T(["1,2,3","4","5"])); return 12345; })(["-a","1,2,3","-a","4","5"]) == 12345); 1355 1356 enum cfg = { 1357 Config cfg; 1358 cfg.arraySep = ','; 1359 return cfg; 1360 }(); 1361 1362 assert(CLI!(cfg, T).parseArgs!((T t) { assert(t == T(["1","2","3","4","5"])); return 12345; })(["-a","1,2,3","-a","4","5"]) == 12345); 1363 } 1364 1365 unittest 1366 { 1367 struct T 1368 { 1369 @(NamedArgument.AllowNoValue !10) int a; 1370 @(NamedArgument.RequireNoValue!20) int b; 1371 } 1372 1373 assert(CLI!T.parseArgs!((T t) { assert(t.a == 10); return 12345; })(["-a"]) == 12345); 1374 assert(CLI!T.parseArgs!((T t) { assert(t.b == 20); return 12345; })(["-b"]) == 12345); 1375 assert(CLI!T.parseArgs!((T t) { assert(t.a == 30); return 12345; })(["-a","30"]) == 12345); 1376 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","30"]) != 0); 1377 } 1378 1379 unittest 1380 { 1381 struct T 1382 { 1383 @(NamedArgument 1384 .PreValidation!((string s) { return s.length > 1 && s[0] == '!'; }) 1385 .Parse !((string s) { return s[1]; }) 1386 .Validation !((char v) { return v >= '0' && v <= '9'; }) 1387 .Action !((ref int a, char v) { a = v - '0'; }) 1388 ) 1389 int a; 1390 } 1391 1392 assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); return 12345; })(["-a","!4"]) == 12345); 1393 } 1394 1395 unittest 1396 { 1397 static struct T 1398 { 1399 int a; 1400 1401 @(NamedArgument("a")) void foo() { a++; } 1402 } 1403 1404 assert(CLI!T.parseArgs!((T t) { assert(t == T(4)); return 12345; })(["-a","-a","-a","-a"]) == 12345); 1405 } 1406 1407 1408 auto Command(string[] name...) 1409 { 1410 return CommandInfo(name.dup); 1411 } 1412 1413 unittest 1414 { 1415 auto a = Command("MYPROG"); 1416 assert(a.names == ["MYPROG"]); 1417 } 1418 1419 1420 1421 unittest 1422 { 1423 @Command("MYPROG") 1424 struct T 1425 { 1426 @(NamedArgument.HideFromHelp()) string s; 1427 } 1428 1429 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-h","-s","asd"]) == 0); 1430 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-h"]) == 0); 1431 1432 assert(CLI!T.parseArgs!((T t, string[] args) { assert(false); })(["-h","-s","asd"]) == 0); 1433 assert(CLI!T.parseArgs!((T t, string[] args) { assert(false); })(["-h"]) == 0); 1434 } 1435 1436 unittest 1437 { 1438 @Command("MYPROG") 1439 struct T 1440 { 1441 @(NamedArgument.Required()) string s; 1442 } 1443 1444 assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); 1445 } 1446 1447 unittest 1448 { 1449 @Command("MYPROG") 1450 struct T 1451 { 1452 @MutuallyExclusive() 1453 { 1454 string a; 1455 string b; 1456 } 1457 } 1458 1459 // Either or no argument is allowed 1460 assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); 1461 assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); 1462 assert(CLI!T.parseArgs!((T t) {})([]) == 0); 1463 1464 // Both arguments are not allowed 1465 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); 1466 } 1467 1468 unittest 1469 { 1470 @Command("MYPROG") 1471 struct T 1472 { 1473 @(MutuallyExclusive().Required()) 1474 { 1475 string a; 1476 string b; 1477 } 1478 } 1479 1480 // Either argument is allowed 1481 assert(CLI!T.parseArgs!((T t) {})(["-a","a"]) == 0); 1482 assert(CLI!T.parseArgs!((T t) {})(["-b","b"]) == 0); 1483 1484 // Both arguments or no argument is not allowed 1485 assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); 1486 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a","-b","b"]) != 0); 1487 } 1488 1489 unittest 1490 { 1491 @Command("MYPROG") 1492 struct T 1493 { 1494 @RequiredTogether() 1495 { 1496 string a; 1497 string b; 1498 } 1499 } 1500 1501 // Both or no argument is allowed 1502 assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); 1503 assert(CLI!T.parseArgs!((T t) {})([]) == 0); 1504 1505 // Single argument is not allowed 1506 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); 1507 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); 1508 } 1509 1510 unittest 1511 { 1512 @Command("MYPROG") 1513 struct T 1514 { 1515 @(RequiredTogether().Required()) 1516 { 1517 string a; 1518 string b; 1519 } 1520 } 1521 1522 // Both arguments are allowed 1523 assert(CLI!T.parseArgs!((T t) {})(["-a","a","-b","b"]) == 0); 1524 1525 // Single argument or no argument is not allowed 1526 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-a","a"]) != 0); 1527 assert(CLI!T.parseArgs!((T t) { assert(false); })(["-b","b"]) != 0); 1528 assert(CLI!T.parseArgs!((T t) { assert(false); })([]) != 0); 1529 } 1530 1531