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, size_t 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).nullable);
60         help.addLine(null);
61         help.addLine(null);
62 
63         if(CommandInfo.description)
64             help.addHeaderWithText("Description:", CommandInfo.description);
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         Arg[][ArgGroup] argsByGroup;
76         foreach(nam; named)
77         {
78             scope ptr = (nam.group in argsByGroup);
79             if(!ptr)
80             {
81                 argsByGroup[nam.group] = Arg[].init;
82                 ptr = (nam.group in argsByGroup);
83             }
84 
85             (*ptr) ~= nam;
86         }
87 
88         foreach(group, args; argsByGroup)
89         {
90             help.addLine(null);
91 
92             if(group == ArgGroup.init)
93                 help.addHeader("Named Arguments:");
94             else if(group.description == null)
95                 help.addHeader(group.name);
96             else
97                 help.addHeaderWithText(group.name, group.description);
98 
99             foreach(arg; args)
100             {
101                 auto descs = [HelpTextDescription(0, arg.description)];
102 
103                 help.addArgument(
104                     arg.name,
105                     descs
106                 );
107             }
108         }
109 
110         this._cached = help.finish();
111         return this._cached;
112     }
113 }
114 
115 unittest
116 {
117     @Command("command", "This is a command that is totally super complicated.")
118     static struct ComplexCommand
119     {
120         @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere")
121         int a;
122         @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere")
123         int b;
124         @ArgPositional("output", "Where to place the output.")
125         string output;
126 
127         @ArgNamed("test-flag", "Test flag, please ignore.")
128         bool flag;
129 
130         @ArgGroup("Debug", "Arguments related to debugging.")
131         {
132             @ArgNamed("verbose|v", "Enables verbose logging.")
133             Nullable!bool verbose;
134 
135             @ArgNamed("log|l", "Specifies a log file to direct output to.")
136             Nullable!string log;
137         }
138 
139         @ArgGroup("I/O", "Arguments related to I/O.")
140         @ArgNamed("config|c", "Specifies the config file to use.")
141         Nullable!string config;
142 
143         void onExecute(){}
144     }
145 
146     auto c = CommandHelpText!ComplexCommand();
147     // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often
148     // that manual validation is good enough.
149     //assert(false, c.generate());
150 }