1 module clid.core.help;
2 
3 import std.traits : hasUDA, getUDAs;
4 import std.meta : Alias;
5 import std.string : leftJustify;
6 import std.uni : toUpper;
7 
8 import clid.attributes;
9 import clid.core.util;
10 
11 /**
12  * Print help information for a given configuration struct.
13  * Params: c = The configuration struct to print help information for.
14  */
15 void printHelp(C)(C c)
16 {
17 	import std.stdio : stderr;
18 
19 	validateStruct!C();
20 	stderr.write(getHelp(c));
21 }
22 
23 private string getHelp(C)(C c) // @suppress(dscanner.suspicious.unused_parameter)
24 {
25 	import std.ascii : newline;
26 
27 	string str = usage!C() ~ newline;
28 
29 	if (hasUnnamedParameters!C)
30 	{
31 		str ~= newline ~ "Arguments:" ~ newline;
32 	}
33 
34 	if (hasNamedParameters!C)
35 	{
36 		str ~= newline ~ "Options:" ~ newline;
37 		static foreach (member; __traits(allMembers, C))
38 		{
39 			static if (isNamedParameter!(C, member))
40 			{
41 				str ~= describe!(value!(C, member)) ~ newline;
42 			}
43 		}
44 
45 		static if (!hasShortParameter!(C)('h') && !hasLongParameter!C("help"))
46 		{
47 			str ~= "\t-h, --help".leftJustify(30) ~ "Show this help\n";
48 		}
49 		else static if (!hasLongParameter!C("help"))
50 		{
51 			str ~= "\t    --help".leftJustify(30) ~ "Show this help\n";
52 		}
53 		else static if (!hasShortParameter!C('h'))
54 		{
55 			str ~= "\t-h        ".leftJustify(30) ~ "Show this help\n";
56 		}
57 	}
58 	return str;
59 }
60 
61 private template getFlagName(alias E)
62 {
63 	static if (getParameter!(E).shortName == ' ')
64 	{
65 		alias getFlagName = Alias!("    --" ~ getParameter!(E).longName);
66 	}
67 	else
68 	{
69 		alias getFlagName = Alias!("-" ~ getParameter!(E)
70 				.shortName ~ ", --" ~ getParameter!(E).longName);
71 	}
72 }
73 
74 private template getFlagUnit(alias E, string prefix = " ")
75 {
76 	static if (hasDescription!(E) && getDescription!(E).optionUnit.length > 0)
77 	{
78 		alias getFlagUnit = Alias!(prefix ~ getDescription!(E).optionUnit);
79 	}
80 	else static if (is(typeof(E) : bool))
81 	{
82 		alias getFlagUnit = Alias!("");
83 	}
84 	else
85 	{
86 		alias getFlagUnit = Alias!(prefix ~ typeof(E).stringof.toUpper);
87 	}
88 }
89 
90 private template getFlag(alias E)
91 {
92 	alias getFlag = Alias!(getFlagName!(E) ~ getFlagUnit!(E));
93 }
94 
95 private template getOnlyDescription(alias E)
96 {
97 	static if (hasDescription!(E))
98 		alias getOnlyDescription = Alias!(getDescription!(E).description);
99 	else
100 		alias getOnlyDescription = Alias!("");
101 }
102 
103 private template getFullDescription(alias E)
104 {
105 	static if (hasUDA!(E, Required))
106 		alias getFullDescription = Alias!("[REQUIRED] " ~ getOnlyDescription!(E));
107 	else
108 		alias getFullDescription = getOnlyDescription!(E);
109 }
110 
111 private template describe(alias E)
112 {
113 	static if (!hasUDA!(E, Description) && !hasUDA!(E, Required))
114 	{
115 		alias describe = Alias!('\t' ~ getFlag!(E));
116 	}
117 	else
118 	{
119 		alias describe = Alias!(leftJustify('\t' ~ getFlag!(E), 30) ~ getFullDescription!(E));
120 	}
121 }
122 
123 private string usage(C)()
124 {
125 	import core.runtime : Runtime;
126 
127 	return "Usage: " ~ Runtime.args()[0] ~ " [OPTION]..." ~ getRequiredUsage!C()
128 		~ getUnnamedUsage!C();
129 }
130 
131 private string getUnnamedUsage(C)()
132 {
133 	string str;
134 
135 	static foreach (member; __traits(allMembers, C))
136 	{
137 		static if (!isNamedParameter!(C, member))
138 		{
139 			static if (isRequired!(C, member))
140 			{
141 				str ~= getFlagUnit!(value!(C, member)).toUpper;
142 			}
143 			else
144 			{
145 				str ~= getFlagUnit!(value!(C, member), " [").toUpper ~ "]";
146 			}
147 		}
148 	}
149 	return str;
150 }
151 
152 private string getRequiredUsage(C)()
153 {
154 	string str;
155 
156 	static foreach (member; __traits(allMembers, C))
157 	{
158 		static if (isRequired!(C, member))
159 		{
160 			str ~= " --" ~ getParameter!(value!(C, member))
161 				.longName ~ getFlagUnit!(value!(C, member)).toUpper;
162 		}
163 	}
164 
165 	return str;
166 }
167 
168 // ======================
169 // Unit tests start here.
170 // ======================
171 
172 unittest
173 {
174 	struct Config
175 	{
176 		@Parameter("foo", 'f')
177 		@Description("Foobar")
178 		int value;
179 
180 		@Parameter("str", 's')
181 		@Description("Just a string")
182 		string stringValue;
183 
184 		@Parameter("time", 't')
185 		@Description("Interval time", "secs")
186 		@Required int time;
187 
188 		@Parameter("bar")
189 		@Description("A random description")
190 		@Required bool other;
191 
192 		@Parameter() string something;
193 	}
194 
195 	immutable Config config;
196 	static assert(getFlag!(config.value) == "-f, --foo INT");
197 	static assert(getFullDescription!(config.value) == "Foobar");
198 
199 	printHelp(config);
200 }
201 
202 unittest
203 {
204 	struct Config
205 	{
206 		@Parameter("foo")
207 		int value;
208 	}
209 
210 	immutable Config config;
211 	static assert(getFlag!(config.value) == "    --foo INT");
212 }