1 /** 2 * parse configuration options 3 * 4 * Copyright: Copyright Digital Mars 2017 5 * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 6 * 7 * Source: $(DRUNTIMESRC core/internal/parseoptions.d) 8 */ 9 10 module core.internal.parseoptions; 11 12 import core.stdc.stdlib; 13 import core.stdc.stdio; 14 import core.stdc.ctype; 15 import core.stdc.string; 16 import core.vararg; 17 import core.internal.traits : externDFunc, hasUDA; 18 19 20 @nogc nothrow: 21 extern extern(C) string[] rt_args() @system; 22 23 extern extern(C) __gshared bool rt_envvars_enabled; 24 extern extern(C) __gshared bool rt_cmdline_enabled; 25 extern extern(C) __gshared string[] rt_options; 26 27 alias rt_configCallBack = string delegate(string) @nogc nothrow; 28 alias fn_configOption = string function(string opt, scope rt_configCallBack dg, bool reverse) @nogc nothrow; 29 alias rt_configOption = externDFunc!("rt.config.rt_configOption", fn_configOption); 30 31 /// UDA for field treated as memory value 32 struct MemVal {} 33 34 /** 35 * initialize members of struct CFG from rt_config options 36 * 37 * options will be read from the environment, the command line or embedded 38 * into the executable as configured (see rt.config) 39 * 40 * fields of the struct are populated by parseOptions(). 41 */ 42 bool initConfigOptions(CFG)(ref CFG cfg, string cfgname) 43 { 44 string parse(string opt) @nogc nothrow 45 { 46 if (!parseOptions(cfg, opt)) 47 return "err"; 48 return null; // continue processing 49 } 50 string s = rt_configOption(cfgname, &parse, true); 51 return s is null; 52 } 53 54 /** 55 * initialize members of struct CFG from a string of sub-options. 56 * 57 * fields of the struct are populated by listing them as space separated 58 * sub-options <field-name>:value, e.g. "precise:1 profile:1" 59 * 60 * supported field value types: 61 * - strings (without spaces) 62 * - integer types (positive values only) 63 * - bool 64 * - float 65 * 66 * If the struct has a member "help" it is called if it is found as a sub-option. 67 * If the struct has a member "errorName", is used as the name reported in error 68 * messages. Otherwise the struct name is used. 69 */ 70 bool parseOptions(CFG)(ref CFG cfg, string opt) 71 { 72 static if (is(typeof(__traits(getMember, CFG, "errorName")))) 73 string errName = cfg.errorName; 74 else 75 string errName = CFG.stringof; 76 opt = skip!isspace(opt); 77 while (opt.length) 78 { 79 auto tail = find!(c => c == ':' || c == '=' || c == ' ')(opt); 80 auto name = opt[0 .. $ - tail.length]; 81 static if (is(typeof(__traits(getMember, CFG, "help")))) 82 if (name == "help") 83 { 84 version (CoreUnittest) {} else 85 cfg.help(); 86 opt = skip!isspace(tail); 87 continue; 88 } 89 if (tail.length <= 1 || tail[0] == ' ') 90 return optError("Missing argument for", name, errName); 91 tail = tail[1 .. $]; 92 93 NAMES_SWITCH: 94 switch (name) 95 { 96 static foreach (field; __traits(allMembers, CFG)) 97 { 98 static if (!is(typeof(__traits(getMember, cfg, field)) == function)) 99 { 100 case field: 101 bool r; 102 103 static if (hasUDA!(__traits(getMember, cfg, field), MemVal)) 104 r = parse(name, tail, __traits(getMember, cfg, field), errName, true); 105 else 106 r = parse(name, tail, __traits(getMember, cfg, field), errName); 107 108 if (!r) 109 return false; 110 111 break NAMES_SWITCH; 112 } 113 } 114 115 default: 116 return optError("Unknown", name, errName); 117 } 118 opt = skip!isspace(tail); 119 } 120 return true; 121 } 122 123 /** 124 Parses an individual option `optname` value from a provided string `str`. 125 The option type is given by the type `T` of the field `res` to which the parsed 126 value will be written too. 127 In case of an error, `errName` will be used to display an error message and 128 the failure of the parsing will be indicated by a falsy return value. 129 130 For boolean values, '0/n/N' (false) or '1/y/Y' (true) are accepted. 131 132 Params: 133 optname = name of the option to parse 134 str = raw string to parse the option value from 135 res = reference to the resulting data field that the option should be parsed too 136 errName = full-text name of the option which should be displayed in case of errors 137 138 Returns: `false` if a parsing error happened. 139 */ 140 bool rt_parseOption(T)(const(char)[] optname, ref inout(char)[] str, ref T res, const(char)[] errName) 141 { 142 return parse(optname, str, res, errName); 143 } 144 145 private: 146 147 bool optError(const scope char[] msg, const scope char[] name, const(char)[] errName) 148 { 149 import core.atomic : atomicLoad; 150 151 version (CoreUnittest) if (inUnittest) return false; 152 153 fprintf(atomicLoad(stderr), "%.*s %.*s option '%.*s'.\n", 154 cast(int)msg.length, msg.ptr, 155 cast(int)errName.length, errName.ptr, 156 cast(int)name.length, name.ptr); 157 return false; 158 } 159 160 inout(char)[] skip(alias pred)(inout(char)[] str) 161 { 162 return find!(c => !pred(c))(str); 163 } 164 165 inout(char)[] find(alias pred)(inout(char)[] str) 166 { 167 foreach (i; 0 .. str.length) 168 if (pred(str[i])) return str[i .. $]; 169 return null; 170 } 171 172 bool parse(T : size_t)(const(char)[] optname, ref inout(char)[] str, ref T res, const(char)[] errName, bool mayHaveSuffix = false) 173 if (is(T == size_t)) 174 in { assert(str.length); } 175 do 176 { 177 size_t i, v; 178 179 auto tail = find!(c => c == ' ')(str); 180 size_t len = str.length - tail.length; 181 182 import core.checkedint : mulu; 183 184 bool overflowed; 185 186 for (; i < len; i++) 187 { 188 char c = str[i]; 189 190 if (isdigit(c)) 191 v = 10 * v + c - '0'; 192 else // non-digit 193 { 194 if (mayHaveSuffix && i == len-1) // suffix 195 { 196 switch (c) 197 { 198 199 case 'G': 200 v = mulu(v, 1024 * 1024 * 1024, overflowed); 201 break; 202 203 case 'M': 204 v = mulu(v, 1024 * 1024, overflowed); 205 break; 206 207 case 'K': 208 v = mulu(v, 1024, overflowed); 209 break; 210 211 case 'B': 212 break; 213 214 default: 215 return parseError("value with unit type M, K or B", optname, str, "with suffix"); 216 } 217 218 if (overflowed) 219 return overflowedError(optname, str); 220 221 i++; 222 break; 223 } 224 else // unexpected non-digit character 225 { 226 i = 0; 227 break; 228 } 229 } 230 } 231 232 if (!i) 233 return parseError("a number", optname, str, errName); 234 235 if (mayHaveSuffix && isdigit(str[len-1])) 236 { 237 // No suffix found, default to megabytes 238 239 v = mulu(v, 1024 * 1024, overflowed); 240 241 if (overflowed) 242 return overflowedError(optname, str); 243 } 244 245 if (v > res.max) 246 return parseError("a number " ~ T.max.stringof ~ " or below", optname, str[0 .. i], errName); 247 str = str[i .. $]; 248 res = v; 249 return true; 250 } 251 252 bool parse(T : size_t)(const(char)[] optname, ref inout(char)[] str, ref T res, const(char)[] errName, bool mayHaveSuffix = false) 253 if (!is(T == size_t)) 254 in { assert(str.length); } 255 do 256 { 257 const oldStr = str; 258 size_t v; 259 if (!parse!size_t(optname, str, v, errName, mayHaveSuffix)) 260 return false; 261 262 if (v > res.max) 263 return parseError("a number " ~ T.max.stringof ~ " or below", optname, oldStr[0 .. $-str.length], errName); 264 res = cast(T) v; 265 return true; 266 } 267 268 bool parse(const(char)[] optname, ref inout(char)[] str, ref bool res, const(char)[] errName) 269 in { assert(str.length); } 270 do 271 { 272 if (str[0] == '1' || str[0] == 'y' || str[0] == 'Y') 273 res = true; 274 else if (str[0] == '0' || str[0] == 'n' || str[0] == 'N') 275 res = false; 276 else 277 return parseError("'0/n/N' or '1/y/Y'", optname, str, errName); 278 str = str[1 .. $]; 279 return true; 280 } 281 282 bool parse(const(char)[] optname, ref inout(char)[] str, ref float res, const(char)[] errName) 283 in { assert(str.length); } 284 do 285 { 286 // % uint f %n \0 287 char[1 + 10 + 1 + 2 + 1] fmt=void; 288 // specify max-width 289 immutable n = snprintf(fmt.ptr, fmt.length, "%%%uf%%n", cast(uint)str.length); 290 assert(n > 4 && n < fmt.length); 291 292 int nscanned; 293 version (CRuntime_DigitalMars) 294 { 295 /* Older sscanf's in snn.lib can write to its first argument, causing a crash 296 * if the string is in readonly memory. Recent updates to DMD 297 * https://github.com/dlang/dmd/pull/6546 298 * put string literals in readonly memory. 299 * Although sscanf has been fixed, 300 * http://ftp.digitalmars.com/snn.lib 301 * this workaround is here so it still works with the older snn.lib. 302 */ 303 // Create mutable copy of str 304 const length = str.length; 305 char* mptr = cast(char*)malloc(length + 1); 306 assert(mptr); 307 memcpy(mptr, str.ptr, length); 308 mptr[length] = 0; 309 const result = sscanf(mptr, fmt.ptr, &res, &nscanned); 310 free(mptr); 311 if (result < 1) 312 return parseError("a float", optname, str, errName); 313 } 314 else 315 { 316 if (sscanf(str.ptr, fmt.ptr, &res, &nscanned) < 1) 317 return parseError("a float", optname, str, errName); 318 } 319 str = str[nscanned .. $]; 320 return true; 321 } 322 323 bool parse(const(char)[] optname, ref inout(char)[] str, ref inout(char)[] res, const(char)[] errName) 324 in { assert(str.length); } 325 do 326 { 327 auto tail = str.find!(c => c == ' '); 328 res = str[0 .. $ - tail.length]; 329 if (!res.length) 330 return parseError("an identifier", optname, str, errName); 331 str = tail; 332 return true; 333 } 334 335 bool parseError(const scope char[] exp, const scope char[] opt, const scope char[] got, const(char)[] errName) 336 { 337 import core.atomic : atomicLoad; 338 339 version (CoreUnittest) if (inUnittest) return false; 340 341 fprintf(atomicLoad(stderr), "Expecting %.*s as argument for %.*s option '%.*s', got '%.*s' instead.\n", 342 cast(int)exp.length, exp.ptr, 343 cast(int)errName.length, errName.ptr, 344 cast(int)opt.length, opt.ptr, 345 cast(int)got.length, got.ptr); 346 return false; 347 } 348 349 bool overflowedError(const scope char[] opt, const scope char[] got) 350 { 351 import core.atomic : atomicLoad; 352 353 version (CoreUnittest) if (inUnittest) return false; 354 355 fprintf(atomicLoad(stderr), "Argument for %.*s option '%.*s' is too big.\n", 356 cast(int)opt.length, opt.ptr, 357 cast(int)got.length, got.ptr); 358 return false; 359 } 360 361 size_t min(size_t a, size_t b) { return a <= b ? a : b; } 362 363 version (CoreUnittest) __gshared bool inUnittest; 364 365 unittest 366 { 367 inUnittest = true; 368 scope (exit) inUnittest = false; 369 370 static struct Config 371 { 372 bool disable; // start disabled 373 ubyte profile; // enable profiling with summary when terminating program 374 string gc = "conservative"; // select gc implementation conservative|manual 375 376 @MemVal size_t initReserve; // initial reserve (bytes) 377 @MemVal size_t minPoolSize = 1 << 20; // initial and minimum pool size (bytes) 378 float heapSizeFactor = 2.0; // heap size to used memory ratio 379 380 @nogc nothrow: 381 void help(); 382 string errorName() @nogc nothrow { return "GC"; } 383 } 384 Config conf; 385 386 assert(!conf.parseOptions("disable")); 387 assert(!conf.parseOptions("disable:")); 388 assert(!conf.parseOptions("disable:5")); 389 assert(conf.parseOptions("disable:y") && conf.disable); 390 assert(conf.parseOptions("disable:n") && !conf.disable); 391 assert(conf.parseOptions("disable:Y") && conf.disable); 392 assert(conf.parseOptions("disable:N") && !conf.disable); 393 assert(conf.parseOptions("disable:1") && conf.disable); 394 assert(conf.parseOptions("disable:0") && !conf.disable); 395 396 assert(conf.parseOptions("disable=y") && conf.disable); 397 assert(conf.parseOptions("disable=n") && !conf.disable); 398 399 assert(conf.parseOptions("profile=0") && conf.profile == 0); 400 assert(conf.parseOptions("profile=1") && conf.profile == 1); 401 assert(conf.parseOptions("profile=2") && conf.profile == 2); 402 assert(!conf.parseOptions("profile=256")); 403 404 assert(conf.parseOptions("disable:1 minPoolSize:16")); 405 assert(conf.disable); 406 assert(conf.minPoolSize == 1024 * 1024 * 16); 407 408 assert(conf.parseOptions("disable:1 minPoolSize:4096B")); 409 assert(conf.disable); 410 assert(conf.minPoolSize == 4096); 411 412 assert(conf.parseOptions("disable:1 minPoolSize:2K help")); 413 assert(conf.disable); 414 assert(conf.minPoolSize == 2048); 415 416 assert(conf.parseOptions("minPoolSize:3G help")); 417 assert(conf.disable); 418 assert(conf.minPoolSize == 1024UL * 1024 * 1024 * 3); 419 420 assert(!conf.parseOptions("minPoolSize:922337203685477G"), "size_t overflow"); 421 422 assert(conf.parseOptions("heapSizeFactor:3.1")); 423 assert(conf.heapSizeFactor == 3.1f); 424 assert(conf.parseOptions("heapSizeFactor:3.1234567890 disable:0")); 425 assert(conf.heapSizeFactor > 3.123f); 426 assert(!conf.disable); 427 assert(!conf.parseOptions("heapSizeFactor:3.0.2.5")); 428 assert(conf.parseOptions("heapSizeFactor:2")); 429 assert(conf.heapSizeFactor == 2.0f); 430 431 assert(!conf.parseOptions("initReserve:foo")); 432 assert(!conf.parseOptions("initReserve:y")); 433 assert(!conf.parseOptions("initReserve:20.5")); 434 435 assert(conf.parseOptions("help")); 436 assert(conf.parseOptions("help profile:1")); 437 assert(conf.parseOptions("help profile:1 help")); 438 439 assert(conf.parseOptions("gc:manual") && conf.gc == "manual"); 440 assert(conf.parseOptions("gc:my-gc~modified") && conf.gc == "my-gc~modified"); 441 assert(conf.parseOptions("gc:conservative help profile:1") && conf.gc == "conservative" && conf.profile == 1); 442 443 // the config parse doesn't know all available GC names, so should accept unknown ones 444 assert(conf.parseOptions("gc:whatever")); 445 }