456 lines
11 KiB
D
456 lines
11 KiB
D
/*
|
|
* 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;
|
|
keys = new string[string];
|
|
}
|
|
|
|
/++
|
|
+ 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));
|
|
}
|