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.all!(c => isAlphaNum(c) || c == '_')()) { 351 throw new InvalidProgramException("invalid name '%s' passed".format(name)); 352 } 353 354 auto entryPtr = name in _nameMap; 355 if (entryPtr !is null) { 356 throw new InvalidProgramException( 357 "duplicate name %s which is already used".format(name) 358 ); 359 } 360 } 361 362 private void validateOption(IOption option) pure @safe { 363 if (!option.abbrev && !option.full) { 364 throw new InvalidProgramException( 365 "option/flag %s must have either long or short form".format(option.name) 366 ); 367 } 368 369 if (option.abbrev) { 370 auto flag = this.getFlagByShort(option.abbrev); 371 if (!flag.isNull) { 372 throw new InvalidProgramException( 373 "duplicate abbrevation -%s, flag %s already uses it".format(option.abbrev, flag.get().name) 374 ); 375 } 376 377 auto other = this.getOptionByShort(option.abbrev); 378 if (!other.isNull) { 379 throw new InvalidProgramException( 380 "duplicate abbrevation -%s, option %s already uses it".format(option.abbrev, other.get().name) 381 ); 382 } 383 } 384 385 if (option.full) { 386 auto flag = this.getFlagByFull(option.full); 387 if (!flag.isNull) { 388 throw new InvalidProgramException( 389 "duplicate -%s, flag %s with this already exists".format(option.full, flag.get().name) 390 ); 391 } 392 393 auto other = this.getOptionByFull(option.full); 394 if (!other.isNull) { 395 throw new InvalidProgramException( 396 "duplicate --%s, option %s with this already exists".format(option.full, other.get().name) 397 ); 398 } 399 } 400 401 if (option.isRequired && option.defaultValue) { 402 throw new InvalidProgramException("cannot have required option with default value"); 403 } 404 } 405 } 406 407 /** 408 * Represents program. 409 * 410 * This is the entry-point for building your program model. 411 */ 412 public class Program: Command { 413 private string _binaryName; 414 private string[] _authors; 415 416 /** 417 * Creates new instance of `Program`. 418 * 419 * Params: 420 * name - Program name 421 * version_ - Program version 422 */ 423 public this(string name, string version_ = "1.0") { 424 super(name, null, version_); 425 this.addBasicOptions(); 426 } 427 428 /** 429 * Sets program name 430 */ 431 public override typeof(this) name(string name) nothrow pure @nogc @safe { 432 return cast(Program)super.name(name); 433 } 434 435 /** 436 * Program name 437 */ 438 public override string name() const nothrow pure @nogc @safe { 439 return this._name; 440 } 441 442 /** 443 * Sets program version 444 */ 445 public override typeof(this) version_(string version_) nothrow pure @nogc @safe { 446 return cast(Program)super.version_(version_); 447 } 448 449 /** 450 * Program version 451 */ 452 public override string version_() const nothrow pure @nogc @safe { 453 return this._version; 454 } 455 456 /** 457 * Sets program summary (one-liner) 458 */ 459 public override typeof(this) summary(string summary) nothrow pure @nogc @safe { 460 return cast(Program)super.summary(summary); 461 } 462 463 /** 464 * Program summary (one-liner) 465 */ 466 public override string summary() nothrow pure @nogc @safe { 467 return this._summary; 468 } 469 470 /// Proxy call to `Command.add` returning `Program`. 471 public typeof(this) add(T: IEntry)(T data) pure @safe { 472 super.add(data); 473 return this; 474 } 475 476 public override typeof(this) add(Command command) pure @safe { 477 super.add(command); 478 return this; 479 } 480 481 public override typeof(this) defaultCommand(string name) pure @safe { 482 super.defaultCommand(name); 483 return this; 484 } 485 486 public override string defaultCommand() nothrow pure @safe @nogc { 487 return this._defaultCommand; 488 } 489 490 /** 491 * Sets program binary name 492 */ 493 public typeof(this) binaryName(string binaryName) nothrow pure @nogc @safe { 494 this._binaryName = binaryName; 495 return this; 496 } 497 498 /** 499 * Program binary name 500 */ 501 public string binaryName() const nothrow pure @nogc @safe { 502 return (this._binaryName !is null) ? this._binaryName : this._name; 503 } 504 505 /** 506 * Adds program author 507 */ 508 public typeof(this) author(string author) nothrow pure @safe { 509 this._authors ~= author; 510 return this; 511 } 512 513 /** 514 * Sets program authors 515 */ 516 public typeof(this) authors(string[] authors) nothrow pure @nogc @safe { 517 this._authors = authors; 518 return this; 519 } 520 521 /** 522 * Program authors 523 */ 524 public string[] authors() nothrow pure @nogc @safe { 525 return this._authors; 526 } 527 528 /** 529 * Sets topic group for the following commands. 530 */ 531 public override typeof(this) topicGroup(string topic) pure @safe { 532 super.topicGroup(topic); 533 return this; 534 } 535 536 /** 537 * Sets topic group for this command. 538 */ 539 public override typeof(this) topic(string topic) nothrow pure @safe @nogc { 540 super.topic(topic); 541 return this; 542 } 543 544 /** 545 * Topic group for this command. 546 */ 547 public override string topic() nothrow pure @safe @nogc { 548 return _topic; 549 } 550 } 551 552 unittest { 553 import std.range : empty; 554 555 auto program = new Program("test"); 556 assert(program.name == "test"); 557 assert(program.binaryName == "test"); 558 assert(program.version_ == "1.0"); 559 assert(program.summary is null); 560 assert(program.authors.empty); 561 assert(program.flags.length == 2); 562 assert(program.flags[0].name == "help"); 563 assert(program.flags[0].abbrev == "h"); 564 assert(program.flags[0].full == "help"); 565 assert(program.flags[1].name == "version"); 566 assert(program.flags[1].abbrev is null); 567 assert(program.flags[1].full == "version"); 568 assert(program.options.empty); 569 assert(program.arguments.empty); 570 } 571 572 unittest { 573 auto program = new Program("test").name("bar"); 574 assert(program.name == "bar"); 575 assert(program.binaryName == "bar"); 576 } 577 578 unittest { 579 auto program = new Program("test", "0.1"); 580 assert(program.version_ == "0.1"); 581 } 582 583 unittest { 584 auto program = new Program("test", "0.1").version_("2.0").version_("kappa"); 585 assert(program.version_ == "kappa"); 586 } 587 588 unittest { 589 auto program = new Program("test").binaryName("kappa"); 590 assert(program.name == "test"); 591 assert(program.binaryName == "kappa"); 592 } 593 594 // name conflicts 595 unittest { 596 import std.exception : assertThrown; 597 598 // FLAGS 599 // flag-flag 600 assertThrown!InvalidProgramException( 601 new Program("test") 602 .add(new Flag("a", "aaa", "desc").name("nnn")) 603 .add(new Flag("b", "bbb", "desc").name("nnn")) 604 ); 605 606 // flag-option 607 assertThrown!InvalidProgramException( 608 new Program("test") 609 .add(new Flag("a", "aaa", "desc").name("nnn")) 610 .add(new Option("b", "bbb", "desc").name("nnn")) 611 ); 612 613 // flag-argument 614 assertThrown!InvalidProgramException( 615 new Program("test") 616 .add(new Flag("a", "aaa", "desc").name("nnn")) 617 .add(new Argument("nnn")) 618 ); 619 620 621 // OPTIONS 622 // option-flag 623 assertThrown!InvalidProgramException( 624 new Program("test") 625 .add(new Option("a", "aaa", "desc").name("nnn")) 626 .add(new Flag("b", "bbb", "desc").name("nnn")) 627 ); 628 629 // option-option 630 assertThrown!InvalidProgramException( 631 new Program("test") 632 .add(new Option("a", "aaa", "desc").name("nnn")) 633 .add(new Option("b", "bbb", "desc").name("nnn")) 634 ); 635 636 // option-argument 637 assertThrown!InvalidProgramException( 638 new Program("test") 639 .add(new Option("a", "aaa", "desc").name("nnn")) 640 .add(new Argument("nnn")) 641 ); 642 643 644 // ARGUMENTS 645 // argument-flag 646 assertThrown!InvalidProgramException( 647 new Program("test") 648 .add(new Argument("nnn")) 649 .add(new Flag("b", "bbb", "desc").name("nnn")) 650 ); 651 652 // argument-option 653 assertThrown!InvalidProgramException( 654 new Program("test") 655 .add(new Argument("nnn")) 656 .add(new Option("b", "bbb", "desc").name("nnn")) 657 ); 658 659 // argument-argument 660 assertThrown!InvalidProgramException( 661 new Program("test") 662 .add(new Argument("nnn")) 663 .add(new Argument("nnn")) 664 ); 665 } 666 667 // abbrev conflicts 668 unittest { 669 import std.exception : assertThrown; 670 671 // FLAGS 672 // flag-flag 673 assertThrown!InvalidProgramException( 674 new Program("test") 675 .add(new Flag("a", "aaa", "desc")) 676 .add(new Flag("a", "bbb", "desc")) 677 ); 678 679 // flag-option 680 assertThrown!InvalidProgramException( 681 new Program("test") 682 .add(new Flag("a", "aaa", "desc")) 683 .add(new Option("a", "bbb", "desc")) 684 ); 685 686 // FLAGS 687 // option-flag 688 assertThrown!InvalidProgramException( 689 new Program("test") 690 .add(new Option("a", "aaa", "desc")) 691 .add(new Flag("a", "bbb", "desc")) 692 ); 693 694 // option-option 695 assertThrown!InvalidProgramException( 696 new Program("test") 697 .add(new Option("a", "aaa", "desc")) 698 .add(new Option("a", "bbb", "desc")) 699 ); 700 } 701 702 // full name conflicts 703 unittest { 704 import std.exception : assertThrown; 705 706 // FLAGS 707 // flag-flag 708 assertThrown!InvalidProgramException( 709 new Program("test") 710 .add(new Flag("a", "aaa", "desc")) 711 .add(new Flag("b", "aaa", "desc")) 712 ); 713 714 // flag-option 715 assertThrown!InvalidProgramException( 716 new Program("test") 717 .add(new Flag("a", "aaa", "desc")) 718 .add(new Option("b", "aaa", "desc")) 719 ); 720 721 // FLAGS 722 // option-flag 723 assertThrown!InvalidProgramException( 724 new Program("test") 725 .add(new Option("a", "aaa", "desc")) 726 .add(new Flag("b", "aaa", "desc")) 727 ); 728 729 // option-option 730 assertThrown!InvalidProgramException( 731 new Program("test") 732 .add(new Option("a", "aaa", "desc")) 733 .add(new Option("b", "aaa", "desc")) 734 ); 735 } 736 737 // repeating 738 unittest { 739 import std.exception : assertThrown; 740 741 assertThrown!InvalidProgramException( 742 new Program("test") 743 .add(new Argument("file", "path").repeating) 744 .add(new Argument("dir", "desc")) 745 ); 746 } 747 748 // invalid option 749 unittest { 750 import std.exception : assertThrown; 751 752 assertThrown!InvalidProgramException( 753 new Program("test") 754 .add(new Flag(null, null, "")) 755 ); 756 757 assertThrown!InvalidProgramException( 758 new Program("test") 759 .add(new Option(null, null, "")) 760 ); 761 } 762 763 // required args out of order 764 unittest { 765 import std.exception : assertThrown; 766 767 assertThrown!InvalidProgramException( 768 new Program("test") 769 .add(new Argument("file", "path").optional) 770 .add(new Argument("dir", "desc")) 771 ); 772 } 773 774 // default required 775 unittest { 776 import std.exception : assertThrown; 777 778 assertThrown!InvalidProgramException( 779 new Program("test") 780 .add(new Option("d", "dir", "desc").defaultValue("test").required) 781 ); 782 783 assertThrown!InvalidProgramException( 784 new Program("test") 785 .add(new Argument("dir", "desc").defaultValue("test").required) 786 ); 787 } 788 789 // flags 790 unittest { 791 import std.exception : assertThrown; 792 import commandr.validators; 793 794 assertThrown!InvalidProgramException( 795 new Program("test") 796 .add(new Flag("a", "bb", "desc") 797 .acceptsValues(["a"])) 798 ); 799 } 800 801 // subcommands 802 unittest { 803 import std.exception : assertThrown; 804 805 assertThrown!InvalidProgramException( 806 new Program("test") 807 .add(new Argument("test", "").defaultValue("test")) 808 .add(new Command("a")) 809 .add(new Command("b")) 810 ); 811 } 812 813 // default command 814 unittest { 815 import std.exception : assertThrown, assertNotThrown; 816 import commandr.validators; 817 818 assertThrown!InvalidProgramException( 819 new Program("test") 820 .defaultCommand("a") 821 .add(new Command("a", "desc")) 822 ); 823 824 assertThrown!InvalidProgramException( 825 new Program("test") 826 .add(new Command("a", "desc")) 827 .defaultCommand("b") 828 ); 829 830 assertNotThrown!InvalidProgramException( 831 new Program("test") 832 .add(new Command("a", "desc")) 833 .defaultCommand(null) 834 ); 835 } 836 837 // topics 838 unittest { 839 import std.exception : assertThrown, assertNotThrown; 840 import commandr.validators; 841 842 auto p = new Program("test") 843 .add(new Command("a", "desc")) 844 .topic("z") 845 .topicGroup("general purpose") 846 .add(new Command("b", "desc")) 847 ; 848 849 assert(p.topic == "z"); 850 assert(p.commands["b"].topic == "general purpose"); 851 }