1 /** 2 * Library that easily serializes and deserializes config files 3 * or directly provide them through the command line based on a 4 * given annotated struct. 5 * 6 * Examples: 7 * Basic use-case 8 * --- 9 * struct MyConfig 10 * { 11 * @Desc("My number.") 12 * int number; 13 * @Desc("My bool.") 14 * bool toggle; 15 * } 16 * 17 * enum usage = "My program version 1.0 does things."; 18 * 19 * int main(string[] args) 20 * { 21 * string[] configArgs = getConfigArguments!Config("myconf.conf", args); 22 * 23 * if (configArgs.length > 0) 24 * { 25 * import std.array : insertInPlace; 26 * 27 * // Prepend them into the command line args 28 * args.insertInPlace(1, configArgs); 29 * } 30 * 31 * MyConfig conf; 32 * bool helpWanted = false; 33 * 34 * import std.getopt : GetOptException; 35 * try 36 * { 37 * conf = initializeConfig!(MyConfig, usage)(args, helpWanted); 38 * } 39 * catch (GetOptException e) 40 * { 41 * import std.stdio : stderr; 42 * stderr.writefln("Invalid argument: %s", e.msg); 43 * return 1; 44 * } 45 * 46 * if (helpWanted) 47 * { 48 * return 0; 49 * } 50 * } 51 * --- 52 */ 53 module zconfig; 54 55 import std.getopt; 56 57 /** 58 * Describes a section. When a struct member is annotated 59 * with it then the member will be written underneath `[section]`. 60 * 61 * During parsing the option will also only be valid if it is 62 * located underneath said `[section]`. It can also be used 63 * as a block annotation. See the examples. 64 * 65 * Examples: 66 * --- 67 * struct MyConfig 68 * { 69 * @Section("foo") @Desc("My number of foo.") 70 * int number; 71 * } 72 * --- 73 * Will serialize to: 74 * ```ini 75 * [foo] 76 * ; My number of foo. 77 * ; Default value: 0 78 * ;number=0 79 * ``` 80 * 81 * Examples: 82 * --- 83 * struct MyConfig 84 * { 85 * @Desc("My \"global\" option.") 86 * int gNumber = -1; 87 * 88 * @Section("bar") 89 * { 90 * @Desc("If true the toggle is activated.") 91 * bool toggleMe; 92 * @Desc("Another number.") 93 * int increment = 1; 94 * } 95 * } 96 * --- 97 * Will serialize to: 98 * ```ini 99 * ; My "global" option. 100 * ; Default value: -1 101 * ;gNumber=-1 102 * 103 * [bar] 104 * ; If true the toggle is activated. 105 * ; Default value: false 106 * ;toggleMe=false 107 * 108 * ; Another number. 109 * ; Default value: 1 110 * ;increment=1 111 * ``` 112 */ 113 struct Section 114 { 115 string section; 116 } 117 118 /** 119 * Adds a description to the option which will be shown 120 * in the command line when `-h` is provided and in the 121 * config file. 122 * 123 * Examples: 124 * --- 125 * MyConfig 126 * { 127 * @Desc("My config option.") 128 * int option; 129 * } 130 * --- 131 * Will be serialized as: 132 * ```ini 133 * ; My config option. 134 * ; Default value: 0 135 * ;option=0 136 * ``` 137 */ 138 struct Desc 139 { 140 string description; 141 } 142 143 /** 144 * Provides an alternative option name. This is mostly 145 * convenient on the command line. 146 * 147 * The shortname will be added verbatim. This allows 148 * to provide multiple alternatives at once. 149 * Internally the struct member name and the short name 150 * will be simply concatenated with the vertical bar ('|'). 151 * 152 * Examples: 153 * --- 154 * struct MyConfig 155 * { 156 * @Short("v") @Desc("Print verbose messages.") 157 * bool verbose; 158 * } 159 * --- 160 * Allows the usage of `./myapp --verbose` and 161 * `./myapp -v`. Both will set the verbose member variable. 162 */ 163 struct Short 164 { 165 string shortname; 166 } 167 168 private struct Argument 169 { 170 string section; 171 string description; 172 string shortname; 173 bool onlyCLI = false; 174 bool configFile = false; 175 bool passThrough = false; 176 bool required = false; 177 } 178 179 /// Do not serialize and deserialize this option from the config file. 180 /// The option will only be available through the command line arguments. 181 enum OnlyCLI; 182 /** 183 * Special option that allows pointing to another config file (ignoring the default). 184 * Can only be provided through the command line arguments. A config struct may only 185 * have one ConfigFile annotation. Otherwise an error is thrown. 186 * 187 * Examples: 188 * --- 189 * struct MyConfig 190 * { 191 * @ConfigFile @Short("c") @Desc("Specific config file to use instead of the default.") 192 * string config = "myconf.conf"; 193 * } 194 * --- 195 */ 196 enum ConfigFile; 197 /// Getopt PassThrough. 198 enum PassThrough; 199 /// Getopt Required. If the option is not provided an exception will be thrown. 200 enum Required; 201 202 /** 203 * Calls getopt with the provided args array. Usually you want to first call 204 * [getConfigArguments] and merge the command line arguments with the config arguments. 205 * Which then get passed to this function. 206 * 207 * The template `ConfigType` is the plain old data struct that 208 * describes the options that are used to generate the getopt parameter list. 209 * `usage` is used for the [defaultGetoptPrinter] for the usage description when `-h` 210 * is provided in the args. 211 * 212 * Params: 213 * args = The arguments to provide to getopt 214 * helpWanted = if `-h` has been parsed by getopt this out parameter will indicate that 215 * 216 * Returns: 217 * A fully filled out `ConfigType` struct 218 * 219 * Examples: 220 * --- 221 * Only parse command line arguments 222 * struct MyConfig 223 * { 224 * @Desc("My number.") 225 * int number; 226 * @Desc("My bool.") 227 * bool toggle; 228 * } 229 * 230 * enum usage = "My program version 1.0 does things."; 231 * 232 * int main(string[] args) 233 * { 234 * import std.getopt : GetOptException; 235 * 236 * MyConfig conf; 237 * bool helpWanted = false; 238 * 239 * try 240 * { 241 * conf = initializeConfig!(MyConfig, usage)(args, helpWanted); 242 * } 243 * catch (GetOptException e) 244 * { 245 * import std.stdio : stderr; 246 * stderr.writefln("Invalid argument: %s", e.msg); 247 * return 1; 248 * } 249 * 250 * if (helpWanted) 251 * { 252 * return 0; 253 * } 254 * } 255 * --- 256 */ 257 ConfigType initializeConfig(ConfigType, string usage)(ref string[] args, out bool helpWanted) 258 { 259 ConfigType newConf; 260 arraySep = ","; 261 mixin(`auto helpInformation = getopt(`, args.stringof, 262 generateGetoptArgumentList!(ConfigType, newConf.stringof), `);`); 263 if (helpInformation.helpWanted) 264 { 265 defaultGetoptPrinter(usage, helpInformation.options); 266 helpWanted = true; 267 } 268 return newConf; 269 } 270 271 private string generateGetoptArgumentList(ConfigType, string configStructName)() 272 { 273 if (__ctfe) 274 { 275 string arglist = ""; 276 immutable ConfigType defaultConfig; 277 foreach (memberName; __traits(allMembers, ConfigType)) 278 { 279 string attribute = memberName; 280 immutable argument = getConfigMemberUDAs!(ConfigType, memberName); 281 static if (argument.passThrough) 282 { 283 arglist ~= `,std.getopt.config.passThrough`; 284 } 285 static if (argument.required && !argument.passThrough) 286 { 287 arglist ~= `,std.getopt.config.required`; 288 } 289 static if (argument.shortname) 290 { 291 attribute ~= `|` ~ argument.shortname; 292 } 293 arglist ~= `,"` ~ attribute ~ `"`; 294 static if (argument.description) 295 { 296 import std.conv : to; 297 import std.format : format; 298 import std.traits : isArray, isSomeString; 299 300 auto member = __traits(getMember, defaultConfig, memberName); 301 static if (isArray!(typeof(member)) && !isSomeString!(typeof(member))) 302 { 303 string defaultValue = format("%-(%s,%)", member); 304 } 305 else 306 { 307 string defaultValue = member.to!string; 308 } 309 310 arglist ~= `,"` ~ argument.description ~ ` Default: ` ~ 311 defaultValue ~ `"`; 312 } 313 arglist ~= ",&" ~ configStructName ~ "." ~ memberName; 314 } 315 return arglist; 316 } 317 else 318 { 319 return ""; 320 } 321 } 322 323 private Argument getConfigMemberUDAs(ConfigType, string memberName)() 324 { 325 Argument arg; 326 foreach (attr; __traits(getAttributes, __traits(getMember, ConfigType, memberName))) 327 { 328 static if (is(typeof(attr) == Section)) 329 { 330 arg.section = attr.section; 331 } 332 else static if (is(typeof(attr) == Short)) 333 { 334 arg.shortname = attr.shortname; 335 } 336 else static if (is(typeof(attr) == Desc)) 337 { 338 arg.description = attr.description; 339 } 340 else static if (is(attr == OnlyCLI)) 341 { 342 arg.onlyCLI = true; 343 } 344 else static if (is(attr == ConfigFile)) 345 { 346 arg.configFile = true; 347 } 348 else static if (is(attr == PassThrough)) 349 { 350 arg.passThrough = true; 351 } 352 else static if (is(attr == Required)) 353 { 354 arg.required = true; 355 } 356 } 357 return arg; 358 } 359 360 /*** 361 * Creates an argument array that contains the options provided in 362 * the config file. 363 * 364 * The resulting array contains all provided and valid 365 * options excluding ones that were provided through the 366 * command line. 367 * The template `ConfigType` is the plain old data struct that 368 * describes the options that are read from the config filename. 369 * 370 * Params: 371 * filename = Filename of the config file 372 * args = Command line arguments provided through main(string[] args) 373 * 374 * Returns: Config exclusive argument array 375 * 376 * Examples: 377 * --------- 378 * struct MyConfig 379 * { 380 * @Desc("My cool number.") 381 * int number; 382 * @Desc("Print verbose messages.") 383 * bool verbose = false; 384 * } 385 * 386 * int main(string[] args) 387 * { 388 * string[] configArgs = getConfigArguments!MyConfig("myconfig.conf", args); 389 * 390 * import std.stdio : writeln; 391 * writeln(configArgs); 392 * } 393 * --------- 394 * Assume we have a `myconfig.conf` file that contains the following options: 395 * ```ini 396 * ; My cool number. 397 * number=5 398 * ; Print verbose messages. 399 * verbose=true 400 * ``` 401 * Calling the above program without any arguments: `./myapp` 402 * will print: `["--number", "5", "--verbose", "true"]`. 403 * 404 * Calling the program with a given argument: `./myapp --number=12` 405 * will print: `["--verbose", "true"]`. 406 * You can see that our provided `--number=12` has been excluded. 407 */ 408 string[] getConfigArguments(ConfigType)(string filename, string[] args) 409 { 410 import std.algorithm : splitter, each, findSplit; 411 import std.stdio : File, writeln; 412 import std.array : empty, split; 413 414 int[string] identifierMap; 415 string[string] shortnameLookupMap; 416 bool haveCustomConfigFile = false; 417 string configFileMember; 418 foreach (memberName; __traits(allMembers, ConfigType)) 419 { 420 immutable argument = getConfigMemberUDAs!(ConfigType, memberName); 421 static if (!argument.onlyCLI && !argument.configFile) 422 { 423 identifierMap[memberName] = 1; 424 } 425 static if (argument.configFile) 426 { 427 assert(haveCustomConfigFile == false, "Can only have one config member with the 'ConfigFile' attribute."); 428 haveCustomConfigFile = true; 429 configFileMember = memberName; 430 } 431 argument.shortname.splitter('|').each!(name => shortnameLookupMap[name] = memberName); 432 } 433 434 int[string] argMap; 435 436 bool argIsConfig = false; 437 string configFilename; 438 439 // Create mappings of each option and extract the special 'ConfigFile' 440 // value if it was provided. The mappings are used to compare against 441 // the values provided in the configuration file. 442 foreach (arg; args) 443 { 444 if (argIsConfig) 445 { 446 configFilename = arg; 447 continue; 448 } 449 import std.string : indexOf; 450 451 auto optionIdentIndex = arg.indexOf(assignChar); 452 string optionIdent; 453 string optionName; 454 455 if (optionIdentIndex < 0) 456 { 457 optionIdentIndex = arg.length; 458 } 459 460 if (arg.length > 2 && arg[0] == optionChar && arg[1] == optionChar) 461 { 462 optionIdent = arg[2 .. optionIdentIndex]; 463 if (optionIdent in shortnameLookupMap) 464 { 465 optionName = shortnameLookupMap[optionIdent]; 466 argMap[optionName] = 1; 467 } 468 else 469 { 470 optionName = optionIdent; 471 argMap[optionIdent] = 1; 472 } 473 } 474 else if (arg.length > 1 && arg[0] == optionChar) 475 { 476 optionIdent = arg[1 .. optionIdentIndex]; 477 if (optionIdent in shortnameLookupMap) 478 { 479 optionName = shortnameLookupMap[optionIdent]; 480 argMap[optionName] = 1; 481 } 482 else 483 { 484 optionName = optionIdent; 485 argMap[optionIdent] = 1; 486 } 487 } 488 if (optionName == configFileMember) 489 { 490 if (optionIdentIndex < arg.length) 491 { 492 configFilename = arg[optionIdentIndex + 1 .. $]; 493 } 494 else 495 { 496 argIsConfig = true; 497 } 498 } 499 } 500 501 if (configFilename == configFilename.init) 502 { 503 configFilename = filename; 504 } 505 506 import std.exception : ErrnoException; 507 import std.stdio : stderr; 508 509 string[string] confMap; 510 File inFile; 511 512 try 513 { 514 inFile = File(configFilename, "r"); 515 } 516 catch (ErrnoException e) 517 { 518 stderr.writefln("Error opening config file: %s", e.msg); 519 return []; 520 } 521 522 scope (exit) 523 inFile.close(); 524 525 foreach (line; inFile.byLine()) 526 { 527 if (line.empty || line[0] == ';') 528 { 529 continue; 530 } 531 if (auto splitted = line.findSplit([assignChar])) 532 { 533 // Only keep the values if there wasn't a command line argument 534 // with the same identifier 535 if (cast(const string) splitted[0] in identifierMap && 536 !(cast(const string) splitted[0] in argMap)) 537 { 538 const key = splitted[0].dup; 539 confMap[key] = splitted[2].dup; 540 } 541 } 542 } 543 544 const confKeys = confMap.keys(); 545 string[] additionalConfArgs; 546 additionalConfArgs.reserve(confKeys.length); 547 foreach (name; confKeys) 548 { 549 import std.utf : toUTF8; 550 551 additionalConfArgs ~= [[optionChar].toUTF8 ~ [optionChar].toUTF8 ~ name, confMap[name]]; 552 } 553 554 return additionalConfArgs; 555 } 556 557 /** 558 * Writes an example config file to the provided filename. 559 * If a struct member does not provide a default value then 560 * .init is used as the default value. 561 * 562 * The only exception being arrays which would normally 563 * default to '[]' are instead simply left blank. If an array 564 * provides a default list then they are printed comma separated. 565 * The reason is that getopt expects such format instead of the 566 * brackets. 567 568 * Params: 569 * filename = Filename to write the example config file to 570 */ 571 void writeExampleConfigFile(ConfigType)(const string filename) 572 { 573 import std.stdio : File; 574 import std.format : formattedWrite, format; 575 import std.string : wrap; 576 import std.array : appender; 577 import std.conv : to; 578 import std.traits : isArray, isSomeString; 579 580 string currentSection = ""; 581 immutable ConfigType defaultConfig; 582 auto outFile = File(filename, "w+"); 583 scope (exit) 584 outFile.close(); 585 auto app = appender!string; 586 587 foreach (memberName; __traits(allMembers, ConfigType)) 588 { 589 immutable argument = getConfigMemberUDAs!(ConfigType, memberName); 590 if (argument.onlyCLI == true || argument.configFile) 591 { 592 continue; 593 } 594 if (currentSection != argument.section) 595 { 596 currentSection = argument.section; 597 app.formattedWrite("[%s]\n", currentSection); 598 } 599 static if (argument.description) 600 { 601 app.formattedWrite("%s", wrap(argument.description, 80, "; ", "; ")); 602 } 603 604 auto member = __traits(getMember, defaultConfig, memberName); 605 static if (isArray!(typeof(member)) && !isSomeString!(typeof(member))) 606 { 607 string defaultValue = format("%-(%s,%)", member); 608 } 609 else 610 { 611 string defaultValue = member.to!string; 612 } 613 app.formattedWrite("; Default value: %s\n", defaultValue); 614 app.formattedWrite(";%s=%s\n", memberName, defaultValue); 615 app ~= "\n"; 616 } 617 outFile.write(app.data); 618 }