Home | History | Annotate | Line # | Download | only in internal
      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     version (CoreUnittest) if (inUnittest) return false;
    150 
    151     fprintf(stderr, "%.*s %.*s option '%.*s'.\n",
    152             cast(int)msg.length, msg.ptr,
    153             cast(int)errName.length, errName.ptr,
    154             cast(int)name.length, name.ptr);
    155     return false;
    156 }
    157 
    158 inout(char)[] skip(alias pred)(inout(char)[] str)
    159 {
    160     return find!(c => !pred(c))(str);
    161 }
    162 
    163 inout(char)[] find(alias pred)(inout(char)[] str)
    164 {
    165     foreach (i; 0 .. str.length)
    166         if (pred(str[i])) return str[i .. $];
    167     return null;
    168 }
    169 
    170 bool parse(T : size_t)(const(char)[] optname, ref inout(char)[] str, ref T res, const(char)[] errName, bool mayHaveSuffix = false)
    171 in { assert(str.length); }
    172 do
    173 {
    174     size_t i, v;
    175 
    176     auto tail = find!(c => c == ' ')(str);
    177     size_t len = str.length - tail.length;
    178 
    179     import core.checkedint : mulu;
    180 
    181     bool overflowed;
    182 
    183     for (; i < len; i++)
    184     {
    185         char c = str[i];
    186 
    187         if (isdigit(c))
    188             v = 10 * v + c - '0';
    189         else // non-digit
    190         {
    191             if (mayHaveSuffix && i == len-1) // suffix
    192             {
    193                 switch (c)
    194                 {
    195 
    196                     case 'G':
    197                         v = mulu(v, 1024 * 1024 * 1024, overflowed);
    198                         break;
    199 
    200                     case 'M':
    201                         v = mulu(v, 1024 * 1024, overflowed);
    202                         break;
    203 
    204                     case 'K':
    205                         v = mulu(v, 1024, overflowed);
    206                         break;
    207 
    208                     case 'B':
    209                         break;
    210 
    211                     default:
    212                         return parseError("value with unit type M, K or B", optname, str, "with suffix");
    213                 }
    214 
    215                 if (overflowed)
    216                     return overflowedError(optname, str);
    217 
    218                 i++;
    219                 break;
    220             }
    221             else // unexpected non-digit character
    222             {
    223                 i = 0;
    224                 break;
    225             }
    226         }
    227     }
    228 
    229     if (!i)
    230         return parseError("a number", optname, str, errName);
    231 
    232     if (mayHaveSuffix && isdigit(str[len-1]))
    233     {
    234         // No suffix found, default to megabytes
    235 
    236         v = mulu(v, 1024 * 1024, overflowed);
    237 
    238         if (overflowed)
    239             return overflowedError(optname, str);
    240     }
    241 
    242     if (v > res.max)
    243         return parseError("a number " ~ T.max.stringof ~ " or below", optname, str[0 .. i], errName);
    244     str = str[i .. $];
    245     res = cast(T) v;
    246     return true;
    247 }
    248 
    249 bool parse(const(char)[] optname, ref inout(char)[] str, ref bool res, const(char)[] errName)
    250 in { assert(str.length); }
    251 do
    252 {
    253     if (str[0] == '1' || str[0] == 'y' || str[0] == 'Y')
    254         res = true;
    255     else if (str[0] == '0' || str[0] == 'n' || str[0] == 'N')
    256         res = false;
    257     else
    258         return parseError("'0/n/N' or '1/y/Y'", optname, str, errName);
    259     str = str[1 .. $];
    260     return true;
    261 }
    262 
    263 bool parse(const(char)[] optname, ref inout(char)[] str, ref float res, const(char)[] errName)
    264 in { assert(str.length); }
    265 do
    266 {
    267     // % uint f %n \0
    268     char[1 + 10 + 1 + 2 + 1] fmt=void;
    269     // specify max-width
    270     immutable n = snprintf(fmt.ptr, fmt.length, "%%%uf%%n", cast(uint)str.length);
    271     assert(n > 4 && n < fmt.length);
    272 
    273     int nscanned;
    274     version (CRuntime_DigitalMars)
    275     {
    276         /* Older sscanf's in snn.lib can write to its first argument, causing a crash
    277         * if the string is in readonly memory. Recent updates to DMD
    278         * https://github.com/dlang/dmd/pull/6546
    279         * put string literals in readonly memory.
    280         * Although sscanf has been fixed,
    281         * http://ftp.digitalmars.com/snn.lib
    282         * this workaround is here so it still works with the older snn.lib.
    283         */
    284         // Create mutable copy of str
    285         const length = str.length;
    286         char* mptr = cast(char*)malloc(length + 1);
    287         assert(mptr);
    288         memcpy(mptr, str.ptr, length);
    289         mptr[length] = 0;
    290         const result = sscanf(mptr, fmt.ptr, &res, &nscanned);
    291         free(mptr);
    292         if (result < 1)
    293             return parseError("a float", optname, str, errName);
    294     }
    295     else
    296     {
    297         if (sscanf(str.ptr, fmt.ptr, &res, &nscanned) < 1)
    298             return parseError("a float", optname, str, errName);
    299     }
    300     str = str[nscanned .. $];
    301     return true;
    302 }
    303 
    304 bool parse(const(char)[] optname, ref inout(char)[] str, ref inout(char)[] res, const(char)[] errName)
    305 in { assert(str.length); }
    306 do
    307 {
    308     auto tail = str.find!(c => c == ' ');
    309     res = str[0 .. $ - tail.length];
    310     if (!res.length)
    311         return parseError("an identifier", optname, str, errName);
    312     str = tail;
    313     return true;
    314 }
    315 
    316 bool parseError(const scope char[] exp, const scope char[] opt, const scope char[] got, const(char)[] errName)
    317 {
    318     version (CoreUnittest) if (inUnittest) return false;
    319 
    320     fprintf(stderr, "Expecting %.*s as argument for %.*s option '%.*s', got '%.*s' instead.\n",
    321             cast(int)exp.length, exp.ptr,
    322             cast(int)errName.length, errName.ptr,
    323             cast(int)opt.length, opt.ptr,
    324             cast(int)got.length, got.ptr);
    325     return false;
    326 }
    327 
    328 bool overflowedError(const scope char[] opt, const scope char[] got)
    329 {
    330     version (CoreUnittest) if (inUnittest) return false;
    331 
    332     fprintf(stderr, "Argument for %.*s option '%.*s' is too big.\n",
    333             cast(int)opt.length, opt.ptr,
    334             cast(int)got.length, got.ptr);
    335     return false;
    336 }
    337 
    338 size_t min(size_t a, size_t b) { return a <= b ? a : b; }
    339 
    340 version (CoreUnittest) __gshared bool inUnittest;
    341 
    342 unittest
    343 {
    344     inUnittest = true;
    345     scope (exit) inUnittest = false;
    346 
    347     static struct Config
    348     {
    349         bool disable;            // start disabled
    350         ubyte profile;           // enable profiling with summary when terminating program
    351         string gc = "conservative"; // select gc implementation conservative|manual
    352 
    353         @MemVal size_t initReserve;      // initial reserve (bytes)
    354         @MemVal size_t minPoolSize = 1 << 20;  // initial and minimum pool size (bytes)
    355         float heapSizeFactor = 2.0; // heap size to used memory ratio
    356 
    357         @nogc nothrow:
    358         void help();
    359         string errorName() @nogc nothrow { return "GC"; }
    360     }
    361     Config conf;
    362 
    363     assert(!conf.parseOptions("disable"));
    364     assert(!conf.parseOptions("disable:"));
    365     assert(!conf.parseOptions("disable:5"));
    366     assert(conf.parseOptions("disable:y") && conf.disable);
    367     assert(conf.parseOptions("disable:n") && !conf.disable);
    368     assert(conf.parseOptions("disable:Y") && conf.disable);
    369     assert(conf.parseOptions("disable:N") && !conf.disable);
    370     assert(conf.parseOptions("disable:1") && conf.disable);
    371     assert(conf.parseOptions("disable:0") && !conf.disable);
    372 
    373     assert(conf.parseOptions("disable=y") && conf.disable);
    374     assert(conf.parseOptions("disable=n") && !conf.disable);
    375 
    376     assert(conf.parseOptions("profile=0") && conf.profile == 0);
    377     assert(conf.parseOptions("profile=1") && conf.profile == 1);
    378     assert(conf.parseOptions("profile=2") && conf.profile == 2);
    379     assert(!conf.parseOptions("profile=256"));
    380 
    381     assert(conf.parseOptions("disable:1 minPoolSize:16"));
    382     assert(conf.disable);
    383     assert(conf.minPoolSize == 1024 * 1024 * 16);
    384 
    385     assert(conf.parseOptions("disable:1 minPoolSize:4096B"));
    386     assert(conf.disable);
    387     assert(conf.minPoolSize == 4096);
    388 
    389     assert(conf.parseOptions("disable:1 minPoolSize:2K help"));
    390     assert(conf.disable);
    391     assert(conf.minPoolSize == 2048);
    392 
    393     assert(conf.parseOptions("minPoolSize:3G help"));
    394     assert(conf.disable);
    395     assert(conf.minPoolSize == 1024UL * 1024 * 1024 * 3);
    396 
    397     assert(!conf.parseOptions("minPoolSize:922337203685477G"), "size_t overflow");
    398 
    399     assert(conf.parseOptions("heapSizeFactor:3.1"));
    400     assert(conf.heapSizeFactor == 3.1f);
    401     assert(conf.parseOptions("heapSizeFactor:3.1234567890 disable:0"));
    402     assert(conf.heapSizeFactor > 3.123f);
    403     assert(!conf.disable);
    404     assert(!conf.parseOptions("heapSizeFactor:3.0.2.5"));
    405     assert(conf.parseOptions("heapSizeFactor:2"));
    406     assert(conf.heapSizeFactor == 2.0f);
    407 
    408     assert(!conf.parseOptions("initReserve:foo"));
    409     assert(!conf.parseOptions("initReserve:y"));
    410     assert(!conf.parseOptions("initReserve:20.5"));
    411 
    412     assert(conf.parseOptions("help"));
    413     assert(conf.parseOptions("help profile:1"));
    414     assert(conf.parseOptions("help profile:1 help"));
    415 
    416     assert(conf.parseOptions("gc:manual") && conf.gc == "manual");
    417     assert(conf.parseOptions("gc:my-gc~modified") && conf.gc == "my-gc~modified");
    418     assert(conf.parseOptions("gc:conservative help profile:1") && conf.gc == "conservative" && conf.profile == 1);
    419 
    420     // the config parse doesn't know all available GC names, so should accept unknown ones
    421     assert(conf.parseOptions("gc:whatever"));
    422 }
    423