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