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 }