/* * Copyright (c) 2024 Jeremy Baxter. All rights reserved. * * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or * organization obtaining a copy of the software and accompanying * documentation covered by this license (the "Software") to use, * reproduce, display, distribute, execute, and transmit the Software, * and to prepare derivative works of the Software, and to permit * third-parties to whom the Software is furnished to do so, all * subject to the following: * * The copyright notices in the Software and this entire statement, * including the above license grant, this restriction and the following * disclaimer, must be included in all copies of the Software, in whole or * in part, and all derivative works of the Software, unless such copies or * derivative works are solely in the form of machine-executable object * code generated by a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND * NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE * DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, * WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /++ + INI is a simple, concise configuration or data interchange + format. It consists of keys and values listed inside a section. + An INI document might look something like this: + --- + [user] + name = Kevin + age = 26 + emailAddress = kevin@example.net + + [config] + useBundledMalloc = false + forceDefaultInit = true + --- + + $(B initial) is a library for parsing INI documents into an `INIUnit`, + a data structure representing an INI document. The function `readINI` + contains the parser; it parses the INI given to it and deserialises + it into the given `INIUnit`. The `readINIFile` function can be used + to read the INI from a file rather than a string. + + $(B initial) is fully @safe, and can be used in @safe code. + + License: [Boost License 1.0](https://www.boost.org/LICENSE_1_0.txt). + Authors: Jeremy Baxter +/ module initial; import std.conv : to, ConvException; import std.exception : basicExceptionCtors, enforce; import std.format : format; import std.traits : isSomeChar; private alias parserEnforce = enforce!INIParseException; @safe: /++ + Structure that represents an INI file. + + Contains `INISection`s in an associative array that can be + directly accessed if needs be, or the index operator can be + used to get an `INISection` and automatically initialise it + if needed. + + The `serialise` method can be used to serialise the entire + structure into an INI document that can be then read again + by the parser and modified by humans. + + Example: + --- + INIUnit ini; + + /* write INI data */ + ini["section"]["key"] = "value"; + ini["section"]["num"] = "4.8"; + + /* read INI data */ + readINI(ini, ` + [section] + num = 5.3 + `); + + /* read INI from file */ + readINIFile(ini, "config.ini"); + + /* write INI to file */ + import std.file : write; + write("config.ini", ini.serialise()); + --- +/ struct INIUnit { INISection[string] sections; /++ Hashmap of INISections in this unit. +/ string defaultSection = "main"; /++ Name of the default section. +/ nothrow: /++ + Returns the value of *k* in the default section + or *defaultValue* if *k* is not present. +/ string key(string k, string defaultValue = null) { return this[defaultSection].key(k, defaultValue); } /++ + Serialises this `INIUnit` into an INI document. + + If *defaultSectionHeading* is true, includes the + section heading of the default section. + Example: + --- + INIUnit ini; + + ini["sections"] = "ages suburbs"; + ini["ages"]["John"] = "37"; + ini["ages"]["Mary"] = "29"; + ini["suburbs"]["John"] = "Gordonton"; + ini["suburbs"]["Mary"] = "Ashmore"; + + writeln(ini.serialise()); + /* + * sections = ages suburbs + * [ages] + * John = 37 + * Mary = 29 + * [suburbs] + * John = Gordonton + * Mary = Ashmore + */ + --- +/ char[] serialise(bool defaultSectionHeading = false) { char[] buf; buf = this[defaultSection].serialise(defaultSectionHeading); foreach (string sect; sections.byKey()) { if (sect == defaultSection) continue; buf ~= this[sect].serialise(); } return buf; } /++ + Returns the `INISection` associated with the name *sect*. + Initialises it if it doesn't exist. +/ ref INISection opIndex(string sect) { if (!(sect in sections)) sections[sect] = INISection(sect); return sections[sect]; } /++ + Sets the value of the key *k* in the default section to *v*. + Example: + --- + ini["key"] = "value"; + --- +/ void opIndexAssign(string v, string k) { sections[defaultSection][k] = v; } /++ + Sets the value of the `INISection` named *sect*. +/ void opIndexAssign(INISection v, string sect) { sections[sect] = v; } } /++ + Represents an INI section. + + Holds keys in the form of an associative array. + Index the `keys` property to get this data raw, or to avoid + range errors, use the `key` or `keyAs` functions. + + `alias this` is applied to the `keys` associative array, + meaning you can index the structure to get or set keys' values. +/ struct INISection { string[string] keys; /++ Hashmap of keys belonging to this section. +/ string name; /++ Name of this section. +/ /++ + Construct a new `INISection` with the given name. + Called by `readINI` and `INIUnit.opIndex`. +/ this(string name) nothrow { this.name = name; } /++ + Returns the value of `k` in this section or + `defaultValue` if the key is not present. +/ string key(string k, string defaultValue = null) nothrow { return k in keys ? keys[k] : defaultValue; } /++ + Using `std.conv`, converts the value of *k* to *T*. + On conversion failure throws an `INITypeException` + with a message containing location information. +/ T keyAs(T)(string k, T defaultValue) { try return k in keys ? keys[k].to!T() : defaultValue; catch (ConvException) throw new INITypeException( format!"unable to convert [%s].%s to %s"(name, k, T.stringof)); } /++ + Serialise this section into an INI document. + + If `sectionHeading` is true, includes the section heading at the top. +/ char[] serialise(bool sectionHeading = true) nothrow { char[] buf; if (sectionHeading) buf ~= "[" ~ name.to!(char[]) ~ "]\n"; foreach (string key; keys.byKey()) { buf ~= key ~ " = " ~ keys[key] ~ "\n"; } return buf; } alias keys this; } /++ + Exception thrown when parsing failure occurs. +/ class INIParseException : Exception { mixin basicExceptionCtors; } /++ + Exception thrown when conversion failure occurs. +/ class INITypeException : Exception { mixin basicExceptionCtors; } private string locate(size_t l, size_t c) { return format!"%d:%d"(l, c); } /++ + Parse the given INI data into an `INIUnit`. + + To parse a file's contents, use readINIFile instead. + + Whitespace is first stripped from the beginning and end of + the line before parsing it. + + The following rules apply when parsing: + $(UL + $(LI if a hash character `#` is found at the beginning of a line, + the rest of the line is ignored) + $(LI if a left square bracket character `[` is found at + the beginning of a line, the line is considered a + section heading) + $(LI inside a section heading, any character can be used + inside the section heading except the right square bracket + character) + $(LI once the right square bracket character `]` is found, + the section heading is considered finished. Any trailing + characters are garbage and will trigger a parse error.) + $(LI if a character except the left square bracket is found + at the beginning of a line, the line is considered an + assignment in the current section) + $(LI once an equals character `=` is found after the first + part of an assignment, the rest of the line is considered + the "value" while the first part is the "key") + $(LI if whitespace is found in the first part of an assignment, + it is ignored and the next equals sign is looked for + immediately) + $(LI if whitespace is found after an equals character `=` + in an assignment, it is ignored until the first + non-whitespace character is found) + $(LI once a non-whitespace character is found after an equals + character `=` in an assignment, it is counted part of the + "value") +/ void readINI(ref INIUnit ini, string data) { import std.string : splitLines, strip; string section; /* name of current section */ string text; /* current key, value, section, whatever */ string key; /* name of key in section */ string value; /* value */ bool inAssign; /* in key assignment? */ bool inHeading; /* in section heading? */ bool needEqls; /* require equals sign immediately? */ bool pastEqls; /* past equals sign in assignment? */ bool trailing; /* trailing garbage after heading? */ section = ini.defaultSection; nextline: foreach (size_t ln, string line; data.splitLines()) { line = line.strip(); if (line.length == 0) continue; inAssign = inHeading = needEqls = pastEqls = trailing = false; text = key = value = ""; foreach (size_t cn, char ch; line) { switch (ch) { /* comment? */ case '#': if (cn == 0) continue nextline; text ~= ch; break; /* beginning of a section heading? */ case '[': if (cn == 0) /* beginning of line? */ inHeading = true; else text ~= ch; break; /* end of a section heading? */ case ']': parserEnforce(inHeading || inAssign, locate(ln, cn) ~ ": unexpected character ']'"); if (inHeading) { section = text; text = ""; inHeading = false; trailing = true; } else if (inAssign) { text ~= ch; } break; /* middle of an assignment? */ case '=': if (inAssign) { if (pastEqls) { /* count it as part of the value */ text ~= ch; } else { key = text; text = ""; pastEqls = true; needEqls = false; } } else { goto default; } break; /* whitespace */ case ' ': case '\t': case '\v': case '\r': case '\n': case '\f': if (inAssign) { if (!pastEqls) needEqls = true; } break; default: if (cn == 0) /* beginning of line? */ inAssign = true; parserEnforce((inAssign || inHeading) && !trailing, locate(ln, cn) ~ ": unexpected character '" ~ ch ~ "'"); if (inAssign) parserEnforce(!needEqls, locate(ln, cn) ~ ": expected '=', not '" ~ ch ~ "'"); text ~= ch; break; } } /* once the line has finished being looped over... */ if (inAssign) { parserEnforce(key.length != 0, locate(ln, line.length - 1) ~ ": key cannot be empty"); value = text; text = ""; if (!(section in ini.sections)) ini[section] = INISection(section); ini[section][key] = value; } } } /++ + Parse INI from a file located at `fileName`. +/ void readINIFile(ref INIUnit ini, string fileName) { import std.file : readText; readINI(ini, readText(fileName)); }