1 /** 2 * Copyright: Copyright Jason White, 2015-2016 3 * License: MIT 4 * Authors: Jason White 5 * 6 * Description: 7 * Parses arguments. 8 * 9 * TODO: 10 * - Handle bundled options 11 */ 12 module darg; 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. They can also be inverted with the "--no" prefix (e.g., 101 * "--nofoo"). 102 */ 103 enum OptionFlag 104 { 105 no, 106 yes, 107 } 108 109 /** 110 * Multiplicity of an argument. 111 */ 112 enum Multiplicity 113 { 114 optional, 115 zeroOrMore, 116 oneOrMore, 117 } 118 119 /** 120 * User defined attribute for a positional argument. 121 */ 122 struct Argument 123 { 124 /** 125 * Name of the argument. Since this is a positional argument, this value is 126 * only used in the help string. 127 */ 128 string name; 129 130 /** 131 * Lower and upper bounds for the number of values this argument can have. 132 */ 133 size_t lowerBound = 1; 134 135 /// Ditto 136 size_t upperBound = 1; 137 138 /** 139 * Constructs an argument with the given name and count. The count specifies 140 * how many argument elements should be consumed from the command line. By 141 * default, this is 1. 142 */ 143 this(string name, size_t count = 1) pure nothrow 144 body 145 { 146 this.name = name; 147 this.lowerBound = count; 148 this.upperBound = count; 149 } 150 151 /** 152 * Constructs an argument with the given name and an upper and lower bound 153 * for how many argument elements should be consumed from the command line. 154 */ 155 this(string name, size_t lowerBound, size_t upperBound) pure nothrow 156 in { assert(lowerBound < upperBound); } 157 body 158 { 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 /** 309 * Returns true if the given argument is a short option. That is, if it starts 310 * with a '-'. 311 */ 312 bool isShortOption(string arg) pure nothrow 313 { 314 return arg.length > 1 && arg[0] == '-' && arg[1] != '-'; 315 } 316 317 unittest 318 { 319 static assert(!isShortOption("")); 320 static assert(!isShortOption("-")); 321 static assert(!isShortOption("a")); 322 static assert(!isShortOption("ab")); 323 static assert( isShortOption("-a")); 324 static assert( isShortOption("-ab")); 325 static assert(!isShortOption("--a")); 326 static assert(!isShortOption("--abc")); 327 } 328 329 /** 330 * Returns true if the given argument is a long option. That is, if it starts 331 * with "--". 332 */ 333 bool isLongOption(string arg) pure nothrow 334 { 335 return arg.length > 2 && arg[0 .. 2] == "--" && arg[2] != '-'; 336 } 337 338 unittest 339 { 340 static assert(!isLongOption("")); 341 static assert(!isLongOption("a")); 342 static assert(!isLongOption("ab")); 343 static assert(!isLongOption("abc")); 344 static assert(!isLongOption("-")); 345 static assert(!isLongOption("-a")); 346 static assert(!isLongOption("--")); 347 static assert( isLongOption("--a")); 348 static assert( isLongOption("--arg")); 349 static assert( isLongOption("--arg=asdf")); 350 } 351 352 /** 353 * Returns true if the given argument is an option. That is, it is either a 354 * short option or a long option. 355 */ 356 bool isOption(string arg) pure nothrow 357 { 358 return isShortOption(arg) || isLongOption(arg); 359 } 360 361 private static struct OptionSplit 362 { 363 string head; 364 string tail; 365 } 366 367 /** 368 * Splits an option on "=". 369 */ 370 private auto splitOption(string option) pure 371 { 372 size_t i = 0; 373 while (i < option.length && option[i] != '=') 374 ++i; 375 376 return OptionSplit( 377 option[0 .. i], 378 (i < option.length) ? option[i+1 .. $] : null 379 ); 380 } 381 382 unittest 383 { 384 static assert(splitOption("") == OptionSplit("", null)); 385 static assert(splitOption("--foo") == OptionSplit("--foo", null)); 386 static assert(splitOption("--foo=") == OptionSplit("--foo", "")); 387 static assert(splitOption("--foo=bar") == OptionSplit("--foo", "bar")); 388 } 389 390 private static struct ArgSplit 391 { 392 const(string)[] head; 393 const(string)[] tail; 394 } 395 396 /** 397 * Splits arguments on "--". 398 */ 399 private auto splitArgs(const(string)[] args) pure 400 { 401 size_t i = 0; 402 while (i < args.length && args[i] != "--") 403 ++i; 404 405 return ArgSplit( 406 args[0 .. i], 407 (i < args.length) ? args[i+1 .. $] : [] 408 ); 409 } 410 411 unittest 412 { 413 static assert(splitArgs([]) == ArgSplit([], [])); 414 static assert(splitArgs(["a", "b"]) == ArgSplit(["a", "b"], [])); 415 static assert(splitArgs(["a", "--"]) == ArgSplit(["a"], [])); 416 static assert(splitArgs(["a", "--", "b"]) == ArgSplit(["a"], ["b"])); 417 static assert(splitArgs(["a", "--", "b", "c"]) == ArgSplit(["a"], ["b", "c"])); 418 } 419 420 /** 421 * Returns an option name without the leading ("--" or "-"). If it is not an 422 * option, returns null. 423 */ 424 private string optionToName(string option) pure nothrow 425 { 426 if (isLongOption(option)) 427 return option[2 .. $]; 428 429 if (isShortOption(option)) 430 return option[1 .. $]; 431 432 return null; 433 } 434 435 unittest 436 { 437 static assert(optionToName("--opt") == "opt"); 438 static assert(optionToName("-opt") == "opt"); 439 static assert(optionToName("-o") == "o"); 440 static assert(optionToName("opt") is null); 441 static assert(optionToName("o") is null); 442 static assert(optionToName("") is null); 443 } 444 445 /** 446 * Returns the appropriate long or short option corresponding to the given name. 447 */ 448 private string nameToOption(string name) pure nothrow 449 { 450 switch (name.length) 451 { 452 case 0: 453 return null; 454 case 1: 455 return "-" ~ name; 456 default: 457 return "--" ~ name; 458 } 459 } 460 461 unittest 462 { 463 static assert(nameToOption("opt") == "--opt"); 464 static assert(nameToOption("o") == "-o"); 465 static assert(nameToOption("") is null); 466 } 467 468 unittest 469 { 470 immutable identities = ["opt", "o", ""]; 471 472 foreach (identity; identities) 473 assert(identity.nameToOption.optionToName == identity); 474 } 475 476 /** 477 * Check if the given type is valid for an option. 478 */ 479 private template isValidOptionType(T) 480 { 481 import std.traits : isBasicType, isSomeString; 482 483 static if (isBasicType!T || 484 isSomeString!T || 485 is(T : OptionHandler) || 486 is(T : ArgumentHandler) 487 ) 488 { 489 enum isValidOptionType = true; 490 } 491 else static if (is(T A : A[])) 492 { 493 enum isValidOptionType = isValidOptionType!A; 494 } 495 else 496 { 497 enum isValidOptionType = false; 498 } 499 } 500 501 unittest 502 { 503 static assert(isValidOptionType!bool); 504 static assert(isValidOptionType!int); 505 static assert(isValidOptionType!float); 506 static assert(isValidOptionType!char); 507 static assert(isValidOptionType!string); 508 static assert(isValidOptionType!(int[])); 509 510 alias void Func1() pure; 511 alias void Func2(string) pure; 512 alias int Func3(); 513 alias int Func4(string); 514 alias void Func5(); 515 alias void Func6(string); 516 517 static assert(isValidOptionType!Func1); 518 static assert(isValidOptionType!Func2); 519 static assert(!isValidOptionType!Func3); 520 static assert(!isValidOptionType!Func4); 521 static assert(!isValidOptionType!Func5); 522 static assert(!isValidOptionType!Func6); 523 } 524 525 /** 526 * Count the number of positional arguments. 527 */ 528 size_t countArgs(Options)() pure nothrow 529 { 530 import std.traits : Identity, getUDAs; 531 import std.algorithm.comparison : min; 532 533 size_t count = 0; 534 535 foreach (member; __traits(allMembers, Options)) 536 { 537 alias symbol = Identity!(__traits(getMember, Options, member)); 538 alias argUDAs = getUDAs!(symbol, Argument); 539 count += min(argUDAs.length, 1); 540 } 541 542 return count; 543 } 544 545 /** 546 * Count the number of options. 547 */ 548 size_t countOpts(Options)() pure nothrow 549 { 550 import std.traits : Identity, getUDAs; 551 import std.algorithm.comparison : min; 552 553 size_t count = 0; 554 555 foreach (member; __traits(allMembers, Options)) 556 { 557 alias symbol = Identity!(__traits(getMember, Options, member)); 558 alias optUDAs = getUDAs!(symbol, Option); 559 count += min(optUDAs.length, 1); 560 } 561 562 return count; 563 } 564 565 unittest 566 { 567 static struct A {} 568 569 static assert(countArgs!A == 0); 570 static assert(countOpts!A == 0); 571 572 static struct B 573 { 574 @Argument("test1") 575 string test1; 576 @Argument("test2") 577 string test2; 578 579 @Option("test3", "test3a") 580 @Option("test3b", "test3c") 581 string test3; 582 } 583 584 static assert(countArgs!B == 2); 585 static assert(countOpts!B == 1); 586 587 static struct C 588 { 589 string test; 590 591 @Argument("test1") 592 string test1; 593 594 @Argument("test2") 595 @Argument("test2a") 596 string test2; 597 598 @Option("test3") 599 string test3; 600 601 @Option("test4") 602 string test4; 603 } 604 605 static assert(countArgs!C == 2); 606 static assert(countOpts!C == 2); 607 } 608 609 /** 610 * Checks if the given options are valid. 611 */ 612 private void validateOptions(Options)() pure nothrow 613 { 614 import std.traits : Identity, getUDAs, fullyQualifiedName; 615 616 foreach (member; __traits(allMembers, Options)) 617 { 618 alias symbol = Identity!(__traits(getMember, Options, member)); 619 alias optUDAs = getUDAs!(symbol, Option); 620 alias argUDAs = getUDAs!(symbol, Argument); 621 622 // Basic error checking 623 static assert(!(optUDAs.length > 0 && argUDAs.length > 0), 624 fullyQualifiedName!symbol ~" cannot be both an Option and an Argument" 625 ); 626 static assert(optUDAs.length <= 1, 627 fullyQualifiedName!symbol ~" cannot have multiple Option attributes" 628 ); 629 static assert(argUDAs.length <= 1, 630 fullyQualifiedName!symbol ~" cannot have multiple Argument attributes" 631 ); 632 633 static if (argUDAs.length > 0) 634 static assert(isValidOptionType!(typeof(symbol)), 635 fullyQualifiedName!symbol ~" is not a valid Argument type" 636 ); 637 638 static if (optUDAs.length > 0) 639 static assert(isValidOptionType!(typeof(symbol)), 640 fullyQualifiedName!symbol ~" is not a valid Option type" 641 ); 642 } 643 } 644 645 /** 646 * Checks if the given option type has an associated argument. Currently, only 647 * an OptionFlag does not have an argument. 648 */ 649 private template hasArgument(T) 650 { 651 static if (is(T : OptionFlag) || is(T : OptionHandler)) 652 enum hasArgument = false; 653 else 654 enum hasArgument = true; 655 } 656 657 unittest 658 { 659 static assert(hasArgument!string); 660 static assert(hasArgument!ArgumentHandler); 661 static assert(hasArgument!int); 662 static assert(hasArgument!bool); 663 static assert(!hasArgument!OptionFlag); 664 static assert(!hasArgument!OptionHandler); 665 } 666 667 /** 668 * Parses an argument. 669 * 670 * Throws: ArgParseError if the given argument cannot be converted to the 671 * requested type. 672 */ 673 T parseArg(T)(string arg) pure 674 { 675 import std.conv : to, ConvException; 676 677 try 678 { 679 return to!T(arg); 680 } 681 catch (ConvException e) 682 { 683 throw new ArgParseError(e.msg); 684 } 685 } 686 687 unittest 688 { 689 import std.exception : ce = collectException; 690 691 assert(parseArg!int("42") == 42); 692 assert(parseArg!string("42") == "42"); 693 assert(ce!ArgParseError(parseArg!size_t("-42"))); 694 } 695 696 /** 697 * Returns the canonical name of the given member's argument. If there is no 698 * argument for the given member, null is returned. 699 */ 700 private string argumentName(Options, string member)() pure 701 { 702 import std.traits : Identity, getUDAs; 703 import std.string : toUpper; 704 705 alias symbol = Identity!(__traits(getMember, Options, member)); 706 707 static if (hasArgument!(typeof(symbol))) 708 { 709 alias metavar = getUDAs!(symbol, MetaVar); 710 static if (metavar.length > 0) 711 return metavar[0].name; 712 else static if (is(typeof(symbol) : ArgumentHandler)) 713 return member.toUpper; 714 else 715 return "<"~ typeof(symbol).stringof ~ ">"; 716 } 717 else 718 { 719 return null; 720 } 721 } 722 723 unittest 724 { 725 static struct Options 726 { 727 @Option("test1") 728 OptionFlag test1; 729 730 @Option("test2") 731 string test2; 732 733 @Option("test3") 734 @MetaVar("asdf") 735 string test3; 736 737 private string _arg; 738 739 @Option("test4") 740 void test4(string arg) pure 741 { 742 _arg = arg; 743 } 744 745 @Option("test5") 746 @MetaVar("metavar") 747 void test5(string arg) pure 748 { 749 _arg = arg; 750 } 751 } 752 753 static assert(argumentName!(Options, "test1") == null); 754 static assert(argumentName!(Options, "test2") == "<string>"); 755 static assert(argumentName!(Options, "test3") == "asdf"); 756 static assert(argumentName!(Options, "test4") == "TEST4"); 757 static assert(argumentName!(Options, "test5") == "metavar"); 758 } 759 760 /** 761 * Constructs a printable usage string at compile time from the given options 762 * structure. 763 */ 764 string usageString(Options)(string program) pure 765 if (is(Options == struct)) 766 { 767 import std.traits : Identity, getUDAs; 768 import std.array : replicate; 769 import std.string : wrap, toUpper; 770 771 string output = "Usage: "~ program; 772 773 string indent = " ".replicate(output.length + 1); 774 775 // List all options 776 foreach (member; __traits(allMembers, Options)) 777 { 778 alias symbol = Identity!(__traits(getMember, Options, member)); 779 alias optUDAs = getUDAs!(symbol, Option); 780 781 static if (optUDAs.length > 0 && optUDAs[0].names.length > 0) 782 { 783 output ~= " ["~ nameToOption(optUDAs[0].names[0]); 784 785 if (immutable name = argumentName!(Options, member)) 786 output ~= "="~ name; 787 788 output ~= "]"; 789 } 790 } 791 792 // List all arguments 793 foreach (member; __traits(allMembers, Options)) 794 { 795 alias symbol = Identity!(__traits(getMember, Options, member)); 796 alias argUDAs = getUDAs!(symbol, Argument); 797 798 static if (argUDAs.length > 0) 799 output ~= " "~ argUDAs[0].usage; 800 } 801 802 return output.wrap(80, null, indent, 4); 803 } 804 805 /** 806 * Generates a help string for a single argument. Returns null if the given 807 * member is not an argument. 808 */ 809 private string argumentHelp(Options, string member)(size_t padding = 14) pure 810 { 811 import std.traits : Identity, getUDAs; 812 import std.array : replicate; 813 import std.string : wrap; 814 815 string output; 816 817 alias symbol = Identity!(__traits(getMember, Options, member)); 818 alias argUDAs = getUDAs!(symbol, Argument); 819 820 static if (argUDAs.length > 0) 821 { 822 enum name = argUDAs[0].name; 823 output ~= " "~ name; 824 825 alias helpUDAs = getUDAs!(symbol, Help); 826 static if (helpUDAs.length > 0) 827 { 828 if (name.length > padding) 829 output ~= "\n"; 830 831 immutable indent = " ".replicate(padding + 3); 832 immutable firstIndent = (name.length > padding) ? indent : 833 " ".replicate(padding - name.length + 2); 834 835 output ~= helpUDAs[0].help.wrap(80, firstIndent, indent, 4); 836 } 837 else 838 { 839 output ~= "\n"; 840 } 841 } 842 843 return output; 844 } 845 846 /** 847 * Generates a help string for a single option. Returns null if the given member 848 * is not an option. 849 */ 850 private string optionHelp(Options, string member)(size_t padding = 14) pure 851 { 852 import std.traits : Identity, getUDAs; 853 import std.array : replicate; 854 import std.string : wrap; 855 856 string output; 857 858 alias symbol = Identity!(__traits(getMember, Options, member)); 859 alias optUDAs = getUDAs!(symbol, Option); 860 861 static if (optUDAs.length > 0) 862 { 863 enum names = optUDAs[0].names; 864 static if (names.length > 0) 865 { 866 output ~= " " ~ nameToOption(names[0]); 867 868 foreach (name; names[1 .. $]) 869 output ~= ", " ~ nameToOption(name); 870 871 if (string arg = argumentName!(Options, member)) 872 output ~= " " ~ arg; 873 874 immutable len = output.length; 875 876 alias helpUDAs = getUDAs!(symbol, Help); 877 static if (helpUDAs.length > 0) 878 { 879 immutable overflow = len > padding+1; 880 if (overflow) 881 output ~= "\n"; 882 883 immutable indent = " ".replicate(padding+3); 884 immutable firstIndent = overflow 885 ? indent : " ".replicate((padding + 1) - len + 2); 886 887 output ~= helpUDAs[0].help.wrap(80, firstIndent, indent, 4); 888 } 889 else 890 { 891 output ~= "\n"; 892 } 893 } 894 } 895 896 return output; 897 } 898 899 /** 900 * Constructs a printable help string at compile time for the given options 901 * structure. 902 */ 903 string helpString(Options)(string description = null) pure 904 if (is(Options == struct)) 905 { 906 import std.format : format; 907 import std.string : wrap; 908 909 string output; 910 911 if (description) 912 output ~= description.wrap(80) ~ "\n"; 913 914 // List all positional arguments. 915 static if(countArgs!Options > 0) 916 { 917 output ~= "Positional arguments:\n"; 918 919 foreach (member; __traits(allMembers, Options)) 920 output ~= argumentHelp!(Options, member); 921 922 static if (countOpts!Options > 0) 923 output ~= "\n"; 924 } 925 926 // List all options 927 static if (countOpts!Options > 0) 928 { 929 output ~= "Optional arguments:\n"; 930 931 foreach (member; __traits(allMembers, Options)) 932 output ~= optionHelp!(Options, member); 933 } 934 935 return output; 936 } 937 938 /** 939 * Parsing configuration. 940 */ 941 enum Config 942 { 943 /** 944 * Enables option bundling. That is, multiple single character options can 945 * be bundled together. 946 */ 947 bundling = 1 << 0, 948 949 /** 950 * Ignore unknown options. These are then parsed as positional arguments. 951 */ 952 ignoreUnknown = 1 << 1, 953 954 /** 955 * Throw the ArgParseHelp exception when the option "help" is specified. 956 * This requires the option to exist in the options struct. 957 */ 958 handleHelp = 1 << 2, 959 960 /** 961 * Default configuration options. 962 */ 963 default_ = bundling | handleHelp, 964 } 965 966 /** 967 * Parses options from the given list of arguments. Note that the first argument 968 * is assumed to be the program name and is ignored. 969 * 970 * Returns: Options structure filled out with values. 971 * 972 * Throws: ArgParseError if arguments are invalid. 973 */ 974 T parseArgs(T)( 975 const(string[]) arguments, 976 Config config = Config.default_ 977 ) pure 978 if (is(T == struct)) 979 { 980 import std.traits : Identity, getUDAs; 981 import std.format : format; 982 import std.range : chain, enumerate, empty, front, popFront; 983 import std.algorithm.iteration : map, filter; 984 985 validateOptions!T; 986 987 T options; 988 989 auto args = splitArgs(arguments); 990 991 // Arguments that have been parsed 992 bool[] parsed; 993 parsed.length = args.head.length; 994 995 // Parsing occurs in two passes: 996 // 997 // 1. Parse all options 998 // 2. Parse all positional arguments 999 // 1000 // After the first pass, only positional arguments and invalid options will 1001 // be left. 1002 1003 for (size_t i = 0; i < args.head.length; ++i) 1004 { 1005 auto opt = splitOption(args.head[i]); 1006 1007 if (immutable name = optionToName(opt.head)) 1008 { 1009 foreach (member; __traits(allMembers, T)) 1010 { 1011 alias symbol = Identity!(__traits(getMember, options, member)); 1012 alias optUDAs = getUDAs!(symbol, Option); 1013 1014 static if (optUDAs.length > 0) 1015 { 1016 if (optUDAs[0] == name) 1017 { 1018 parsed[i] = true; 1019 1020 static if (hasArgument!(typeof(symbol))) 1021 { 1022 if (opt.tail) 1023 { 1024 static if (is(typeof(symbol) : ArgumentHandler)) 1025 __traits(getMember, options, member)(opt.tail); 1026 else 1027 __traits(getMember, options, member) = 1028 parseArg!(typeof(symbol))(opt.tail); 1029 } 1030 else 1031 { 1032 ++i; 1033 1034 if (i >= args.head.length || isOption(args.head[i])) 1035 throw new ArgParseError( 1036 "Expected argument for option '%s'" 1037 .format(opt.head) 1038 ); 1039 1040 static if (is(typeof(symbol) : ArgumentHandler)) 1041 __traits(getMember, options, member)(args.head[i]); 1042 else 1043 __traits(getMember, options, member) = 1044 parseArg!(typeof(symbol))(args.head[i]); 1045 1046 parsed[i] = true; 1047 } 1048 } 1049 else 1050 { 1051 if (opt.tail) 1052 throw new ArgParseError( 1053 "Option '%s' does not take an argument" 1054 .format(opt.head) 1055 ); 1056 1057 // Handle a request for help 1058 if ((config & Config.handleHelp) == 1059 Config.handleHelp && optUDAs[0] == "help") 1060 throw new ArgParseHelp(""); 1061 1062 static if (is(typeof(symbol) : OptionHandler)) 1063 __traits(getMember, options, member)(); 1064 else static if (is(typeof(symbol) : OptionFlag)) 1065 __traits(getMember, options, member) = 1066 OptionFlag.yes; 1067 else 1068 static assert(false); 1069 } 1070 } 1071 } 1072 } 1073 } 1074 } 1075 1076 if ((config & Config.ignoreUnknown) != Config.ignoreUnknown) 1077 { 1078 // Any left over options are erroneous 1079 for (size_t i = 0; i < args.head.length; ++i) 1080 { 1081 if (!parsed[i] && isOption(args.head[i])) 1082 { 1083 throw new ArgParseError( 1084 "Unknown option '"~ args.head[i] ~"'" 1085 ); 1086 } 1087 } 1088 } 1089 1090 // Left over arguments 1091 auto leftOver = args.head 1092 .enumerate 1093 .filter!(a => !parsed[a[0]]) 1094 .map!(a => a[1]) 1095 .chain(args.tail); 1096 1097 // Only positional arguments are left 1098 foreach (member; __traits(allMembers, T)) 1099 { 1100 alias symbol = Identity!(__traits(getMember, options, member)); 1101 alias argUDAs = getUDAs!(symbol, Argument); 1102 1103 static if (argUDAs.length > 0) 1104 { 1105 // Keep consuming arguments until the multiplicity is satisfied 1106 for (size_t i = 0; i < argUDAs[0].upperBound; ++i) 1107 { 1108 // Out of arguments? 1109 if (leftOver.empty) 1110 { 1111 if (i >= argUDAs[0].lowerBound) 1112 break; // Multiplicity is satisfied 1113 1114 throw new ArgParseError(argUDAs[0].multiplicityError(i)); 1115 } 1116 1117 // Set argument or add to list of arguments. 1118 static if (argUDAs[0].upperBound <= 1) 1119 { 1120 static if (is(typeof(symbol) : ArgumentHandler)) 1121 __traits(getMember, options, member)(leftOver.front); 1122 else 1123 __traits(getMember, options, member) = 1124 parseArg!(typeof(symbol))(leftOver.front); 1125 } 1126 else 1127 { 1128 static if (is(typeof(symbol) : ArgumentHandler)) 1129 __traits(getMember, options, member)(leftOver.front); 1130 else 1131 { 1132 import std.range.primitives : ElementType; 1133 __traits(getMember, options, member) ~= 1134 parseArg!(ElementType!(typeof(symbol)))(leftOver.front); 1135 } 1136 } 1137 1138 leftOver.popFront(); 1139 } 1140 } 1141 } 1142 1143 if (!leftOver.empty) 1144 throw new ArgParseError("Too many arguments specified"); 1145 1146 return options; 1147 } 1148 1149 /// 1150 unittest 1151 { 1152 static struct Options 1153 { 1154 string testValue; 1155 1156 @Option("test") 1157 void test(string arg) pure 1158 { 1159 testValue = arg; 1160 } 1161 1162 @Option("help") 1163 @Help("Prints help on command line arguments.") 1164 OptionFlag help; 1165 1166 @Option("version") 1167 @Help("Prints version information.") 1168 OptionFlag version_; 1169 1170 @Argument("path", Multiplicity.oneOrMore) 1171 @Help("Path to the build description.") 1172 string[] path; 1173 1174 @Option("dryrun", "n") 1175 @Help("Don't make any functional changes. Just print what might" ~ 1176 " happen.") 1177 OptionFlag dryRun; 1178 1179 @Option("threads", "j") 1180 @Help("The number of threads to use. Default is the number of" ~ 1181 " logical cores.") 1182 size_t threads; 1183 1184 @Option("color") 1185 @Help("When to colorize the output.") 1186 @MetaVar("{auto,always,never}") 1187 string color = "auto"; 1188 } 1189 1190 immutable options = parseArgs!Options([ 1191 "arg1", 1192 "--help", 1193 "--version", 1194 "--test", 1195 "test test", 1196 "--dryrun", 1197 "--threads", 1198 "42", 1199 "--color=test", 1200 "--", 1201 "arg2", 1202 ]); 1203 1204 assert(options == Options( 1205 "test test", 1206 OptionFlag.yes, 1207 OptionFlag.yes, 1208 ["arg1", "arg2"], 1209 OptionFlag.yes, 1210 42, 1211 "test", 1212 )); 1213 } 1214 1215 /// 1216 unittest 1217 { 1218 static struct Options 1219 { 1220 @Option("help") 1221 @Help("Prints help on command line usage.") 1222 OptionFlag help; 1223 1224 @Option("version") 1225 @Help("Prints version information.") 1226 OptionFlag version_; 1227 1228 @Argument("command", Multiplicity.optional) 1229 @Help("Subcommand") 1230 string command; 1231 1232 @Argument("args", Multiplicity.zeroOrMore) 1233 @Help("Arguments for the command.") 1234 const(string)[] args; 1235 } 1236 1237 immutable options = parseArgs!Options([ 1238 "--version", 1239 "status", 1240 "--asdf", 1241 "blah blah" 1242 ], Config.ignoreUnknown); 1243 1244 assert(options == Options( 1245 OptionFlag.no, 1246 OptionFlag.yes, 1247 "status", 1248 ["--asdf", "blah blah"] 1249 )); 1250 }