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