add initial.d
This commit is contained in:
commit
c8f3a9e1ae
1 changed files with 444 additions and 0 deletions
444
initial.d
Normal file
444
initial.d
Normal file
|
@ -0,0 +1,444 @@
|
|||
/*
|
||||
* 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)
|
||||
{
|
||||
this[defaultSection][k] = v;
|
||||
}
|
||||
|
||||
/++
|
||||
+ Sets the value of the `INISection` named *sect*.
|
||||
+/
|
||||
void
|
||||
opIndexAssign(INISection v, string sect)
|
||||
{
|
||||
this[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 the given key
|
||||
+ to the given type. On conversion failure throws an
|
||||
+ `INITypeException` with a message containing location
|
||||
+ information.
|
||||
+/
|
||||
T
|
||||
keyAs(T)(string k)
|
||||
{
|
||||
try
|
||||
return key(k).to!T();
|
||||
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 once a left square bracket character `[` is found at
|
||||
+ the beginning of a line, the line is considered a
|
||||
+ section heading)
|
||||
+ $(LI after the left square bracket character, 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 once 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 when whitespace is found in the first part of an assignment,
|
||||
+ it is ignored and the next equals sign is looked for
|
||||
+ immediately)
|
||||
+ $(LI when 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;
|
||||
|
||||
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) {
|
||||
/* 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));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue