1 /**
2 	Parses and allows querying the command line arguments and configuration
3 	file.
4 
5 	The optional configuration file (vibe.conf) is a JSON file, containing an
6 	object with the keys corresponding to option names, and values corresponding
7 	to their values. It is searched for in the local directory, user's home
8 	directory, or /etc/vibe/ (POSIX only), whichever is found first.
9 
10 	Copyright: © 2012-2016 Sönke Ludwig
11 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
12 	Authors: Sönke Ludwig, Vladimir Panteleev
13 */
14 module vibe.core.args;
15 
16 import vibe.core.log;
17 import std.json;
18 
19 import std.algorithm : any, map, sort;
20 import std.array : array, join, replicate, split;
21 import std.exception;
22 import std.file;
23 import std.getopt;
24 import std.path : buildPath;
25 import std..string : format, stripRight, wrap;
26 
27 import core.runtime;
28 
29 
30 /**
31 	Finds and reads an option from the configuration file or command line.
32 
33 	Command line options take precedence over configuration file entries.
34 
35 	Params:
36 		names = Option names. Separate multiple name variants with "|",
37 			as for $(D std.getopt).
38 		pvalue = Pointer to store the value. Unchanged if value was not found.
39 		help_text = Text to be displayed when the application is run with
40 			--help.
41 
42 	Returns:
43 		$(D true) if the value was found, $(D false) otherwise.
44 
45 	See_Also: readRequiredOption
46 */
47 bool readOption(T)(string names, T* pvalue, string help_text)
48 	if (isOptionValue!T || is(T : E[], E) && isOptionValue!E)
49 {
50 	// May happen due to http://d.puremagic.com/issues/show_bug.cgi?id=9881
51 	if (g_args is null) init();
52 
53 	OptionInfo info;
54 	info.names = names.split("|").sort!((a, b) => a.length < b.length)().array();
55 	info.hasValue = !is(T == bool);
56 	info.helpText = help_text;
57 	assert(!g_options.any!(o => o.names == info.names)(), "readOption() may only be called once per option name.");
58 	g_options ~= info;
59 
60 	immutable olen = g_args.length;
61 	getopt(g_args, getoptConfig, names, pvalue);
62 	if (g_args.length < olen) return true;
63 
64 	if (g_haveConfig) {
65 		foreach (name; info.names)
66 			if (auto pv = name in g_config) {
67 				static if (isOptionValue!T) {
68 					*pvalue = fromValue!T(*pv);
69 				} else {
70 					*pvalue = (*pv).array.map!(j => fromValue!(typeof(T.init[0]))(j)).array;
71 				}
72 				return true;
73 			}
74 	}
75 
76 	return false;
77 }
78 
79 unittest {
80 	bool had_json = g_haveConfig;
81 	JSONValue json = g_config;
82 
83 	scope (exit) {
84 		g_haveConfig = had_json;
85 		g_config = json;
86 	}
87 
88 	g_haveConfig = true;
89 	g_config = parseJSON(`{
90 		"a": true,
91 		"b": 16000,
92 		"c": 2000000000,
93 		"d": 8000000000,
94 		"e": 1.0,
95 		"f": 2.0,
96 		"g": "bar",
97 		"h": [false, true],
98 		"i": [-16000, 16000],
99 		"j": [-2000000000, 2000000000],
100 		"k": [-8000000000, 8000000000],
101 		"l": [-1.0, 1.0],
102 		"m": [-2.0, 2.0],
103 		"n": ["bar", "baz"]
104 	}`);
105 
106 	bool b; readOption("a", &b, ""); assert(b == true);
107 	short s; readOption("b", &s, ""); assert(s == 16_000);
108 	int i; readOption("c", &i, ""); assert(i == 2_000_000_000);
109 	long l; readOption("d", &l, ""); assert(l == 8_000_000_000);
110 	float f; readOption("e", &f, ""); assert(f == cast(float)1.0);
111 	double d; readOption("f", &d, ""); assert(d == 2.0);
112 	string st; readOption("g", &st, ""); assert(st == "bar");
113 	bool[] ba; readOption("h", &ba, ""); assert(ba == [false, true]);
114 	short[] sa; readOption("i", &sa, ""); assert(sa == [-16000, 16000]);
115 	int[] ia; readOption("j", &ia, ""); assert(ia == [-2_000_000_000, 2_000_000_000]);
116 	long[] la; readOption("k", &la, ""); assert(la == [-8_000_000_000, 8_000_000_000]);
117 	float[] fa; readOption("l", &fa, ""); assert(fa == [cast(float)-1.0, cast(float)1.0]);
118 	double[] da; readOption("m", &da, ""); assert(da == [-2.0, 2.0]);
119 	string[] sta; readOption("n", &sta, ""); assert(sta == ["bar", "baz"]);
120 }
121 
122 
123 /**
124 	The same as readOption, but throws an exception if the given option is missing.
125 
126 	See_Also: readOption
127 */
128 T readRequiredOption(T)(string names, string help_text)
129 {
130 	string formattedNames() {
131 		return names.split("|").map!(s => s.length == 1 ? "-" ~ s : "--" ~ s).join("/");
132 	}
133 	T ret;
134 	enforce(readOption(names, &ret, help_text) || g_help,
135 		format("Missing mandatory option %s.", formattedNames()));
136 	return ret;
137 }
138 
139 
140 /**
141 	Prints a help screen consisting of all options encountered in getOption calls.
142 */
143 void printCommandLineHelp()
144 {
145 	enum dcolumn = 20;
146 	enum ncolumns = 80;
147 
148 	logInfo("Usage: %s <options>\n", g_args[0]);
149 	foreach (opt; g_options) {
150 		string shortopt;
151 		string[] longopts;
152 		if (opt.names[0].length == 1 && !opt.hasValue) {
153 			shortopt = "-"~opt.names[0];
154 			longopts = opt.names[1 .. $];
155 		} else {
156 			shortopt = "  ";
157 			longopts = opt.names;
158 		}
159 
160 		string optionString(string name)
161 		{
162 			if (name.length == 1) return "-"~name~(opt.hasValue ? " <value>" : "");
163 			else return "--"~name~(opt.hasValue ? "=<value>" : "");
164 		}
165 
166 		string[] lopts; foreach(lo; longopts) lopts ~= optionString(lo);
167 		auto optstr = format(" %s %s", shortopt, lopts.join(", "));
168 		if (optstr.length < dcolumn) optstr ~= replicate(" ", dcolumn - optstr.length);
169 
170 		auto indent = replicate(" ", dcolumn+1);
171 		auto desc = wrap(opt.helpText, ncolumns - dcolumn - 2, optstr.length > dcolumn ? indent : "", indent).stripRight();
172 
173 		if (optstr.length > dcolumn)
174 			logInfo("%s\n%s", optstr, desc);
175 		else logInfo("%s %s", optstr, desc);
176 	}
177 }
178 
179 
180 /**
181 	Checks for unrecognized command line options and display a help screen.
182 
183 	This function is called automatically from `vibe.appmain` and from
184 	`vibe.core.core.runApplication` to check for correct command line usage.
185 	It will print a help screen in case of unrecognized options.
186 
187 	Params:
188 		args_out = Optional parameter for storing any arguments not handled
189 				   by any readOption call. If this is left to null, an error
190 				   will be triggered whenever unhandled arguments exist.
191 
192 	Returns:
193 		If "--help" was passed, the function returns false. In all other
194 		cases either true is returned or an exception is thrown.
195 */
196 bool finalizeCommandLineOptions(string[]* args_out = null)
197 {
198 	scope(exit) g_args = null;
199 
200 	if (args_out) {
201 		*args_out = g_args;
202 	} else if (g_args.length > 1) {
203 		logError("Unrecognized command line option: %s\n", g_args[1]);
204 		printCommandLineHelp();
205 		throw new Exception("Unrecognized command line option.");
206 	}
207 
208 	if (g_help) {
209 		printCommandLineHelp();
210 		return false;
211 	}
212 
213 	return true;
214 }
215 
216 /** Tests if a given type is supported by `readOption`.
217 
218 	Allowed types are Booleans, integers, floating point values and strings.
219 	In addition to plain values, arrays of values are also supported.
220 */
221 enum isOptionValue(T) = is(T == bool) || is(T : long) || is(T : double) || is(T == string);
222 
223 /**
224 	This functions allows the usage of a custom command line argument parser
225 	with vibe.d.
226 
227 	$(OL
228 		$(LI build executable with version(VibeDisableCommandLineParsing))
229 		$(LI parse main function arguments with a custom command line parser)
230 		$(LI pass vibe.d arguments to `setCommandLineArgs`)
231 		$(LI use vibe.d command line parsing utilities)
232 	)
233 
234 	Params:
235 		args = The arguments that should be handled by vibe.d
236 */
237 void setCommandLineArgs(string[] args)
238 {
239 	g_args = args;
240 }
241 
242 ///
243 unittest {
244 	import std.format : format;
245 	string[] args = ["--foo", "10"];
246 	setCommandLineArgs(args);
247 }
248 
249 private struct OptionInfo {
250 	string[] names;
251 	bool hasValue;
252 	string helpText;
253 }
254 
255 private {
256 	__gshared string[] g_args;
257 	__gshared bool g_haveConfig;
258 	__gshared JSONValue g_config;
259 	__gshared OptionInfo[] g_options;
260 	__gshared bool g_help;
261 }
262 
263 private string[] getConfigPaths()
264 {
265 	string[] result = [""];
266 	import std.process : environment;
267 	version (Windows)
268 		result ~= environment.get("USERPROFILE");
269 	else
270 		result ~= [environment.get("HOME"), "/etc/vibe/"];
271 	return result;
272 }
273 
274 // this is invoked by the first readOption call (at least vibe.core will perform one)
275 private void init()
276 {
277 	version (VibeDisableCommandLineParsing) {}
278 	else g_args = Runtime.args;
279 
280 	if (!g_args.length) g_args = ["dummy"];
281 
282 	// TODO: let different config files override individual fields
283 	auto searchpaths = getConfigPaths();
284 	foreach (spath; searchpaths) {
285 		auto cpath = buildPath(spath, configName);
286 		if (cpath.exists) {
287 			scope(failure) logError("Failed to parse config file %s.", cpath);
288 			auto text = cpath.readText();
289 			g_config = text.parseJSON();
290 			g_haveConfig = true;
291 			break;
292 		}
293 	}
294 
295 	if (!g_haveConfig)
296 		logDiagnostic("No config file found in %s", searchpaths);
297 
298 	readOption("h|help", &g_help, "Prints this help screen.");
299 }
300 
301 private T fromValue(T)(in JSONValue val)
302 {
303 	import std.conv : to;
304 	static if (is(T == bool)) {
305         // JSONType.TRUE has been deprecated in v2.087.0
306         static if (is(typeof(JSONType.true_)))
307             return val.type == JSONType.true_;
308         else
309             return val.type == JSON_TYPE.TRUE;
310     }
311 	else static if (is(T : long)) return val.integer.to!T;
312 	else static if (is(T : double)) return val.floating.to!T;
313 	else static if (is(T == string)) return val.str;
314 	else static assert(false);
315 
316 }
317 
318 private enum configName = "vibe.conf";
319 
320 private template ValueTuple(T...) { alias ValueTuple = T; }
321 
322 private alias getoptConfig = ValueTuple!(std.getopt.config.passThrough, std.getopt.config.bundling);