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);