├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── dub.sdl └── source └── darg.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | *.o 5 | *.obj 6 | __test__library__ 7 | dub.selections.json 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | d: 8 | - dmd 9 | - ldc 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Jason White 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [buildbadge]: https://travis-ci.org/jasonwhite/darg.svg?branch=master 2 | [buildstatus]: https://travis-ci.org/jasonwhite/darg 3 | 4 | # D Argument Parser [![Build Status][buildbadge]][buildstatus] 5 | 6 | Better command line argument parsing using D's powerful compile-time code 7 | generation facilities. 8 | 9 | **Note**: This is a stable library, but it is no longer maintained. If you'd 10 | like to help out with maintanence please make an issue letting me know! 11 | 12 | ## Example 13 | 14 | ```d 15 | import std.stdio; 16 | import darg; 17 | 18 | struct Options 19 | { 20 | @Option("help", "h") 21 | @Help("Prints this help.") 22 | OptionFlag help; 23 | 24 | @Option("threads", "t") 25 | @Help("Number of threads to use.") 26 | size_t threads; 27 | 28 | @Argument("file", Multiplicity.zeroOrMore) 29 | @Help("Input files") 30 | string[] files; 31 | } 32 | 33 | // Generate the usage and help string at compile time. 34 | immutable usage = usageString!Options("example"); 35 | immutable help = helpString!Options; 36 | 37 | int main(string[] args) 38 | { 39 | Options options; 40 | 41 | try 42 | { 43 | options = parseArgs!Options(args[1 .. $]); 44 | } 45 | catch (ArgParseError e) 46 | { 47 | writeln(e.msg); 48 | writeln(usage); 49 | return 1; 50 | } 51 | catch (ArgParseHelp e) 52 | { 53 | // Help was requested 54 | writeln(usage); 55 | write(help); 56 | return 0; 57 | } 58 | 59 | foreach (f; options.files) 60 | { 61 | // Use files 62 | } 63 | 64 | return 0; 65 | } 66 | ``` 67 | 68 | $ ./example --help 69 | Usage: example [--help] [--threads=] [file...] 70 | 71 | Positional arguments: 72 | file Input files 73 | 74 | Optional arguments: 75 | --help, -h Prints this help. 76 | --threads, -t 77 | Number of threads to use. 78 | 79 | $ ./example --foobar 80 | Unknown option '--foobar' 81 | Usage: program [--help] [--threads=] [file...] 82 | 83 | 84 | ## License 85 | 86 | [MIT License](/LICENSE.md) 87 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "darg" 2 | description "D command line argument parser." 3 | copyright "Copyright © 2015-2016, Jason White" 4 | authors "Jason White" 5 | license "MIT" 6 | -------------------------------------------------------------------------------- /source/darg.d: -------------------------------------------------------------------------------- 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. 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") == ""); 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 | } 1328 | --------------------------------------------------------------------------------