1 /** 2 * Program data model 3 * 4 * This module along with `commandr.option` contains all types needed to build 5 * your program model - program options, flags, arguments and all subcommands. 6 * 7 * After creating your program model, you can use it to: 8 * - parse the arguments with `parse` or `parseArgs` 9 * - print help with `printHelp` or just the usage with `printUsage` 10 * - create completion script with `createBashCompletionScript` 11 * 12 * Examples: 13 * --- 14 * auto program = new Program("grit") 15 * .add(new Flag("v", "verbose", "verbosity")) 16 * .add(new Command("branch", "branch management") 17 * .add(new Command("add", "adds branch") 18 * .add(new Argument("name")) 19 * ) 20 * .add(new Command("rm", "removes branch") 21 * .add(new Argument("name")) 22 * ) 23 * ) 24 * ; 25 * --- 26 * 27 * See_Also: 28 * `Command`, `Program`, `parse` 29 */ 30 module commandr.program; 31 32 import commandr.option; 33 import commandr.utils; 34 import std.algorithm : all, reverse, map, filter; 35 import std.ascii : isAlphaNum; 36 import std.array : array; 37 import std.range : empty, chainRanges = chain; 38 import std.string : format; 39 40 41 /** 42 * Thrown when program definition contains error. 43 * 44 * Errors include (but not limited to): duplicate entry name, option with no short and no long value. 45 */ 46 public class InvalidProgramException : Exception { 47 /// Creates new instance of InvalidProgramException 48 public this(string msg) nothrow pure @safe { 49 super(msg); 50 } 51 } 52 53 /** 54 * Represents a command. 55 * 56 * Commands contain basic information such as name, version summary as well as 57 * flags, options, arguments and sub-commands. 58 * 59 * `Program` is a `Command` as well, thus all methods are available in `Program`. 60 * 61 * See_Also: 62 * `Program` 63 */ 64 public class Command { 65 private string _name; 66 private string _version; 67 private string _summary; 68 private string _topic; 69 private string _topicStart; 70 private Object[string] _nameMap; 71 private Flag[] _flags; 72 private Option[] _options; 73 private Argument[] _arguments; 74 private Command[string] _commands; 75 private Command _parent; 76 private string _defaultCommand; 77 78 /** 79 * Creates new instance of Command. 80 * 81 * Params: 82 * name - command name 83 * summary - command summary (one-liner) 84 * version_ - command version 85 */ 86 public this(string name, string summary = null, string version_ = "1.0") pure @safe { 87 this._name = name; 88 this._summary = summary; 89 this._version = version_; 90 this.add(new Flag("h", "help", "prints help")); 91 } 92 93 /** 94 * Sets command name 95 * 96 * Params: 97 * name - unique name 98 */ 99 public typeof(this) name(string name) nothrow pure @nogc @safe { 100 this._name = name; 101 return this; 102 } 103 104 /** 105 * Program name 106 */ 107 public string name() nothrow pure @nogc @safe { 108 return this._name; 109 } 110 111 /** 112 * Sets command version 113 */ 114 public typeof(this) version_(string version_) nothrow pure @nogc @safe { 115 this._version = version_; 116 return this; 117 } 118 119 /** 120 * Program version 121 */ 122 public string version_() nothrow pure @nogc @safe { 123 return this._version; 124 } 125 126 /** 127 * Sets program summary (one-liner) 128 */ 129 public typeof(this) summary(string summary) nothrow pure @nogc @safe { 130 this._summary = summary; 131 return this; 132 } 133 134 /** 135 * Program summary 136 */ 137 public string summary() nothrow pure @nogc @safe { 138 return this._summary; 139 } 140 141 142 /** 143 * Adds option 144 * 145 * Throws: 146 * `InvalidProgramException` 147 */ 148 public typeof(this) add(Option option) pure @safe { 149 validateName(option.name); 150 validateOption(option); 151 152 if (option.isRequired && option.defaultValue) { 153 throw new InvalidProgramException("cannot have required option with default value"); 154 } 155 156 _options ~= option; 157 _nameMap[option.name] = option; 158 return this; 159 } 160 161 /** 162 * Command options 163 */ 164 public Option[] options() nothrow pure @nogc @safe { 165 return this._options; 166 } 167 168 /** 169 * Adds command flag 170 * 171 * Throws: 172 * `InvalidProgramException` 173 */ 174 public typeof(this) add(Flag flag) pure @safe { 175 validateName(flag.name); 176 validateOption(flag); 177 178 if (flag.defaultValue) { 179 throw new InvalidProgramException("flag %s cannot have default value".format(flag.name)); 180 } 181 182 if (flag.isRequired) { 183 throw new InvalidProgramException("flag %s cannot be required".format(flag.name)); 184 } 185 186 if (flag.validators) { 187 throw new InvalidProgramException("flag %s cannot have validators".format(flag.name)); 188 } 189 190 _flags ~= flag; 191 _nameMap[flag.name] = flag; 192 return this; 193 } 194 195 /** 196 * Command flags 197 */ 198 public Flag[] flags() nothrow pure @nogc @safe { 199 return this._flags; 200 } 201 202 /** 203 * Adds command argument 204 * 205 * Throws: 206 * `InvalidProgramException` 207 */ 208 public typeof(this) add(Argument argument) pure @safe { 209 validateName(argument.name); 210 211 if (_arguments.length && _arguments[$-1].isRepeating) { 212 throw new InvalidProgramException("cannot add arguments past repeating"); 213 } 214 215 if (argument.isRequired && _arguments.length > 0 && !_arguments[$-1].isRequired) { 216 throw new InvalidProgramException("cannot add required argument past optional one"); 217 } 218 219 if (argument.isRequired && argument.defaultValue) { 220 throw new InvalidProgramException("cannot have required argument with default value"); 221 } 222 223 this._arguments ~= argument; 224 _nameMap[argument.name] = argument; 225 return this; 226 } 227 228 /** 229 * Command arguments 230 */ 231 public Argument[] arguments() nothrow pure @nogc @safe { 232 return this._arguments; 233 } 234 235 /** 236 * Registers subcommand 237 * 238 * Throws: 239 * `InvalidProgramException` 240 */ 241 public typeof(this) add(Command command) pure @safe { 242 if (command.name in this._commands) { 243 throw new InvalidProgramException("duplicate command %s".format(command.name)); 244 } 245 246 // this is also checked by adding argument, but we want better error message 247 if (!_arguments.empty && _arguments[$-1].isRepeating) { 248 throw new InvalidProgramException("cannot have sub-commands and repeating argument"); 249 } 250 251 if (!_arguments.empty && !_arguments[$-1].isRequired) { 252 throw new InvalidProgramException("cannot have sub-commands and non-required argument"); 253 } 254 255 command._topic = this._topicStart; 256 command._parent = this; 257 _commands[command.name] = command; 258 259 return this; 260 } 261 262 /** 263 * Command sub-commands 264 */ 265 public Command[string] commands() nothrow pure @nogc @safe { 266 return this._commands; 267 } 268 269 /** 270 * Sets default command. 271 */ 272 public typeof(this) defaultCommand(string name) pure @safe { 273 if (name !is null) { 274 if ((name in _commands) is null) { 275 throw new InvalidProgramException("setting default command to non-existing one"); 276 } 277 } 278 this._defaultCommand = name; 279 280 return this; 281 } 282 283 /** 284 * Gets default command 285 */ 286 public string defaultCommand() nothrow pure @safe @nogc { 287 return this._defaultCommand; 288 } 289 290 public typeof(this) topicGroup(string topic) pure @safe { 291 this._topicStart = topic; 292 return this; 293 } 294 295 public typeof(this) topic(string topic) nothrow pure @safe @nogc { 296 this._topic = topic; 297 return this; 298 } 299 300 public string topic() nothrow pure @safe @nogc { 301 return _topic; 302 } 303 304 /** 305 * Gets command chain. 306 * 307 * Chain is a array of strings which contains all parent command names. 308 * For a deeply nested sub command like `git branch add`, `add` sub-command 309 * chain would return `["git", "branch", "add"]`. 310 */ 311 public string[] chain() pure nothrow @safe { 312 string[] chain = [this.name]; 313 Command curr = this._parent; 314 while (curr !is null) { 315 chain ~= curr.name; 316 curr = curr._parent; 317 } 318 319 chain.reverse(); 320 return chain; 321 } 322 323 public Command parent() nothrow pure @safe @nogc { 324 return _parent; 325 } 326 327 public string[] fullNames() nothrow pure @safe { 328 return chainRanges( 329 _flags.map!(f => f.full), 330 _options.map!(o => o.full) 331 ).filter!`a && a.length`.array; 332 } 333 334 public string[] abbrevations() nothrow pure @safe { 335 return chainRanges( 336 _flags.map!(f => f.abbrev), 337 _options.map!(o => o.abbrev) 338 ).filter!`a && a.length`.array; 339 } 340 341 private void addBasicOptions() { 342 this.add(new Flag(null, "version", "prints version")); 343 } 344 345 private void validateName(string name) pure @safe { 346 if (!name) { 347 throw new InvalidProgramException("name cannot be empty"); 348 } 349 350 if (name[0] == '-') 351 throw new InvalidProgramException("invalid name '%s' -- cannot begin with '-'".format(name)); 352 353 if (!name.all!(c => isAlphaNum(c) || c == '_' || c == '-')()) { 354 throw new InvalidProgramException("invalid name '%s' passed".format(name)); 355 } 356 357 auto entryPtr = name in _nameMap; 358 if (entryPtr !is null) { 359 throw new InvalidProgramException( 360 "duplicate name %s which is already used".format(name) 361 ); 362 } 363 } 364 365 private void validateOption(IOption option) pure @safe { 366 if (!option.abbrev && !option.full) { 367 throw new InvalidProgramException( 368 "option/flag %s must have either long or short form".format(option.name) 369 ); 370 } 371 372 if (option.abbrev) { 373 auto flag = this.getFlagByShort(option.abbrev); 374 if (!flag.isNull) { 375 throw new InvalidProgramException( 376 "duplicate abbrevation -%s, flag %s already uses it".format(option.abbrev, flag.get().name) 377 ); 378 } 379 380 auto other = this.getOptionByShort(option.abbrev); 381 if (!other.isNull) { 382 throw new InvalidProgramException( 383 "duplicate abbrevation -%s, option %s already uses it".format(option.abbrev, other.get().name) 384 ); 385 } 386 } 387 388 if (option.full) { 389 auto flag = this.getFlagByFull(option.full); 390 if (!flag.isNull) { 391 throw new InvalidProgramException( 392 "duplicate -%s, flag %s with this already exists".format(option.full, flag.get().name) 393 ); 394 } 395 396 auto other = this.getOptionByFull(option.full); 397 if (!other.isNull) { 398 throw new InvalidProgramException( 399 "duplicate --%s, option %s with this already exists".format(option.full, other.get().name) 400 ); 401 } 402 } 403 404 if (option.isRequired && option.defaultValue) { 405 throw new InvalidProgramException("cannot have required option with default value"); 406 } 407 } 408 } 409 410 /** 411 * Represents program. 412 * 413 * This is the entry-point for building your program model. 414 */ 415 public class Program: Command { 416 private string _binaryName; 417 private string[] _authors; 418 419 /** 420 * Creates new instance of `Program`. 421 * 422 * Params: 423 * name - Program name 424 * version_ - Program version 425 */ 426 public this(string name, string version_ = "1.0") { 427 super(name, null, version_); 428 this.addBasicOptions(); 429 } 430 431 /** 432 * Sets program name 433 */ 434 public override typeof(this) name(string name) nothrow pure @nogc @safe { 435 return cast(Program)super.name(name); 436 } 437 438 /** 439 * Program name 440 */ 441 public override string name() const nothrow pure @nogc @safe { 442 return this._name; 443 } 444 445 /** 446 * Sets program version 447 */ 448 public override typeof(this) version_(string version_) nothrow pure @nogc @safe { 449 return cast(Program)super.version_(version_); 450 } 451 452 /** 453 * Program version 454 */ 455 public override string version_() const nothrow pure @nogc @safe { 456 return this._version; 457 } 458 459 /** 460 * Sets program summary (one-liner) 461 */ 462 public override typeof(this) summary(string summary) nothrow pure @nogc @safe { 463 return cast(Program)super.summary(summary); 464 } 465 466 /** 467 * Program summary (one-liner) 468 */ 469 public override string summary() nothrow pure @nogc @safe { 470 return this._summary; 471 } 472 473 /// Proxy call to `Command.add` returning `Program`. 474 public typeof(this) add(T: IEntry)(T data) pure @safe { 475 super.add(data); 476 return this; 477 } 478 479 public override typeof(this) add(Command command) pure @safe { 480 super.add(command); 481 return this; 482 } 483 484 public override typeof(this) defaultCommand(string name) pure @safe { 485 super.defaultCommand(name); 486 return this; 487 } 488 489 public override string defaultCommand() nothrow pure @safe @nogc { 490 return this._defaultCommand; 491 } 492 493 /** 494 * Sets program binary name 495 */ 496 public typeof(this) binaryName(string binaryName) nothrow pure @nogc @safe { 497 this._binaryName = binaryName; 498 return this; 499 } 500 501 /** 502 * Program binary name 503 */ 504 public string binaryName() const nothrow pure @nogc @safe { 505 return (this._binaryName !is null) ? this._binaryName : this._name; 506 } 507 508 /** 509 * Adds program author 510 */ 511 public typeof(this) author(string author) nothrow pure @safe { 512 this._authors ~= author; 513 return this; 514 } 515 516 /** 517 * Sets program authors 518 */ 519 public typeof(this) authors(string[] authors) nothrow pure @nogc @safe { 520 this._authors = authors; 521 return this; 522 } 523 524 /** 525 * Program authors 526 */ 527 public string[] authors() nothrow pure @nogc @safe { 528 return this._authors; 529 } 530 531 /** 532 * Sets topic group for the following commands. 533 */ 534 public override typeof(this) topicGroup(string topic) pure @safe { 535 super.topicGroup(topic); 536 return this; 537 } 538 539 /** 540 * Sets topic group for this command. 541 */ 542 public override typeof(this) topic(string topic) nothrow pure @safe @nogc { 543 super.topic(topic); 544 return this; 545 } 546 547 /** 548 * Topic group for this command. 549 */ 550 public override string topic() nothrow pure @safe @nogc { 551 return _topic; 552 } 553 } 554 555 unittest { 556 import std.range : empty; 557 558 auto program = new Program("test"); 559 assert(program.name == "test"); 560 assert(program.binaryName == "test"); 561 assert(program.version_ == "1.0"); 562 assert(program.summary is null); 563 assert(program.authors.empty); 564 assert(program.flags.length == 2); 565 assert(program.flags[0].name == "help"); 566 assert(program.flags[0].abbrev == "h"); 567 assert(program.flags[0].full == "help"); 568 assert(program.flags[1].name == "version"); 569 assert(program.flags[1].abbrev is null); 570 assert(program.flags[1].full == "version"); 571 assert(program.options.empty); 572 assert(program.arguments.empty); 573 } 574 575 unittest { 576 auto program = new Program("test").name("bar"); 577 assert(program.name == "bar"); 578 assert(program.binaryName == "bar"); 579 } 580 581 unittest { 582 auto program = new Program("test", "0.1"); 583 assert(program.version_ == "0.1"); 584 } 585 586 unittest { 587 auto program = new Program("test", "0.1").version_("2.0").version_("kappa"); 588 assert(program.version_ == "kappa"); 589 } 590 591 unittest { 592 auto program = new Program("test").binaryName("kappa"); 593 assert(program.name == "test"); 594 assert(program.binaryName == "kappa"); 595 } 596 597 // name conflicts 598 unittest { 599 import std.exception : assertThrown; 600 601 // FLAGS 602 // flag-flag 603 assertThrown!InvalidProgramException( 604 new Program("test") 605 .add(new Flag("a", "aaa", "desc").name("nnn")) 606 .add(new Flag("b", "bbb", "desc").name("nnn")) 607 ); 608 609 // flag-option 610 assertThrown!InvalidProgramException( 611 new Program("test") 612 .add(new Flag("a", "aaa", "desc").name("nnn")) 613 .add(new Option("b", "bbb", "desc").name("nnn")) 614 ); 615 616 // flag-argument 617 assertThrown!InvalidProgramException( 618 new Program("test") 619 .add(new Flag("a", "aaa", "desc").name("nnn")) 620 .add(new Argument("nnn")) 621 ); 622 623 624 // OPTIONS 625 // option-flag 626 assertThrown!InvalidProgramException( 627 new Program("test") 628 .add(new Option("a", "aaa", "desc").name("nnn")) 629 .add(new Flag("b", "bbb", "desc").name("nnn")) 630 ); 631 632 // option-option 633 assertThrown!InvalidProgramException( 634 new Program("test") 635 .add(new Option("a", "aaa", "desc").name("nnn")) 636 .add(new Option("b", "bbb", "desc").name("nnn")) 637 ); 638 639 // option-argument 640 assertThrown!InvalidProgramException( 641 new Program("test") 642 .add(new Option("a", "aaa", "desc").name("nnn")) 643 .add(new Argument("nnn")) 644 ); 645 646 647 // ARGUMENTS 648 // argument-flag 649 assertThrown!InvalidProgramException( 650 new Program("test") 651 .add(new Argument("nnn")) 652 .add(new Flag("b", "bbb", "desc").name("nnn")) 653 ); 654 655 // argument-option 656 assertThrown!InvalidProgramException( 657 new Program("test") 658 .add(new Argument("nnn")) 659 .add(new Option("b", "bbb", "desc").name("nnn")) 660 ); 661 662 // argument-argument 663 assertThrown!InvalidProgramException( 664 new Program("test") 665 .add(new Argument("nnn")) 666 .add(new Argument("nnn")) 667 ); 668 } 669 670 // abbrev conflicts 671 unittest { 672 import std.exception : assertThrown; 673 674 // FLAGS 675 // flag-flag 676 assertThrown!InvalidProgramException( 677 new Program("test") 678 .add(new Flag("a", "aaa", "desc")) 679 .add(new Flag("a", "bbb", "desc")) 680 ); 681 682 // flag-option 683 assertThrown!InvalidProgramException( 684 new Program("test") 685 .add(new Flag("a", "aaa", "desc")) 686 .add(new Option("a", "bbb", "desc")) 687 ); 688 689 // FLAGS 690 // option-flag 691 assertThrown!InvalidProgramException( 692 new Program("test") 693 .add(new Option("a", "aaa", "desc")) 694 .add(new Flag("a", "bbb", "desc")) 695 ); 696 697 // option-option 698 assertThrown!InvalidProgramException( 699 new Program("test") 700 .add(new Option("a", "aaa", "desc")) 701 .add(new Option("a", "bbb", "desc")) 702 ); 703 } 704 705 // full name conflicts 706 unittest { 707 import std.exception : assertThrown; 708 709 // FLAGS 710 // flag-flag 711 assertThrown!InvalidProgramException( 712 new Program("test") 713 .add(new Flag("a", "aaa", "desc")) 714 .add(new Flag("b", "aaa", "desc")) 715 ); 716 717 // flag-option 718 assertThrown!InvalidProgramException( 719 new Program("test") 720 .add(new Flag("a", "aaa", "desc")) 721 .add(new Option("b", "aaa", "desc")) 722 ); 723 724 // FLAGS 725 // option-flag 726 assertThrown!InvalidProgramException( 727 new Program("test") 728 .add(new Option("a", "aaa", "desc")) 729 .add(new Flag("b", "aaa", "desc")) 730 ); 731 732 // option-option 733 assertThrown!InvalidProgramException( 734 new Program("test") 735 .add(new Option("a", "aaa", "desc")) 736 .add(new Option("b", "aaa", "desc")) 737 ); 738 } 739 740 // repeating 741 unittest { 742 import std.exception : assertThrown; 743 744 assertThrown!InvalidProgramException( 745 new Program("test") 746 .add(new Argument("file", "path").repeating) 747 .add(new Argument("dir", "desc")) 748 ); 749 } 750 751 // invalid option 752 unittest { 753 import std.exception : assertThrown; 754 755 assertThrown!InvalidProgramException( 756 new Program("test") 757 .add(new Flag(null, null, "")) 758 ); 759 760 assertThrown!InvalidProgramException( 761 new Program("test") 762 .add(new Option(null, null, "")) 763 ); 764 } 765 766 // required args out of order 767 unittest { 768 import std.exception : assertThrown; 769 770 assertThrown!InvalidProgramException( 771 new Program("test") 772 .add(new Argument("file", "path").optional) 773 .add(new Argument("dir", "desc")) 774 ); 775 } 776 777 // default required 778 unittest { 779 import std.exception : assertThrown; 780 781 assertThrown!InvalidProgramException( 782 new Program("test") 783 .add(new Option("d", "dir", "desc").defaultValue("test").required) 784 ); 785 786 assertThrown!InvalidProgramException( 787 new Program("test") 788 .add(new Argument("dir", "desc").defaultValue("test").required) 789 ); 790 } 791 792 // flags 793 unittest { 794 import std.exception : assertThrown; 795 import commandr.validators; 796 797 assertThrown!InvalidProgramException( 798 new Program("test") 799 .add(new Flag("a", "bb", "desc") 800 .acceptsValues(["a"])) 801 ); 802 } 803 804 // subcommands 805 unittest { 806 import std.exception : assertThrown; 807 808 assertThrown!InvalidProgramException( 809 new Program("test") 810 .add(new Argument("test", "").defaultValue("test")) 811 .add(new Command("a")) 812 .add(new Command("b")) 813 ); 814 } 815 816 // default command 817 unittest { 818 import std.exception : assertThrown, assertNotThrown; 819 import commandr.validators; 820 821 assertThrown!InvalidProgramException( 822 new Program("test") 823 .defaultCommand("a") 824 .add(new Command("a", "desc")) 825 ); 826 827 assertThrown!InvalidProgramException( 828 new Program("test") 829 .add(new Command("a", "desc")) 830 .defaultCommand("b") 831 ); 832 833 assertNotThrown!InvalidProgramException( 834 new Program("test") 835 .add(new Command("a", "desc")) 836 .defaultCommand(null) 837 ); 838 } 839 840 // topics 841 unittest { 842 import std.exception : assertThrown, assertNotThrown; 843 import commandr.validators; 844 845 auto p = new Program("test") 846 .add(new Command("a", "desc")) 847 .topic("z") 848 .topicGroup("general purpose") 849 .add(new Command("b", "desc")) 850 ; 851 852 assert(p.topic == "z"); 853 assert(p.commands["b"].topic == "general purpose"); 854 }