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 }