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