configparser.d 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. ///
  2. /// Permission to use, copy, modify, and/or distribute this software for any
  3. /// purpose with or without fee is herby granted.
  4. ///
  5. /// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  6. /// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  7. /// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  8. /// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  9. /// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  10. /// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  11. /// OR IN CONNECTION WITH THE USE OR PEFORMANCE OF THIS SOFTWARE.
  12. ///
  13. ///
  14. /// An incomplete single-file INI parser for D.
  15. ///
  16. /// The API should be similar to python's configparse module. Internally it
  17. /// uses the standard D associative array.
  18. ///
  19. /// Example:
  20. /// ---
  21. /// import configparser;
  22. ///
  23. /// auto config = new ConfigParser();
  24. /// // no sections initially
  25. /// assert(config.sections.length == 0);
  26. /// // Section names ("Program Settings") are case-sensitive
  27. /// conf.addSection("Storage Paths");
  28. /// // Option names ("CONFIG_PATH") are case-insensitive
  29. /// // (internally, they are all converted to lower-case)
  30. /// conf.set("Program Settings", "CONFIG_PATH", "/home/user/.local/config");
  31. /// ---
  32. ///
  33. /// Authors: Mio
  34. /// Date: 2023-11-11
  35. /// Homepage: https://codeberg.org/supercell/mlib
  36. /// License: 0BSD
  37. /// Version: 0.5.2
  38. ///
  39. /// History:
  40. /// 0.5.2 Make sections() and options() to be @trusted
  41. /// 0.5.1 Implement ConfigParserException
  42. /// 0.5 Add .write(string), .write(OutputRange), and .readString(string)
  43. /// 0.4 Add .write(File)
  44. /// 0.3 Fix option values not always being treated as lowercase.
  45. /// 0.2 Add .getBool()
  46. /// 0.1 Initial release
  47. ///
  48. module mlib.configparser;
  49. import std.conv : ConvException;
  50. import std.stdio : File;
  51. import std.range.primitives : isOutputRange;
  52. public class ConfigParserException : Exception
  53. {
  54. this(string msg) @safe pure
  55. {
  56. super(msg);
  57. }
  58. }
  59. public class DuplicateSectionException : ConfigParserException
  60. {
  61. private string m_section;
  62. this(string section) @safe pure
  63. {
  64. const msg = "Section " ~ section ~ " already exists.";
  65. m_section = section;
  66. super(msg);
  67. }
  68. ///
  69. /// The section that caused this exception.
  70. ///
  71. string section() const @safe pure
  72. {
  73. return m_section;
  74. }
  75. }
  76. ///
  77. /// An exception that is thrown by a strict parser which indicates
  78. /// that an option appears twice within any one section.
  79. ///
  80. public class DuplicateOptionException : ConfigParserException
  81. {
  82. private string m_option;
  83. private string m_section;
  84. this(string option, string section) @safe pure
  85. {
  86. const msg = "Option " ~ option ~ " in section " ~ section ~ " already exists.";
  87. m_option = option;
  88. m_section = section;
  89. super(msg);
  90. }
  91. ///
  92. /// The option that caused this exception.
  93. ///
  94. string option() const @safe pure
  95. {
  96. return m_option;
  97. }
  98. ///
  99. /// The section that the responsible option is a part of.
  100. ///
  101. string section() const @safe pure
  102. {
  103. return m_section;
  104. }
  105. }
  106. ///
  107. /// An exception that is thrown when a specified section could not be
  108. /// found.
  109. ///
  110. public class NoSectionException : ConfigParserException
  111. {
  112. private string m_section;
  113. this(string section) @safe pure
  114. {
  115. const msg = "Section '" ~ section ~ "' does not exist.";
  116. m_section = section;
  117. super(msg);
  118. }
  119. ///
  120. /// The section that could not be found.
  121. ///
  122. string section() const @safe pure
  123. {
  124. return m_section;
  125. }
  126. }
  127. ///
  128. /// An exception that is thrown when a specified option could not be
  129. /// found in the specified section.
  130. ///
  131. public class NoOptionException : ConfigParserException
  132. {
  133. private string m_section;
  134. private string m_option;
  135. this(string section, string option) @safe pure
  136. {
  137. const msg = "Section '" ~ section ~ "' does not have option '" ~ option ~ "'.";
  138. m_section = section;
  139. m_option = option;
  140. super(msg);
  141. }
  142. ///
  143. /// The section that was specified.
  144. ///
  145. string section() const @safe pure
  146. {
  147. return m_section;
  148. }
  149. ///
  150. /// The option that could not be found.
  151. ///
  152. string option() const @safe pure
  153. {
  154. return m_option;
  155. }
  156. }
  157. ///
  158. /// The main configuration parser.
  159. ///
  160. public class ConfigParser
  161. {
  162. private char[] m_delimiters;
  163. private char[] m_commentPrefixes;
  164. private bool m_strict;
  165. /** current section for parsing */
  166. private string m_currentSection;
  167. private string[string][string] m_sections;
  168. ///
  169. /// Creates a new instance of ConfigParser.
  170. ///
  171. /// Params:
  172. /// delimiters = The characters used to divide keys from values.
  173. /// commentPrefixes = The characters used to prefix comments in
  174. /// otherwise empty lines.
  175. /// strict = Should the parser prevent any duplicate sections or
  176. /// options when reading from a single source.
  177. ///
  178. this(char[] delimiters = ['=', ':'], char[] commentPrefixes = ['#', ';'],
  179. bool strict = true) @safe pure
  180. {
  181. m_delimiters = delimiters;
  182. m_commentPrefixes = commentPrefixes;
  183. m_strict = strict;
  184. }
  185. ///
  186. /// Return an array containing the available sections.
  187. ///
  188. string[] sections() const @trusted pure
  189. {
  190. return m_sections.keys();
  191. }
  192. ///
  193. @safe pure unittest
  194. {
  195. auto conf = new ConfigParser();
  196. assert(0 == conf.sections().length);
  197. conf.addSection("Section");
  198. assert(1 == conf.sections().length);
  199. }
  200. ///
  201. /// Add a section named `section` to the instance.
  202. ///
  203. /// Throws:
  204. /// - DuplicateSectionError if a section by the given name already
  205. /// exists.
  206. ///
  207. void addSection(string section) @safe pure
  208. {
  209. if (section in m_sections)
  210. {
  211. throw new DuplicateSectionException(section);
  212. }
  213. m_sections[section] = null;
  214. }
  215. ///
  216. @safe pure unittest
  217. {
  218. import std.exception : assertNotThrown, assertThrown;
  219. auto conf = new ConfigParser();
  220. /* doesn't yet exist */
  221. assertNotThrown!DuplicateSectionException(conf.addSection("sample"));
  222. /* already exists */
  223. assertThrown!DuplicateSectionException(conf.addSection("sample"));
  224. }
  225. ///
  226. /// Indicates whether the named `section` is present in the configuration.
  227. ///
  228. /// Params:
  229. /// section = The section to check for in the configuration.
  230. ///
  231. /// Returns: `true` if the section exists, `false` otherwise.
  232. ///
  233. bool hasSection(string section) @safe pure const
  234. {
  235. return (section in m_sections) !is null;
  236. }
  237. ///
  238. @safe pure unittest
  239. {
  240. auto conf = new ConfigParser();
  241. conf.addSection("nExt");
  242. assert(true == conf.hasSection("nExt"), "Close the world.");
  243. assert(false == conf.hasSection("world"), "Open the nExt.");
  244. }
  245. ///
  246. /// Returns a list of options available from the specified *section*.
  247. ///
  248. /// Throws:
  249. /// - NoSectionException if the specified exception does not exist.
  250. ///
  251. string[] options(string section) @trusted pure const
  252. {
  253. if (false == this.hasSection(section))
  254. {
  255. throw new NoSectionException(section);
  256. }
  257. return m_sections[section].keys();
  258. }
  259. ///
  260. @safe pure unittest
  261. {
  262. import std.exception : assertNotThrown, assertThrown;
  263. auto conf = new ConfigParser();
  264. conf.addSection("Settings");
  265. assertNotThrown!NoSectionException(conf.options("Settings"));
  266. assertThrown!NoSectionException(conf.options("void"));
  267. string[] options = conf.options("Settings");
  268. assert(0 == options.length, "More keys than we need");
  269. }
  270. ///
  271. /// If the given *section* exists, and contains the given *option*,
  272. /// return true; otherwise return false.
  273. ///
  274. bool hasOption(string section, string option) @safe pure const
  275. {
  276. import std.string : toLower;
  277. if (false == this.hasSection(section))
  278. {
  279. return false;
  280. }
  281. const lowercaseOption = toLower(option);
  282. return (lowercaseOption in m_sections[section]) !is null;
  283. }
  284. ///
  285. @safe pure unittest
  286. {
  287. auto writer = new ConfigParser();
  288. writer.addSection("valid_section");
  289. writer.set("valid_section", "valid_option", "value");
  290. assert(false == writer.hasOption("invalid_section", "invalid_option"));
  291. assert(false == writer.hasOption("valid_section", "invalid_option"));
  292. assert(true == writer.hasOption("valid_section", "valid_option"));
  293. }
  294. /*
  295. string[] read(string[] filenames)
  296. {
  297. return null;
  298. }*/
  299. ///
  300. /// Attempt to read and parse configuration data from a file
  301. /// specified by *filename*.
  302. ///
  303. void read(string filename) @safe
  304. {
  305. File file = File(filename, "r");
  306. scope(exit) { file.close(); }
  307. read(file, false);
  308. }
  309. ///
  310. @safe unittest
  311. {
  312. import std.file : remove;
  313. import std.stdio : File;
  314. auto configFile = File("test.conf", "w+");
  315. configFile.writeln("[Section 1]");
  316. configFile.writeln("key=value");
  317. configFile.writeln("\n[Section 2]");
  318. configFile.writeln("key2 = value");
  319. configFile.close();
  320. auto conf = new ConfigParser();
  321. conf.read("test.conf");
  322. assert(2 == conf.sections.length, "Incorrect Sections length");
  323. assert(true == conf.hasSection("Section 1"),
  324. "Config file doesn't have Section 1");
  325. assert(true == conf.hasOption("Section 1", "key"),
  326. "Config file doesn't have 'key' in 'Section 1'");
  327. remove("test.conf");
  328. }
  329. ///
  330. /// Parse a config file.
  331. ///
  332. /// Params:
  333. /// file = Reference to the file from which to read.
  334. /// close = Close the file when finished parsing.
  335. ///
  336. void read(ref File file, bool close = true) @trusted
  337. {
  338. import std.array : array;
  339. import std.algorithm.searching : canFind;
  340. import std.string : strip;
  341. scope(exit)
  342. {
  343. if (close)
  344. {
  345. file.close();
  346. }
  347. }
  348. foreach(const(char)[] inputLine; file.byLine)
  349. {
  350. const line = cast(string)strip(inputLine);
  351. if (line == "" || canFind(m_commentPrefixes, line[0]))
  352. {
  353. /* ignore empty lines or comments */
  354. continue;
  355. }
  356. if ('[' == line[0])
  357. {
  358. parseSectionHeader(line);
  359. }
  360. else
  361. {
  362. parseLine(line);
  363. }
  364. }
  365. }
  366. ///
  367. /// Parser configuration data from a string.
  368. ///
  369. void readString(string str) @safe pure
  370. {
  371. import std.algorithm.searching : canFind;
  372. import std.string : lineSplitter, strip;
  373. foreach(inputLine; lineSplitter(str))
  374. {
  375. const line = strip(inputLine);
  376. if ("" == line || canFind(m_commentPrefixes, line[0]))
  377. {
  378. /* ignore empty lines or comment lines */
  379. continue;
  380. }
  381. if ('[' == line[0])
  382. {
  383. parseSectionHeader(line);
  384. }
  385. else
  386. {
  387. parseLine(line);
  388. }
  389. }
  390. }
  391. ///
  392. @safe pure unittest
  393. {
  394. const input = "[section]
  395. option = value
  396. [second section]
  397. option = value";
  398. auto reader = new ConfigParser();
  399. reader.readString(input);
  400. assert(reader.hasSection("section"));
  401. assert(reader.hasSection("second section"));
  402. assert("value" == reader.get("section", "option"));
  403. assert("value" == reader.get("second section", "option"));
  404. }
  405. ///
  406. /// Get an `option` value for the named `section`.
  407. ///
  408. /// Params:
  409. /// section = The section to look for the given `option`.
  410. /// option = The option to return the value of
  411. /// fallback = Fallback value if the `option` is not found. Can be null.
  412. ///
  413. /// Returns:
  414. /// - The value for `option` if it is found.
  415. /// - `null` if the `option` is not found and `fallback` is not provided.
  416. /// - `fallback` if the `option` is not found and `fallback` is provided.
  417. ///
  418. /// Throws:
  419. /// - NoSectionException if the `section` does not exist and no fallback is provided.
  420. /// - NoOptionException if the `option` does not exist and no fallback is provided.
  421. ///
  422. string get(string section, string option) @safe pure const
  423. {
  424. import std.string : toLower;
  425. const lowercaseOption = toLower(option);
  426. if (false == this.hasSection(section))
  427. {
  428. throw new NoSectionException(section);
  429. }
  430. if (false == this.hasOption(section, lowercaseOption))
  431. {
  432. throw new NoOptionException(section, lowercaseOption);
  433. }
  434. return m_sections[section][lowercaseOption];
  435. }
  436. ///
  437. @safe pure unittest
  438. {
  439. import std.exception : assertThrown;
  440. auto conf = new ConfigParser();
  441. conf.addSection("Section");
  442. conf.set("Section", "option", "value");
  443. assert(conf.get("Section", "option") == "value");
  444. assertThrown!NoSectionException(conf.get("section", "option"));
  445. assertThrown!NoOptionException(conf.get("Section", "void"));
  446. }
  447. /// Ditto
  448. string get(string section, string option, string fallback) @safe pure const
  449. {
  450. try
  451. {
  452. return get(section, option);
  453. }
  454. catch (NoSectionException e)
  455. {
  456. return fallback;
  457. }
  458. catch (NoOptionException e)
  459. {
  460. return fallback;
  461. }
  462. }
  463. ///
  464. @safe pure unittest
  465. {
  466. import std.exception : assertThrown;
  467. auto conf = new ConfigParser();
  468. conf.addSection("Section");
  469. conf.set("Section", "option", "value");
  470. assert("value" == conf.get("Section", "option"));
  471. assert("fallback" == conf.get("section", "option", "fallback"));
  472. assert("fallback" == conf.get("Section", "void", "fallback"));
  473. /* can use null for fallback */
  474. assert(null is conf.get("section", "option", null));
  475. assert(null is conf.get("Section", "void", null));
  476. }
  477. ///
  478. /// A convenience method which parses the value of `option` in `section`
  479. /// to an integer.
  480. ///
  481. /// Params:
  482. /// section = The section to look for the given `option`.
  483. /// option = The option to return the value for.
  484. /// fallback = The fallback value to use if `option` isn't found.
  485. ///
  486. /// Throws:
  487. /// - NoSectionFoundException if `section` doesn't exist.
  488. /// - NoOptionFoundException if the `section` doesn't contain `option`.
  489. /// - ConvException if it failed to parse the value to an int.
  490. /// - ConvOverflowException if the value would overflow an int.
  491. /// /// See_Also: get()
  492. ///
  493. int getInt(string section, string option) @safe pure const
  494. {
  495. import std.conv : parse;
  496. auto res = get(section, option);
  497. return parse!int(res);
  498. }
  499. /// Ditto
  500. int getInt(string section, string option, int fallback) @safe pure const
  501. {
  502. try
  503. {
  504. return getInt(section, option);
  505. }
  506. catch (NoSectionException nse)
  507. {
  508. return fallback;
  509. }
  510. catch (NoOptionException noe)
  511. {
  512. return fallback;
  513. }
  514. catch (ConvException ce)
  515. {
  516. return fallback;
  517. }
  518. }
  519. /*
  520. double getDouble(string section, string option)
  521. {
  522. }
  523. double getDouble(string section, string option, double fallback)
  524. {
  525. }
  526. float getFloat(string section, string option)
  527. {
  528. }
  529. float getFloat(string section, string option, float fallback)
  530. {
  531. }*/
  532. ///
  533. /// A convenience method which coerces the $(I option) in the
  534. /// specified $(I section) to a boolean value.
  535. ///
  536. /// Note that the accepted values for the option are "1", "yes",
  537. /// "true", and "on", which cause this method to return `true`, and
  538. /// "0", "no", "false", and "off", which cause it to return `false`.
  539. ///
  540. /// These string values are checked in a case-insensitive manner.
  541. ///
  542. /// Params:
  543. /// section = The section to look for the given option.
  544. /// option = The option to return the value for.
  545. /// fallback = The fallback value to use if the option was not found.
  546. ///
  547. /// Throws:
  548. /// - NoSectionFoundException if `section` doesn't exist.
  549. /// - NoOptionFoundException if the `section` doesn't contain `option`.
  550. /// - ConvException if any other value was found.
  551. ///
  552. bool getBool(string section, string option) @safe pure const
  553. {
  554. import std.string : toLower;
  555. const value = get(section, option).toLower;
  556. switch (value)
  557. {
  558. case "1":
  559. case "yes":
  560. case "true":
  561. case "on":
  562. return true;
  563. case "0":
  564. case "no":
  565. case "false":
  566. case "off":
  567. return false;
  568. default:
  569. throw new ConvException("No valid boolean value found");
  570. }
  571. }
  572. /// Ditto
  573. bool getBool(string section, string option, bool fallback) @safe pure const
  574. {
  575. try
  576. {
  577. return getBool(section, option);
  578. }
  579. catch (NoSectionException e)
  580. {
  581. return fallback;
  582. }
  583. catch (NoOptionException e)
  584. {
  585. return fallback;
  586. }
  587. catch (ConvException e)
  588. {
  589. return fallback;
  590. }
  591. }
  592. /*
  593. string[string] items(string section)
  594. {
  595. }*/
  596. ///
  597. /// Remove the specified `option` from the specified `section`.
  598. ///
  599. /// Params:
  600. /// section = The section to remove from.
  601. /// option = The option to remove from section.
  602. ///
  603. /// Retruns:
  604. /// `true` if option existed, false otherwise.
  605. ///
  606. /// Throws:
  607. /// - NoSectionException if the specified section doesn't exist.
  608. ///
  609. bool removeOption(string section, string option) @safe pure
  610. {
  611. if ((section in m_sections) is null)
  612. {
  613. throw new NoSectionException(section);
  614. }
  615. if (option in m_sections[section])
  616. {
  617. m_sections[section].remove(option);
  618. return true;
  619. }
  620. return false;
  621. }
  622. ///
  623. @safe pure unittest
  624. {
  625. import std.exception : assertThrown;
  626. auto conf = new ConfigParser();
  627. conf.addSection("Default");
  628. conf.set("Default", "exists", "true");
  629. assertThrown!NoSectionException(conf.removeOption("void", "false"));
  630. assert(false == conf.removeOption("Default", "void"));
  631. assert(true == conf.removeOption("Default", "exists"));
  632. }
  633. ///
  634. /// Remove the specified `section` from the config.
  635. ///
  636. /// Params:
  637. /// section = The section to remove.
  638. ///
  639. /// Returns:
  640. /// `true` if the section existed, `false` otherwise.
  641. ///
  642. bool removeSection(string section) @safe pure
  643. {
  644. if (section in m_sections)
  645. {
  646. m_sections.remove(section);
  647. return true;
  648. }
  649. return false;
  650. }
  651. ///
  652. @safe pure unittest
  653. {
  654. auto conf = new ConfigParser();
  655. conf.addSection("Exists");
  656. assert(false == conf.removeSection("DoesNotExist"));
  657. assert(true == conf.removeSection("Exists"));
  658. }
  659. ///
  660. /// If the given $(I section) exists, set the given $(I option) to the
  661. /// specified $(I value).
  662. ///
  663. /// Throws:
  664. /// - NoSectionException if the $(I section) does $(B not) exist.
  665. ///
  666. void set(string section, string option, string value) @safe pure
  667. {
  668. import std.string : toLower;
  669. if (false == this.hasSection(section))
  670. {
  671. throw new NoSectionException(section);
  672. }
  673. const lowercaseOption = toLower(option);
  674. m_sections[section][lowercaseOption] = value;
  675. }
  676. ///
  677. @safe pure unittest
  678. {
  679. import std.exception : assertThrown;
  680. auto conf = new ConfigParser();
  681. assertThrown!NoSectionException(conf.set("Section", "option", "value"));
  682. conf.addSection("Section");
  683. conf.set("Section", "option", "value");
  684. assert(conf.get("Section", "option") == "value");
  685. }
  686. ///
  687. /// Write a representation of the configuration to the
  688. /// provided *file*.
  689. ///
  690. /// This representation can be parsed by future calls to
  691. /// `read`. This does **not** close the file after writing.
  692. ///
  693. /// Params:
  694. /// file = An open file which was opened in text mode.
  695. /// spaceAroundDelimiters = The delimiters between keys and
  696. /// values are surrounded by spaces.
  697. ///
  698. /// Note: Comments from the original file are not preserved when
  699. /// writing the configuration back.
  700. ///
  701. void write(ref File file, bool spaceAroundDelimiters = true) @safe const
  702. {
  703. const del = spaceAroundDelimiters ? " = " : "=";
  704. foreach(const section, const options; m_sections)
  705. {
  706. file.writefln("[%s]", section);
  707. foreach(const option, const value; options)
  708. {
  709. file.writefln("%s%s%s", option, del, value);
  710. }
  711. }
  712. }
  713. ///
  714. @safe unittest
  715. {
  716. import std.file : remove;
  717. import std.stdio : File;
  718. auto writer = new ConfigParser();
  719. writer.addSection("general");
  720. writer.addSection("GUI");
  721. writer.set("GUI", "WINDOW_WIDTH", "848");
  722. writer.set("GUI", "WINDOW_HEIGHT", "480");
  723. auto file = File("test.ini", "w+");
  724. scope(exit) remove(file.name);
  725. writer.write(file);
  726. file.rewind();
  727. auto reader = new ConfigParser();
  728. reader.read(file);
  729. assert(reader.hasSection("general"), "reader does not contain general section");
  730. assert(reader.hasSection("GUI"), "reader does not contain GUI section");
  731. assert(reader.get("GUI", "WINDOW_WIDTH") == "848", "reader GUI.WINDOW_WIDTH is not 848");
  732. assert(reader.getInt("GUI", "WINDOW_WIDTH") == 848, "reader GUI.WINDOW_WIDTH is not 848 (int)");
  733. assert(reader.get("GUI", "WINDOW_HEIGHT") == "480", "reader GUI.WINDOW_HEIGHT is not 480");
  734. assert(reader.getInt("GUI", "WINDOW_HEIGHT") == 480, "reader GUI.WINDOW_HEIGHT is not 480 (int)");
  735. }
  736. ///
  737. /// Write a representation of the configuration to the
  738. /// specified *filename*.
  739. ///
  740. /// This representation can be parsed by future calls to
  741. /// `read`. This does **not** close the file after writing.
  742. ///
  743. /// Params:
  744. /// filename = The name of the file to write to.
  745. /// spaceAroundDelimiters = The delimiters between keys and
  746. /// values are surrounded by spaces.
  747. ///
  748. /// Note: Comments from the original file are not preserved when
  749. /// writing the configuration back.
  750. ///
  751. void write(string filename, bool spaceAroundDelimiters = true) @safe const
  752. {
  753. auto file = File(filename, "w+");
  754. write(file, spaceAroundDelimiters);
  755. }
  756. ///
  757. @safe unittest
  758. {
  759. import std.file : remove;
  760. enum kFilename = __PRETTY_FUNCTION__ ~ ".ini";
  761. auto writer = new ConfigParser();
  762. writer.addSection("general");
  763. writer.addSection("output");
  764. writer.set("general", "featureX", "true");
  765. writer.set("output", "featureX", "false");
  766. writer.write(kFilename);
  767. scope(exit) remove(kFilename);
  768. auto reader = new ConfigParser();
  769. reader.read(kFilename);
  770. assert(reader.hasSection("general"), "reader does not contain 'general' section");
  771. assert(reader.hasSection("output"), "reader does not contain 'output' section");
  772. assert(reader.getBool("general", "featureX") == true, "reader general.featureX is not true (bool)");
  773. assert(reader.getBool("output", "featureX") == false, "reader output.featureX is not false (bool)");
  774. }
  775. ///
  776. /// Write a representation of the configuration to the
  777. /// provided *buffer*.
  778. ///
  779. /// This representation can be parsed by future calls to
  780. /// `read`.
  781. ///
  782. /// Params:
  783. /// buffer = An OutputRange to write to (i.e. std.OutBuffer).
  784. /// spaceAroundDelimiters = The delimiters between keys and
  785. /// values are surrounded by spaces.
  786. ///
  787. /// Note: Comments from the original file are not preserved when
  788. /// writing the configuration back.
  789. ///
  790. void write(T)(T buffer, bool spaceAroundDelimiters = true) @safe pure const
  791. if (isOutputRange!(T, string))
  792. {
  793. import std.format : format;
  794. const del = spaceAroundDelimiters ? " = " : "=";
  795. foreach(const section, const options; m_sections)
  796. {
  797. buffer.put(format!"[%s]\n"(section));
  798. foreach(const option, const value; options)
  799. {
  800. buffer.put(format!"%s%s%s\n"(option, del, value));
  801. }
  802. }
  803. }
  804. ///
  805. @safe pure unittest
  806. {
  807. import std.outbuffer : OutBuffer;
  808. OutBuffer buffer = new OutBuffer();
  809. ConfigParser writer = new ConfigParser();
  810. writer.addSection("general");
  811. writer.addSection("output");
  812. writer.set("general", "featureX", "false");
  813. writer.set("output", "featureX", "true");
  814. writer.write(buffer);
  815. ConfigParser reader = new ConfigParser();
  816. reader.readString(buffer.toString());
  817. assert(reader.hasSection("general"), "reader does not contain 'general' section.");
  818. assert(reader.hasSection("output"), "reader does not contain 'output' section.");
  819. assert(reader.getBool("general", "featureX") == false);
  820. assert(reader.getBool("output", "featureX") == true);
  821. }
  822. private:
  823. void parseSectionHeader(const ref string line) @safe pure
  824. {
  825. import std.array : appender, assocArray;
  826. auto sectionHeader = appender!string;
  827. /* presume that the last character is ] */
  828. sectionHeader.reserve(line.length - 1);
  829. string popped = line[1 .. $];
  830. foreach(const c; popped)
  831. {
  832. if (c != ']')
  833. {
  834. sectionHeader.put(c);
  835. }
  836. else
  837. {
  838. break;
  839. }
  840. }
  841. m_currentSection = sectionHeader.data();
  842. if (m_currentSection in m_sections && m_strict)
  843. {
  844. throw new DuplicateSectionException(m_currentSection);
  845. }
  846. try
  847. {
  848. this.addSection(m_currentSection);
  849. }
  850. catch (DuplicateSectionException)
  851. {
  852. /* no-op - just making sure the section exists. */
  853. }
  854. }
  855. void parseLine(const ref string line) @safe pure
  856. {
  857. import std.string : indexOfAny, toLower, strip;
  858. const idx = line.indexOfAny(m_delimiters);
  859. if (-1 == idx)
  860. {
  861. return;
  862. }
  863. const option = line[0 .. idx].dup.strip.toLower;
  864. const string value = line[idx + 1 .. $].dup.strip;
  865. if (option in m_sections[m_currentSection] && m_strict)
  866. {
  867. throw new DuplicateOptionException(option, m_currentSection);
  868. }
  869. m_sections[m_currentSection][option] = value;
  870. }
  871. }