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 RejectedSoftware e.K.
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 vibe.data.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 {
49 	// May happen due to http://d.puremagic.com/issues/show_bug.cgi?id=9881
50 	if (g_args is null) init();
51 
52 	OptionInfo info;
53 	info.names = names.split("|").sort!((a, b) => a.length < b.length)().array();
54 	info.hasValue = !is(T == bool);
55 	info.helpText = help_text;
56 	assert(!g_options.any!(o => o.names == info.names)(), "readOption() may only be called once per option name.");
57 	g_options ~= info;
58 
59 	immutable olen = g_args.length;
60 	getopt(g_args, getoptConfig, names, pvalue);
61 	if (g_args.length < olen) return true;
62 
63 	if (g_haveConfig) {
64 		foreach (name; info.names)
65 			if (auto pv = name in g_config) {
66 				*pvalue = deserializeJson!T(*pv);
67 				return true;
68 			}
69 	}
70 
71 	return false;
72 }
73 
74 
75 /**
76 	The same as readOption, but throws an exception if the given option is missing.
77 
78 	See_Also: readOption
79 */
80 T readRequiredOption(T)(string names, string help_text)
81 {
82 	string formattedNames() {
83 		return names.split("|").map!(s => s.length == 1 ? "-" ~ s : "--" ~ s).join("/");
84 	}
85 	T ret;
86 	enforce(readOption(names, &ret, help_text) || g_help,
87 		format("Missing mandatory option %s.", formattedNames()));
88 	return ret;
89 }
90 
91 
92 /**
93 	Prints a help screen consisting of all options encountered in getOption calls.
94 */
95 void printCommandLineHelp()
96 {
97 	enum dcolumn = 20;
98 	enum ncolumns = 80;
99 
100 	logInfo("Usage: %s <options>\n", g_args[0]);
101 	foreach (opt; g_options) {
102 		string shortopt;
103 		string[] longopts;
104 		if (opt.names[0].length == 1 && !opt.hasValue) {
105 			shortopt = "-"~opt.names[0];
106 			longopts = opt.names[1 .. $];
107 		} else {
108 			shortopt = "  ";
109 			longopts = opt.names;
110 		}
111 
112 		string optionString(string name)
113 		{
114 			if (name.length == 1) return "-"~name~(opt.hasValue ? " <value>" : "");
115 			else return "--"~name~(opt.hasValue ? "=<value>" : "");
116 		}
117 
118 		string[] lopts; foreach(lo; longopts) lopts ~= optionString(lo);
119 		auto optstr = format(" %s %s", shortopt, lopts.join(", "));
120 		if (optstr.length < dcolumn) optstr ~= replicate(" ", dcolumn - optstr.length);
121 
122 		auto indent = replicate(" ", dcolumn+1);
123 		auto desc = wrap(opt.helpText, ncolumns - dcolumn - 2, optstr.length > dcolumn ? indent : "", indent).stripRight();
124 
125 		if (optstr.length > dcolumn)
126 			logInfo("%s\n%s", optstr, desc);
127 		else logInfo("%s %s", optstr, desc);
128 	}
129 }
130 
131 
132 /**
133 	Checks for unrecognized command line options and display a help screen.
134 
135 	This function is called automatically from `vibe.appmain` and from
136 	`vibe.core.core.runApplication` to check for correct command line usage.
137 	It will print a help screen in case of unrecognized options.
138 
139 	Params:
140 		args_out = Optional parameter for storing any arguments not handled
141 				   by any readOption call. If this is left to null, an error
142 				   will be triggered whenever unhandled arguments exist.
143 
144 	Returns:
145 		If "--help" was passed, the function returns false. In all other
146 		cases either true is returned or an exception is thrown.
147 */
148 bool finalizeCommandLineOptions(string[]* args_out = null)
149 {
150 	scope(exit) g_args = null;
151 
152 	if (args_out) {
153 		*args_out = g_args;
154 	} else if (g_args.length > 1) {
155 		logError("Unrecognized command line option: %s\n", g_args[1]);
156 		printCommandLineHelp();
157 		throw new Exception("Unrecognized command line option.");
158 	}
159 
160 	if (g_help) {
161 		printCommandLineHelp();
162 		return false;
163 	}
164 
165 	return true;
166 }
167 
168 
169 private struct OptionInfo {
170 	string[] names;
171 	bool hasValue;
172 	string helpText;
173 }
174 
175 private {
176 	__gshared string[] g_args;
177 	__gshared bool g_haveConfig;
178 	__gshared Json g_config;
179 	__gshared OptionInfo[] g_options;
180 	__gshared bool g_help;
181 }
182 
183 private string[] getConfigPaths()
184 {
185 	string[] result = [""];
186 	import std.process : environment;
187 	version (Windows)
188 		result ~= environment.get("USERPROFILE");
189 	else
190 		result ~= [environment.get("HOME"), "/etc/vibe/"];
191 	return result;
192 }
193 
194 // this is invoked by the first readOption call (at least vibe.core will perform one)
195 private void init()
196 {
197 	import vibe.utils.string : stripUTF8Bom;
198 
199 	version (VibeDisableCommandLineParsing) {}
200 	else g_args = Runtime.args;
201 
202 	if (!g_args.length) g_args = ["dummy"];
203 
204 	// TODO: let different config files override individual fields
205 	auto searchpaths = getConfigPaths();
206 	foreach (spath; searchpaths) {
207 		auto cpath = buildPath(spath, configName);
208 		if (cpath.exists) {
209 			scope(failure) logError("Failed to parse config file %s.", cpath);
210 			auto text = stripUTF8Bom(cpath.readText());
211 			g_config = text.parseJson();
212 			g_haveConfig = true;
213 			break;
214 		}
215 	}
216 
217 	if (!g_haveConfig)
218 		logDiagnostic("No config file found in %s", searchpaths);
219 
220 	readOption("h|help", &g_help, "Prints this help screen.");
221 }
222 
223 private enum configName = "vibe.conf";
224 
225 private template ValueTuple(T...) { alias ValueTuple = T; }
226 
227 private alias getoptConfig = ValueTuple!(std.getopt.config.passThrough, std.getopt.config.bundling);