1 /* 2 * Hunt - A refined core library for D programming language. 3 * 4 * Copyright (C) 2018-2019 HuntLabs 5 * 6 * Website: https://www.huntlabs.net/ 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.util.Argument; 13 14 /** 15 * Generic argument parsing exception. 16 */ 17 class ArgParseError : Exception 18 { 19 /** 20 */ 21 this(string msg) pure nothrow 22 { 23 super(msg); 24 } 25 } 26 27 /** 28 * Thrown when help is requested. 29 */ 30 class ArgParseHelp : Exception 31 { 32 this(string msg) pure nothrow 33 { 34 super(msg); 35 } 36 } 37 38 /** 39 * User defined attribute for an option. 40 */ 41 struct Option 42 { 43 /// List of names the option can have. 44 string[] names; 45 46 /** 47 * Constructs the option with a list of names. Note that any leading "-" or 48 * "--" should be omitted. This is added automatically depending on the 49 * length of the name. 50 */ 51 this(string[] names...) pure nothrow 52 { 53 this.names = names; 54 } 55 56 /** 57 * Returns true if the given option name is equivalent to this option. 58 */ 59 bool opEquals(string opt) const pure nothrow 60 { 61 foreach (name; names) 62 { 63 if (name == opt) 64 return true; 65 } 66 67 return false; 68 } 69 70 unittest 71 { 72 static assert(Option("foo") == "foo"); 73 static assert(Option("foo", "f") == "foo"); 74 static assert(Option("foo", "f") == "f"); 75 static assert(Option("foo", "bar", "baz") == "foo"); 76 static assert(Option("foo", "bar", "baz") == "bar"); 77 static assert(Option("foo", "bar", "baz") == "baz"); 78 79 static assert(Option("foo", "bar") != "baz"); 80 } 81 82 /** 83 * Returns the canonical name of this option. That is, its first name. 84 */ 85 string toString() const pure nothrow 86 { 87 return names.length > 0 ? (nameToOption(names[0])) : null; 88 } 89 90 unittest 91 { 92 static assert(Option().toString is null); 93 static assert(Option("foo", "bar", "baz").toString == "--foo"); 94 static assert(Option("f", "bar", "baz").toString == "-f"); 95 } 96 } 97 98 /** 99 * An option flag. These types of options are handled specially and never have 100 * an argument. 101 */ 102 enum OptionFlag : bool 103 { 104 /// The option flag was not specified. 105 no = false, 106 107 /// The option flag was specified. 108 yes = true, 109 } 110 111 /** 112 * Multiplicity of an argument. 113 */ 114 enum Multiplicity 115 { 116 optional, 117 zeroOrMore, 118 oneOrMore, 119 } 120 121 /** 122 * User defined attribute for a positional argument. 123 */ 124 struct Argument 125 { 126 /** 127 * Name of the argument. Since this is a positional argument, this value is 128 * only used in the help string. 129 */ 130 string name; 131 132 /** 133 * Lower and upper bounds for the number of values this argument can have. 134 */ 135 size_t lowerBound = 1; 136 137 /// Ditto 138 size_t upperBound = 1; 139 140 /** 141 * Constructs an argument with the given name and count. The count specifies 142 * how many argument elements should be consumed from the command line. By 143 * default, this is 1. 144 */ 145 this(string name, size_t count = 1) pure nothrow 146 { 147 this.name = name; 148 this.lowerBound = count; 149 this.upperBound = count; 150 } 151 152 /** 153 * Constructs an argument with the given name and an upper and lower bound 154 * for how many argument elements should be consumed from the command line. 155 */ 156 this(string name, size_t lowerBound, size_t upperBound) pure nothrow 157 { 158 assert(lowerBound < upperBound); 159 this.name = name; 160 this.lowerBound = lowerBound; 161 this.upperBound = upperBound; 162 } 163 164 /** 165 * An argument with a multiplicity specifier. 166 */ 167 this(string name, Multiplicity multiplicity) pure nothrow 168 { 169 this.name = name; 170 171 final switch (multiplicity) 172 { 173 case Multiplicity.optional: 174 this.lowerBound = 0; 175 this.upperBound = 1; 176 break; 177 case Multiplicity.zeroOrMore: 178 this.lowerBound = 0; 179 this.upperBound = size_t.max; 180 break; 181 case Multiplicity.oneOrMore: 182 this.lowerBound = 1; 183 this.upperBound = size_t.max; 184 break; 185 } 186 } 187 188 /** 189 * Convert to a usage string. 190 */ 191 @property string usage() const pure 192 { 193 import std.format : format; 194 195 if (lowerBound == 0) 196 { 197 if (upperBound == 1) 198 return "["~ name ~"]"; 199 else if (upperBound == upperBound.max) 200 return "["~ name ~"...]"; 201 202 return "["~ name ~"... (up to %d times)]".format(upperBound); 203 } 204 else if (lowerBound == 1) 205 { 206 if (upperBound == 1) 207 return name; 208 else if (upperBound == upperBound.max) 209 return name ~ " ["~ name ~"...]"; 210 211 return name ~ " ["~ name ~"... (up to %d times)]" 212 .format(upperBound-1); 213 } 214 215 if (lowerBound == upperBound) 216 return name ~" (multiplicity of %d)" 217 .format(upperBound); 218 219 return name ~" ["~ name ~"... (between %d and %d times)]" 220 .format(lowerBound-1, upperBound-1); 221 } 222 223 /** 224 * Get a multiplicity error string. 225 * 226 * Params: 227 * specified = The number of parameters that were specified. 228 * 229 * Returns: 230 * If there is an error, returns a string explaining the error. Otherwise, 231 * returns $(D null). 232 */ 233 @property string multiplicityError(size_t specified) const pure 234 { 235 import std.format : format; 236 237 if (specified >= lowerBound && specified <= upperBound) 238 return null; 239 240 if (specified < lowerBound) 241 { 242 if (lowerBound == 1 && upperBound == 1) 243 return "Expected a value for positional argument '%s'" 244 .format(name); 245 else 246 return ("Expected at least %d values for positional argument" ~ 247 " '%s'. Only %d values were specified.") 248 .format(lowerBound, name, specified); 249 } 250 251 // This should never happen. Argument parsing is not greedy. 252 return "Too many values specified for positional argument '%s'."; 253 } 254 } 255 256 unittest 257 { 258 with (Argument("lion")) 259 { 260 assert(name == "lion"); 261 assert(lowerBound == 1); 262 assert(upperBound == 1); 263 } 264 265 with (Argument("tiger", Multiplicity.optional)) 266 { 267 assert(lowerBound == 0); 268 assert(upperBound == 1); 269 } 270 271 with (Argument("bear", Multiplicity.zeroOrMore)) 272 { 273 assert(lowerBound == 0); 274 assert(upperBound == size_t.max); 275 } 276 277 with (Argument("dinosaur", Multiplicity.oneOrMore)) 278 { 279 assert(lowerBound == 1); 280 assert(upperBound == size_t.max); 281 } 282 } 283 284 /** 285 * Help string for an option or positional argument. 286 */ 287 struct Help 288 { 289 /// Help string. 290 string help; 291 } 292 293 /** 294 * Meta variable name. 295 */ 296 struct MetaVar 297 { 298 /// Name of the meta variable. 299 string name; 300 } 301 302 /** 303 * Function signatures that can handle arguments or options. 304 */ 305 private alias void OptionHandler() pure; 306 private alias void ArgumentHandler(string) pure; /// Ditto 307 308 template isOptionHandler(Func) 309 { 310 import std.meta : AliasSeq; 311 import std.traits : arity, hasFunctionAttributes, isFunction, ReturnType; 312 313 static if (isFunction!Func) 314 { 315 enum isOptionHandler = 316 hasFunctionAttributes!(Func, "pure") && 317 is(ReturnType!Func == void) && 318 arity!Func == 0; 319 } 320 else 321 { 322 enum isOptionHandler = false; 323 } 324 } 325 326 template isArgumentHandler(Func) 327 { 328 import std.meta : AliasSeq; 329 import std.traits : hasFunctionAttributes, isFunction, Parameters, ReturnType; 330 331 static if (isFunction!Func) 332 { 333 enum isArgumentHandler = 334 hasFunctionAttributes!(Func, "pure") && 335 is(ReturnType!Func == void) && 336 is(Parameters!Func == AliasSeq!(string)); 337 } 338 else 339 { 340 enum isArgumentHandler = false; 341 } 342 } 343 344 unittest 345 { 346 struct TemplateOptions(T) 347 { 348 void optionHandler() pure { } 349 void argumentHandler(string) pure { } 350 string foo() pure { return ""; } 351 void bar() { import std.stdio : writeln; writeln("bar"); } 352 void baz(int) pure { } 353 } 354 355 TemplateOptions!int options; 356 357 static assert(!is(typeof(options.optionHandler) : OptionHandler)); 358 static assert(!is(typeof(options.argumentHandler) : ArgumentHandler)); 359 static assert(!is(typeof(options.foo) : ArgumentHandler)); 360 static assert(!is(typeof(options.bar) : ArgumentHandler)); 361 static assert(!is(typeof(options.baz) : ArgumentHandler)); 362 363 static assert(isOptionHandler!(typeof(options.optionHandler))); 364 static assert(!isOptionHandler!(typeof(options.argumentHandler))); 365 static assert(!isOptionHandler!(typeof(options.foo))); 366 static assert(!isOptionHandler!(typeof(options.bar))); 367 static assert(!isOptionHandler!(typeof(options.baz))); 368 369 static assert(!isArgumentHandler!(typeof(options.optionHandler))); 370 static assert(isArgumentHandler!(typeof(options.argumentHandler))); 371 static assert(!isArgumentHandler!(typeof(options.foo))); 372 static assert(!isArgumentHandler!(typeof(options.bar))); 373 static assert(!isArgumentHandler!(typeof(options.baz))); 374 } 375 376 /** 377 * Returns true if the given argument is a short option. That is, if it starts 378 * with a '-'. 379 */ 380 bool isShortOption(string arg) pure nothrow 381 { 382 return arg.length > 1 && arg[0] == '-' && arg[1] != '-'; 383 } 384 385 unittest 386 { 387 static assert(!isShortOption("")); 388 static assert(!isShortOption("-")); 389 static assert(!isShortOption("a")); 390 static assert(!isShortOption("ab")); 391 static assert( isShortOption("-a")); 392 static assert( isShortOption("-ab")); 393 static assert(!isShortOption("--a")); 394 static assert(!isShortOption("--abc")); 395 } 396 397 /** 398 * Returns true if the given argument is a long option. That is, if it starts 399 * with "--". 400 */ 401 bool isLongOption(string arg) pure nothrow 402 { 403 return arg.length > 2 && arg[0 .. 2] == "--" && arg[2] != '-'; 404 } 405 406 unittest 407 { 408 static assert(!isLongOption("")); 409 static assert(!isLongOption("a")); 410 static assert(!isLongOption("ab")); 411 static assert(!isLongOption("abc")); 412 static assert(!isLongOption("-")); 413 static assert(!isLongOption("-a")); 414 static assert(!isLongOption("--")); 415 static assert( isLongOption("--a")); 416 static assert( isLongOption("--arg")); 417 static assert( isLongOption("--arg=asdf")); 418 } 419 420 /** 421 * Returns true if the given argument is an option. That is, it is either a 422 * short option or a long option. 423 */ 424 bool isOption(string arg) pure nothrow 425 { 426 return isShortOption(arg) || isLongOption(arg); 427 } 428 429 private static struct OptionSplit 430 { 431 string head; 432 string tail; 433 } 434 435 /** 436 * Splits an option on "=". 437 */ 438 private auto splitOption(string option) pure 439 { 440 size_t i = 0; 441 while (i < option.length && option[i] != '=') 442 ++i; 443 444 return OptionSplit( 445 option[0 .. i], 446 (i < option.length) ? option[i+1 .. $] : null 447 ); 448 } 449 450 unittest 451 { 452 static assert(splitOption("") == OptionSplit("", null)); 453 static assert(splitOption("--foo") == OptionSplit("--foo", null)); 454 static assert(splitOption("--foo=") == OptionSplit("--foo", "")); 455 static assert(splitOption("--foo=bar") == OptionSplit("--foo", "bar")); 456 } 457 458 private static struct ArgSplit 459 { 460 const(string)[] head; 461 const(string)[] tail; 462 } 463 464 /** 465 * Splits arguments on "--". 466 */ 467 private auto splitArgs(const(string)[] args) pure 468 { 469 size_t i = 0; 470 while (i < args.length && args[i] != "--") 471 ++i; 472 473 return ArgSplit( 474 args[0 .. i], 475 (i < args.length) ? args[i+1 .. $] : [] 476 ); 477 } 478 479 unittest 480 { 481 static assert(splitArgs([]) == ArgSplit([], [])); 482 static assert(splitArgs(["a", "b"]) == ArgSplit(["a", "b"], [])); 483 static assert(splitArgs(["a", "--"]) == ArgSplit(["a"], [])); 484 static assert(splitArgs(["a", "--", "b"]) == ArgSplit(["a"], ["b"])); 485 static assert(splitArgs(["a", "--", "b", "c"]) == ArgSplit(["a"], ["b", "c"])); 486 } 487 488 /** 489 * Returns an option name without the leading ("--" or "-"). If it is not an 490 * option, returns null. 491 */ 492 private string optionToName(string option) pure nothrow 493 { 494 if (isLongOption(option)) 495 return option[2 .. $]; 496 497 if (isShortOption(option)) 498 return option[1 .. $]; 499 500 return null; 501 } 502 503 unittest 504 { 505 static assert(optionToName("--opt") == "opt"); 506 static assert(optionToName("-opt") == "opt"); 507 static assert(optionToName("-o") == "o"); 508 static assert(optionToName("opt") is null); 509 static assert(optionToName("o") is null); 510 static assert(optionToName("") is null); 511 } 512 513 /** 514 * Returns the appropriate long or short option corresponding to the given name. 515 */ 516 private string nameToOption(string name) pure nothrow 517 { 518 switch (name.length) 519 { 520 case 0: 521 return null; 522 case 1: 523 return "-" ~ name; 524 default: 525 return "--" ~ name; 526 } 527 } 528 529 unittest 530 { 531 static assert(nameToOption("opt") == "--opt"); 532 static assert(nameToOption("o") == "-o"); 533 static assert(nameToOption("") is null); 534 } 535 536 unittest 537 { 538 immutable identities = ["opt", "o", ""]; 539 540 foreach (identity; identities) 541 assert(identity.nameToOption.optionToName == identity); 542 } 543 544 /** 545 * Check if the given type is valid for an option. 546 */ 547 private template isValidOptionType(T) 548 { 549 import std.traits : isBasicType, isSomeString; 550 551 static if (isBasicType!T || 552 isSomeString!T || 553 isOptionHandler!T || 554 isArgumentHandler!T 555 ) 556 { 557 enum isValidOptionType = true; 558 } 559 else static if (is(T A : A[])) 560 { 561 enum isValidOptionType = isValidOptionType!A; 562 } 563 else 564 { 565 enum isValidOptionType = false; 566 } 567 } 568 569 unittest 570 { 571 static assert(isValidOptionType!bool); 572 static assert(isValidOptionType!int); 573 static assert(isValidOptionType!float); 574 static assert(isValidOptionType!char); 575 static assert(isValidOptionType!string); 576 static assert(isValidOptionType!(int[])); 577 578 alias void Func1() pure; 579 alias void Func2(string) pure; 580 alias int Func3(); 581 alias int Func4(string); 582 alias void Func5(); 583 alias void Func6(string); 584 585 static assert(isValidOptionType!Func1); 586 static assert(isValidOptionType!Func2); 587 static assert(!isValidOptionType!Func3); 588 static assert(!isValidOptionType!Func4); 589 static assert(!isValidOptionType!Func5); 590 static assert(!isValidOptionType!Func6); 591 } 592 593 /** 594 * Count the number of positional arguments. 595 */ 596 size_t countArgs(Options)() pure nothrow 597 { 598 import std.meta : Alias; 599 import std.traits : getUDAs; 600 import std.algorithm.comparison : min; 601 602 size_t count = 0; 603 604 foreach (member; __traits(allMembers, Options)) 605 { 606 alias symbol = Alias!(__traits(getMember, Options, member)); 607 alias argUDAs = getUDAs!(symbol, Argument); 608 count += min(argUDAs.length, 1); 609 } 610 611 return count; 612 } 613 614 /** 615 * Count the number of options. 616 */ 617 size_t countOpts(Options)() pure nothrow 618 { 619 import std.meta : Alias; 620 import std.traits : getUDAs; 621 import std.algorithm.comparison : min; 622 623 size_t count = 0; 624 625 foreach (member; __traits(allMembers, Options)) 626 { 627 alias symbol = Alias!(__traits(getMember, Options, member)); 628 alias optUDAs = getUDAs!(symbol, Option); 629 count += min(optUDAs.length, 1); 630 } 631 632 return count; 633 } 634 635 unittest 636 { 637 static struct A {} 638 639 static assert(countArgs!A == 0); 640 static assert(countOpts!A == 0); 641 642 static struct B 643 { 644 @Argument("test1") 645 string test1; 646 @Argument("test2") 647 string test2; 648 649 @Option("test3", "test3a") 650 @Option("test3b", "test3c") 651 string test3; 652 } 653 654 static assert(countArgs!B == 2); 655 static assert(countOpts!B == 1); 656 657 static struct C 658 { 659 string test; 660 661 @Argument("test1") 662 string test1; 663 664 @Argument("test2") 665 @Argument("test2a") 666 string test2; 667 668 @Option("test3") 669 string test3; 670 671 @Option("test4") 672 string test4; 673 } 674 675 static assert(countArgs!C == 2); 676 static assert(countOpts!C == 2); 677 } 678 679 /** 680 * Checks if the given options are valid. 681 */ 682 private void validateOptions(Options)() pure nothrow 683 { 684 import std.meta : Alias; 685 import std.traits : getUDAs, fullyQualifiedName; 686 687 foreach (member; __traits(allMembers, Options)) 688 { 689 alias symbol = Alias!(__traits(getMember, Options, member)); 690 alias optUDAs = getUDAs!(symbol, Option); 691 alias argUDAs = getUDAs!(symbol, Argument); 692 693 // Basic error checking 694 static assert(!(optUDAs.length > 0 && argUDAs.length > 0), 695 fullyQualifiedName!symbol ~" cannot be both an Option and an Argument" 696 ); 697 static assert(optUDAs.length <= 1, 698 fullyQualifiedName!symbol ~" cannot have multiple Option attributes" 699 ); 700 static assert(argUDAs.length <= 1, 701 fullyQualifiedName!symbol ~" cannot have multiple Argument attributes" 702 ); 703 704 static if (argUDAs.length > 0) 705 static assert(isValidOptionType!(typeof(symbol)), 706 fullyQualifiedName!symbol ~" is not a valid Argument type" 707 ); 708 709 static if (optUDAs.length > 0) 710 static assert(isValidOptionType!(typeof(symbol)), 711 fullyQualifiedName!symbol ~" is not a valid Option type" 712 ); 713 } 714 } 715 716 /** 717 * Checks if the given option type has an associated argument. Currently, only 718 * an OptionFlag does not have an argument. 719 */ 720 private template hasArgument(T) 721 { 722 static if (is(T : OptionFlag) || isOptionHandler!T) 723 enum hasArgument = false; 724 else 725 enum hasArgument = true; 726 } 727 728 unittest 729 { 730 static assert(hasArgument!string); 731 static assert(hasArgument!ArgumentHandler); 732 static assert(hasArgument!int); 733 static assert(hasArgument!bool); 734 static assert(!hasArgument!OptionFlag); 735 static assert(!hasArgument!OptionHandler); 736 } 737 738 /** 739 * Parses an argument. 740 * 741 * Throws: ArgParseError if the given argument cannot be converted to the 742 * requested type. 743 */ 744 T parseArg(T)(string arg) pure 745 { 746 import std.conv : to, ConvException; 747 748 try 749 { 750 return to!T(arg); 751 } 752 catch (ConvException e) 753 { 754 throw new ArgParseError(e.msg); 755 } 756 } 757 758 unittest 759 { 760 import std.exception : ce = collectException; 761 762 assert(parseArg!int("42") == 42); 763 assert(parseArg!string("42") == "42"); 764 assert(ce!ArgParseError(parseArg!size_t("-42"))); 765 } 766 767 /** 768 * Returns the canonical name of the given member's argument. If there is no 769 * argument for the given member, null is returned. 770 */ 771 private string argumentName(Options, string member)() pure 772 { 773 import std.meta : Alias; 774 import std.traits : getUDAs; 775 import std.string : toUpper; 776 777 alias symbol = Alias!(__traits(getMember, Options, member)); 778 779 static if (hasArgument!(typeof(symbol))) 780 { 781 alias metavar = getUDAs!(symbol, MetaVar); 782 static if (metavar.length > 0) 783 return metavar[0].name; 784 else static if (isArgumentHandler!(typeof(symbol))) 785 return member.toUpper; 786 else 787 return "<"~ typeof(symbol).stringof ~ ">"; 788 } 789 else 790 { 791 return null; 792 } 793 } 794 795 unittest 796 { 797 static struct Options 798 { 799 @Option("test1") 800 OptionFlag test1; 801 802 @Option("test2") 803 string test2; 804 805 @Option("test3") 806 @MetaVar("asdf") 807 string test3; 808 809 private string _arg; 810 811 @Option("test4") 812 void test4(string arg) pure 813 { 814 _arg = arg; 815 } 816 817 @Option("test5") 818 @MetaVar("metavar") 819 void test5(string arg) pure 820 { 821 _arg = arg; 822 } 823 } 824 825 static assert(argumentName!(Options, "test1") == null); 826 static assert(argumentName!(Options, "test2") == "<string>"); 827 static assert(argumentName!(Options, "test3") == "asdf"); 828 static assert(argumentName!(Options, "test4") == "TEST4"); 829 static assert(argumentName!(Options, "test5") == "metavar"); 830 } 831 832 /** 833 * Constructs a printable usage string at compile time from the given options 834 * structure. 835 */ 836 string usageString(Options)(string program) pure 837 if (is(Options == struct)) 838 { 839 import std.meta : Alias; 840 import std.traits : getUDAs; 841 import std.array : replicate; 842 import std.string : wrap, toUpper; 843 844 string output = "Usage: "~ program; 845 846 string indent = " ".replicate(output.length + 1); 847 848 // List all options 849 foreach (member; __traits(allMembers, Options)) 850 { 851 alias symbol = Alias!(__traits(getMember, Options, member)); 852 alias optUDAs = getUDAs!(symbol, Option); 853 854 static if (optUDAs.length > 0 && optUDAs[0].names.length > 0) 855 { 856 output ~= " ["~ nameToOption(optUDAs[0].names[0]); 857 858 if (immutable name = argumentName!(Options, member)) 859 output ~= "="~ name; 860 861 output ~= "]"; 862 } 863 } 864 865 // List all arguments 866 foreach (member; __traits(allMembers, Options)) 867 { 868 alias symbol = Alias!(__traits(getMember, Options, member)); 869 alias argUDAs = getUDAs!(symbol, Argument); 870 871 static if (argUDAs.length > 0) 872 output ~= " "~ argUDAs[0].usage; 873 } 874 875 return output.wrap(80, null, indent, 4); 876 } 877 878 /** 879 * Generates a help string for a single argument. Returns null if the given 880 * member is not an argument. 881 */ 882 private string argumentHelp(Options, string member)(size_t padding = 14) pure 883 { 884 import std.meta : Alias; 885 import std.traits : getUDAs; 886 import std.array : replicate; 887 import std.string : wrap; 888 889 string output; 890 891 alias symbol = Alias!(__traits(getMember, Options, member)); 892 alias argUDAs = getUDAs!(symbol, Argument); 893 894 static if (argUDAs.length > 0) 895 { 896 enum name = argUDAs[0].name; 897 output ~= " "~ name; 898 899 alias helpUDAs = getUDAs!(symbol, Help); 900 static if (helpUDAs.length > 0) 901 { 902 if (name.length > padding) 903 output ~= "\n"; 904 905 immutable indent = " ".replicate(padding + 3); 906 immutable firstIndent = (name.length > padding) ? indent : 907 " ".replicate(padding - name.length + 2); 908 909 output ~= helpUDAs[0].help.wrap(80, firstIndent, indent, 4); 910 } 911 else 912 { 913 output ~= "\n"; 914 } 915 } 916 917 return output; 918 } 919 920 /** 921 * Generates a help string for a single option. Returns null if the given member 922 * is not an option. 923 */ 924 private string optionHelp(Options, string member)(size_t padding = 14) pure 925 { 926 import std.meta : Alias; 927 import std.traits : getUDAs; 928 import std.array : replicate; 929 import std.string : wrap; 930 931 string output; 932 933 alias symbol = Alias!(__traits(getMember, Options, member)); 934 alias optUDAs = getUDAs!(symbol, Option); 935 936 static if (optUDAs.length > 0) 937 { 938 enum names = optUDAs[0].names; 939 static if (names.length > 0) 940 { 941 output ~= " " ~ nameToOption(names[0]); 942 943 foreach (name; names[1 .. $]) 944 output ~= ", " ~ nameToOption(name); 945 946 if (string arg = argumentName!(Options, member)) 947 output ~= " " ~ arg; 948 949 immutable len = output.length; 950 951 alias helpUDAs = getUDAs!(symbol, Help); 952 static if (helpUDAs.length > 0) 953 { 954 immutable overflow = len > padding+1; 955 if (overflow) 956 output ~= "\n"; 957 958 immutable indent = " ".replicate(padding+3); 959 immutable firstIndent = overflow 960 ? indent : " ".replicate((padding + 1) - len + 2); 961 962 output ~= helpUDAs[0].help.wrap(80, firstIndent, indent, 4); 963 } 964 else 965 { 966 output ~= "\n"; 967 } 968 } 969 } 970 971 return output; 972 } 973 974 /** 975 * Constructs a printable help string at compile time for the given options 976 * structure. 977 */ 978 string helpString(Options)(string description = null) pure 979 if (is(Options == struct)) 980 { 981 import std.format : format; 982 import std.string : wrap; 983 984 string output; 985 986 if (description) 987 output ~= description.wrap(80) ~ "\n"; 988 989 // List all positional arguments. 990 static if(countArgs!Options > 0) 991 { 992 output ~= "Positional arguments:\n"; 993 994 foreach (member; __traits(allMembers, Options)) 995 output ~= argumentHelp!(Options, member); 996 997 static if (countOpts!Options > 0) 998 output ~= "\n"; 999 } 1000 1001 // List all options 1002 static if (countOpts!Options > 0) 1003 { 1004 output ~= "Optional arguments:\n"; 1005 1006 foreach (member; __traits(allMembers, Options)) 1007 output ~= optionHelp!(Options, member); 1008 } 1009 1010 return output; 1011 } 1012 1013 /** 1014 * Parsing configuration. 1015 */ 1016 enum Config 1017 { 1018 /** 1019 * Enables option bundling. That is, multiple single character options can 1020 * be bundled together. 1021 */ 1022 bundling = 1 << 0, 1023 1024 /** 1025 * Ignore unknown options. These are then parsed as positional arguments. 1026 */ 1027 ignoreUnknown = 1 << 1, 1028 1029 /** 1030 * Throw the ArgParseHelp exception when the option "help" is specified. 1031 * This requires the option to exist in the options struct. 1032 */ 1033 handleHelp = 1 << 2, 1034 1035 /** 1036 * Default configuration options. 1037 */ 1038 default_ = bundling | handleHelp, 1039 } 1040 1041 /** 1042 * Parses options from the given list of arguments. Note that the first argument 1043 * is assumed to be the program name and is ignored. 1044 * 1045 * Returns: Options structure filled out with values. 1046 * 1047 * Throws: ArgParseError if arguments are invalid. 1048 */ 1049 T parseArgs(T)( 1050 const(string[]) arguments, 1051 Config config = Config.default_ 1052 ) pure 1053 if (is(T == struct)) 1054 { 1055 import std.meta : Alias; 1056 import std.traits : getUDAs; 1057 import std.format : format; 1058 import std.range : chain, enumerate, empty, front, popFront; 1059 import std.algorithm.iteration : map, filter; 1060 1061 validateOptions!T; 1062 1063 T options; 1064 1065 auto args = splitArgs(arguments); 1066 1067 // Arguments that have been parsed 1068 bool[] parsed; 1069 parsed.length = args.head.length; 1070 1071 // Parsing occurs in two passes: 1072 // 1073 // 1. Parse all options 1074 // 2. Parse all positional arguments 1075 // 1076 // After the first pass, only positional arguments and invalid options will 1077 // be left. 1078 1079 for (size_t i = 0; i < args.head.length; ++i) 1080 { 1081 auto opt = splitOption(args.head[i]); 1082 1083 if (immutable name = optionToName(opt.head)) 1084 { 1085 foreach (member; __traits(allMembers, T)) 1086 { 1087 alias symbol = Alias!(__traits(getMember, options, member)); 1088 alias optUDAs = getUDAs!(symbol, Option); 1089 1090 static if (optUDAs.length > 0) 1091 { 1092 if (optUDAs[0] == name) 1093 { 1094 parsed[i] = true; 1095 1096 static if (hasArgument!(typeof(symbol))) 1097 { 1098 if (opt.tail) 1099 { 1100 static if (isArgumentHandler!(typeof(symbol))) 1101 __traits(getMember, options, member)(opt.tail); 1102 else 1103 __traits(getMember, options, member) = 1104 parseArg!(typeof(symbol))(opt.tail); 1105 } 1106 else 1107 { 1108 ++i; 1109 1110 if (i >= args.head.length || isOption(args.head[i])) 1111 throw new ArgParseError( 1112 "Expected argument for option '%s'" 1113 .format(opt.head) 1114 ); 1115 1116 static if (isArgumentHandler!(typeof(symbol))) 1117 __traits(getMember, options, member)(args.head[i]); 1118 else 1119 __traits(getMember, options, member) = 1120 parseArg!(typeof(symbol))(args.head[i]); 1121 1122 parsed[i] = true; 1123 } 1124 } 1125 else 1126 { 1127 if (opt.tail) 1128 throw new ArgParseError( 1129 "Option '%s' does not take an argument" 1130 .format(opt.head) 1131 ); 1132 1133 // Handle a request for help 1134 if ((config & Config.handleHelp) == 1135 Config.handleHelp && optUDAs[0] == "help") 1136 throw new ArgParseHelp(""); 1137 1138 static if (isOptionHandler!(typeof(symbol))) 1139 __traits(getMember, options, member)(); 1140 else static if (is(typeof(symbol) : OptionFlag)) 1141 __traits(getMember, options, member) = 1142 OptionFlag.yes; 1143 else 1144 static assert(false); 1145 } 1146 } 1147 } 1148 } 1149 } 1150 } 1151 1152 if ((config & Config.ignoreUnknown) != Config.ignoreUnknown) 1153 { 1154 // Any left over options are erroneous 1155 for (size_t i = 0; i < args.head.length; ++i) 1156 { 1157 if (!parsed[i] && isOption(args.head[i])) 1158 { 1159 throw new ArgParseError( 1160 "Unknown option '"~ args.head[i] ~"'" 1161 ); 1162 } 1163 } 1164 } 1165 1166 // Left over arguments 1167 auto leftOver = args.head 1168 .enumerate 1169 .filter!(a => !parsed[a[0]]) 1170 .map!(a => a[1]) 1171 .chain(args.tail); 1172 1173 // Only positional arguments are left 1174 foreach (member; __traits(allMembers, T)) 1175 { 1176 alias symbol = Alias!(__traits(getMember, options, member)); 1177 alias argUDAs = getUDAs!(symbol, Argument); 1178 1179 static if (argUDAs.length > 0) 1180 { 1181 // Keep consuming arguments until the multiplicity is satisfied 1182 for (size_t i = 0; i < argUDAs[0].upperBound; ++i) 1183 { 1184 // Out of arguments? 1185 if (leftOver.empty) 1186 { 1187 if (i >= argUDAs[0].lowerBound) 1188 break; // Multiplicity is satisfied 1189 1190 throw new ArgParseError(argUDAs[0].multiplicityError(i)); 1191 } 1192 1193 // Set argument or add to list of arguments. 1194 static if (argUDAs[0].upperBound <= 1) 1195 { 1196 static if (isArgumentHandler!(typeof(symbol))) 1197 __traits(getMember, options, member)(leftOver.front); 1198 else 1199 __traits(getMember, options, member) = 1200 parseArg!(typeof(symbol))(leftOver.front); 1201 } 1202 else 1203 { 1204 static if (isArgumentHandler!(typeof(symbol))) 1205 __traits(getMember, options, member)(leftOver.front); 1206 else 1207 { 1208 import std.range.primitives : ElementType; 1209 __traits(getMember, options, member) ~= 1210 parseArg!(ElementType!(typeof(symbol)))(leftOver.front); 1211 } 1212 } 1213 1214 leftOver.popFront(); 1215 } 1216 } 1217 } 1218 1219 if (!leftOver.empty) 1220 throw new ArgParseError("Too many arguments specified"); 1221 1222 return options; 1223 } 1224 1225 /// 1226 unittest 1227 { 1228 static struct Options 1229 { 1230 string testValue; 1231 1232 @Option("test") 1233 void test(string arg) pure 1234 { 1235 testValue = arg; 1236 } 1237 1238 @Option("help") 1239 @Help("Prints help on command line arguments.") 1240 OptionFlag help; 1241 1242 @Option("version") 1243 @Help("Prints version information.") 1244 OptionFlag version_; 1245 1246 @Argument("path", Multiplicity.oneOrMore) 1247 @Help("Path to the build description.") 1248 string[] path; 1249 1250 @Option("dryrun", "n") 1251 @Help("Don't make any functional changes. Just print what might" ~ 1252 " happen.") 1253 OptionFlag dryRun; 1254 1255 @Option("threads", "j") 1256 @Help("The number of threads to use. Default is the number of" ~ 1257 " logical cores.") 1258 size_t threads; 1259 1260 @Option("color") 1261 @Help("When to colorize the output.") 1262 @MetaVar("{auto,always,never}") 1263 string color = "auto"; 1264 } 1265 1266 immutable options = parseArgs!Options([ 1267 "arg1", 1268 "--version", 1269 "--test", 1270 "test test", 1271 "--dryrun", 1272 "--threads", 1273 "42", 1274 "--color=test", 1275 "--", 1276 "arg2", 1277 ]); 1278 1279 assert(options == Options( 1280 "test test", 1281 OptionFlag.no, 1282 OptionFlag.yes, 1283 ["arg1", "arg2"], 1284 OptionFlag.yes, 1285 42, 1286 "test", 1287 )); 1288 } 1289 1290 /// 1291 unittest 1292 { 1293 static struct Options 1294 { 1295 @Option("help") 1296 @Help("Prints help on command line usage.") 1297 OptionFlag help; 1298 1299 @Option("version") 1300 @Help("Prints version information.") 1301 OptionFlag version_; 1302 1303 @Argument("command", Multiplicity.optional) 1304 @Help("Subcommand") 1305 string command; 1306 1307 @Argument("args", Multiplicity.zeroOrMore) 1308 @Help("Arguments for the command.") 1309 const(string)[] args; 1310 } 1311 1312 immutable options = parseArgs!Options([ 1313 "--version", 1314 "status", 1315 "--asdf", 1316 "blah blah" 1317 ], Config.ignoreUnknown); 1318 1319 assert(options == Options( 1320 OptionFlag.no, 1321 OptionFlag.yes, 1322 "status", 1323 ["--asdf", "blah blah"] 1324 )); 1325 }