1 module jcli.helptext.helptext;
2 
3 import jcli.core, jcli.text, jcli.introspect, std;
4 
5 struct CommandHelpText(alias CommandT_)
6 {
7     alias CommandT          = CommandT_;
8     alias CommandInfo       = commandInfoFor!CommandT;
9 
10     private string _cached;
11 
12     string generate(string appName = null, uint width = 180)
13     {
14         if(this._cached)
15             return this._cached;
16 
17         HelpText help = HelpText.make(width);
18 
19         if(appName is null)
20             appName = thisExePath().baseName;
21 
22         static struct Arg
23         {
24             string name;
25             string description;
26             ArgGroup group;
27             bool optional;
28         }
29 
30         string patternToNamedArgList(Pattern pattern)
31         {
32             return pattern.patterns
33                           .map!(p => p.length == 1 ? "-"~p : "--"~p)
34                           .fold!((a,b) => a.length ? a~"|"~b : b)("");
35         }
36 
37         Arg[] positionals;
38         Arg[] named;
39 
40         static foreach(i, pos; CommandInfo.positionalArgs)
41             positionals ~= Arg(pos.uda.name, pos.uda.description);
42         static foreach(i, nam; CommandInfo.namedArgs)
43         {
44             named ~= Arg(
45                 patternToNamedArgList(nam.uda.pattern),
46                 nam.uda.description,
47                 nam.group,
48                 !!(nam.existence & ArgExistence.optional)
49             );
50         }
51 
52         named.multiSort!("a.optional != b.optional", "a.name < b.name");
53 
54         help.addLineWithPrefix("Usage: ", "%s %s%s%s".format(
55             appName,
56             CommandInfo.pattern.patterns.walkLength ? CommandInfo.pattern.patterns.front : "DEFAULT",
57             positionals.map!(p => "<"~p.name~">").fold!((a,b) => a~" "~b)(""),
58             named.map!(p => p.optional ? "["~p.name~"]" : p.name).fold!((a,b) => a~" "~b)("")
59         ), AnsiStyleSet.init.style(AnsiStyle.init.bold));
60 
61         if(CommandInfo.description)
62             help.addHeaderWithText("Description:", CommandInfo.description);
63 
64         if(positionals.length)
65         {
66             help.addHeader("Positional Arguments:");
67             foreach(pos; positionals)
68             {
69                 help.addArgument(
70                     pos.name,
71                     [HelpTextDescription(0, pos.description)]
72                 );
73             }
74         }
75 
76         Arg[][ArgGroup] argsByGroup;
77         foreach(nam; named)
78         {
79             scope ptr = (nam.group in argsByGroup);
80             if(!ptr)
81             {
82                 argsByGroup[nam.group] = Arg[].init;
83                 ptr = (nam.group in argsByGroup);
84             }
85 
86             (*ptr) ~= nam;
87         }
88 
89         foreach(group, args; argsByGroup)
90         {
91             help.addLine(null);
92 
93             if(group == ArgGroup.init)
94                 help.addHeader("Named Arguments:");
95             else if(group.description == null)
96                 help.addHeader(group.name);
97             else
98                 help.addHeaderWithText(group.name, group.description);
99 
100             foreach(arg; args)
101             {
102                 auto descs = [HelpTextDescription(0, arg.description)];
103 
104                 help.addArgument(
105                     arg.name,
106                     descs
107                 );
108             }
109         }
110 
111         this._cached = help.finish();
112         return this._cached;
113     }
114 }
115 
116 unittest
117 {
118     @Command("command", "This is a command that is totally super complicated.")
119     static struct ComplexCommand
120     {
121         @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere")
122         int a;
123         @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere")
124         int b;
125         @ArgPositional("output", "Where to place the output.")
126         string output;
127 
128         @ArgNamed("test-flag", "Test flag, please ignore.")
129         bool flag;
130 
131         @ArgGroup("Debug", "Arguments related to debugging.")
132         {
133             @ArgNamed("verbose|v", "Enables verbose logging.")
134             Nullable!bool verbose;
135 
136             @ArgNamed("log|l", "Specifies a log file to direct output to.")
137             Nullable!string log;
138         }
139 
140         @ArgGroup("I/O", "Arguments related to I/O.")
141         @ArgNamed("config|c", "Specifies the config file to use.")
142         Nullable!string config;
143 
144         void onExecute(){}
145     }
146 
147     auto c = CommandHelpText!ComplexCommand();
148     // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often
149     // that manual validation is good enough.
150     //assert(false, c.generate());
151 }