diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8027f7..85521cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,36 @@
# Changelog
-## [0.2.0]()
+## [v0.3.0](https://git.zhirov.kz/dlang/readconf/compare/v0.2.0...v0.3.0) (2023.03.30)
-26.03.2023
+### New
+
+- Read multiple configuration files
+- Quick access to a file/section/parameter using indexes
+
+### Other
+
+- Updated unittests
+- Added [examples](examples/) of configuration files
+- [Wiki](https://git.zhirov.kz/dlang/readconf/wiki) added
+
+## [v0.2.0](https://git.zhirov.kz/dlang/readconf/compare/v0.1.1...v0.2.0) (2023.03.26)
### New
- Adding sections support
- Taking into account spaces and tabs to separate the parameter, value and comment
- Added aliases to convenient parameter access to the value
+
+## [v0.1.1](https://git.zhirov.kz/dlang/readconf/compare/v0.1.0...v0.1.1) (2023.03.24)
+
+### Bug fixes
+
+- Checking empty keys
+- Reading an expression in quotation marks
+
+## [v0.1.0](https://git.zhirov.kz/dlang/readconf/commits/6409917cbe6a287db73fe3eea4bccaadf00379e7) (2023.03.23)
+
+### The first stable working release
+
+- The parameters are separated by `=` or `=>`
+- The ability to comment lines through delimiters `;`, `#`, `//`
diff --git a/README.md b/README.md
index ca0752b..3b9c369 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,25 @@
-# readconf
+
+
+
+
+readconf
+
+[![license](https://img.shields.io/github/license/AlexanderZhirov/readconf.svg?sort=semver&style=for-the-badge&color=green)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
+[![main](https://img.shields.io/badge/dynamic/json.svg?label=git.zhirov.kz&style=for-the-badge&url=https://git.zhirov.kz/api/v1/repos/dlang/readconf/tags&query=$[0].name&color=violet&logo=D)](https://git.zhirov.kz/dlang/readconf)
+[![githab](https://img.shields.io/github/v/tag/AlexanderZhirov/readconf.svg?sort=semver&style=for-the-badge&color=blue&label=github&logo=D)](https://github.com/AlexanderZhirov/readconf)
+[![dub](https://img.shields.io/dub/v/readconf.svg?sort=semver&style=for-the-badge&color=orange&logo=D)](https://code.dlang.org/packages/readconf)
+[![linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)](https://www.linux.org/)
Singleton for reading the configuration file required for your program.
## What can do
-1. Separation of parameter and value by separators `=` and `=>`
-2. Commenting on lines using special characters `;`, `#`, `//`, `/*`
-3. Support for sections for describing parameter blocks (sections are set by the name in `[]`)
-4. Support for spaces and tabs for visual separation
+- Reading multiple configuration files
+- Separation of parameters by sections
+- Access to parameters and sections using keys and indexes
+- Commenting on lines
+
+You will get more detailed information on the [wiki](https://git.zhirov.kz/dlang/readconf/wiki).
## Quick start
@@ -23,7 +35,7 @@ import std.stdio;
void main()
{
- rc.read("./settings.conf");
+ rc.read("./tests/settings.conf");
foreach (key, param; rc.sn.keys())
writefln("%s => %s", key, param);
@@ -33,7 +45,7 @@ void main()
foreach (key, param; rc.sn("part2").keys())
writefln("%s => %s", key, param);
- writeln(rc.sn("part2").key("value1"));
+ writeln(rc["part2"]["value1"]);
}
```
@@ -53,6 +65,20 @@ value3 => good value!
this value will be in the new section
```
-## Dub
+## Unittests
-Add a dependency on `"readconf": "~>0.2.0"`
+The unittests provide [examples](examples/) of configuration files and the `settings.conf` file located in the [tests](tests/):
+
+```sh
+Running bin/readconf-test-unittest
+ ✓ test __unittest_L4_C1
+ ✓ test __unittest_L106_C1
+ ✓ test __unittest_L25_C1
+ ✓ test __unittest_L51_C1
+
+Summary: 4 passed, 0 failed in 7 ms
+```
+
+## DUB
+
+Add a dependency on `"readconf": "~>0.3.0"`
diff --git a/dub.json b/dub.json
index 7f3fc7f..50b4a10 100644
--- a/dub.json
+++ b/dub.json
@@ -33,6 +33,6 @@
],
"targetName": "readconf",
"dependencies": {
- "singlog": "~>0.1.0"
+ "singlog": "~>0.2.1"
}
}
\ No newline at end of file
diff --git a/dub.selections.json b/dub.selections.json
index 377bd39..a98f018 100644
--- a/dub.selections.json
+++ b/dub.selections.json
@@ -3,6 +3,6 @@
"versions": {
"datefmt": "1.0.4",
"silly": "1.1.1",
- "singlog": "0.1.0"
+ "singlog": "0.2.1"
}
}
diff --git a/examples/comments.conf b/examples/comments.conf
new file mode 100644
index 0000000..0a9a588
--- /dev/null
+++ b/examples/comments.conf
@@ -0,0 +1,11 @@
+This line will be a comment, since it does not match the basic template
+parameter1 => value1 ; This will be a comment
+parameter2 => value2 # This will be a comment
+parameter3 => value3 // This will be a comment
+parameter4 => value4 /* This will be a comment
+parameter5 => value5;This will not be a comment
+parameter6 => value6// This will also be a whole value
+parameter7 => //value7 ;The value will not be read
+parameter8 => "//value8" # Now the value is correctly
+parameter9 => ';value9' // The value is correctly too
+parameter10 => '"value10"' // Quotes inside
\ No newline at end of file
diff --git a/examples/sections.conf b/examples/sections.conf
new file mode 100644
index 0000000..acaca5f
--- /dev/null
+++ b/examples/sections.conf
@@ -0,0 +1,22 @@
+parameter1 = value1
+parameter_2 = value2
+
+[first-section] ; Creating the first section
+parameter1 = value3 ; A section may contain similar parameter names
+parameter_2 = value4
+
+[second-section] // Another section
+parameter1 = value5
+parameter_2 = value6
+
+[] /* Addition of the main section
+parameter3 = value7 /* A new parameter will be added
+parameter1 = value8 /* And parameter1 will be redefined
+
+[first-section] # Addition of the first section
+parameter3 = value9
+parameter4 = value10
+
+[_section] # Creating the new section
+parameter1 = value11
+parameter2 = value12
\ No newline at end of file
diff --git a/examples/simple.conf b/examples/simple.conf
new file mode 100644
index 0000000..4700094
--- /dev/null
+++ b/examples/simple.conf
@@ -0,0 +1,16 @@
+parameter1=value1
+parameter2=>value2
+ parameter3=value3
+ parameter4=>value4
+
+_parameter5 = value5
+parameter6 => value6
+ parameter7 = value7
+
+ parameter8 => value8
+parameter9 =value9
+parameter-10 =>value10
+parameter11 = value11
+
+
+parameter12_ => value12
\ No newline at end of file
diff --git a/img/logo.png b/img/logo.png
new file mode 100644
index 0000000..c0351cb
Binary files /dev/null and b/img/logo.png differ
diff --git a/source/readconf.d b/source/readconf.d
index 6899641..33fc0a6 100644
--- a/source/readconf.d
+++ b/source/readconf.d
@@ -7,27 +7,32 @@ import std.meta;
import singlog;
/**
- * Read config object
+ * **Get an object to read the configuration file**
+ *
+ * - The `read()` will allow you to read the configuration file
+ * - `cf()` or `configFile()` will allow you to refer to the read file to get the parameters
*/
alias rc = Config.file;
+private const string mainSection = "[]";
+
class Config
{
private:
enum {
- GROUP_PROPERTY = 4,
+ GROUP_PARAMETER = 4,
GROUP_VALUE_1 = 11, // string
GROUP_VALUE_2 = 14, // "strin"
GROUP_VALUE_3 = 16, // 'string'
- GROUP_SECTION_OTHER_OUTER = 17, // "[string]"
- GROUP_SECTION_OTHER_INNER = 18, // "[string]"
- GROUP_SECTION_MAIN = 20, // "[]"
+ GROUP_SECTION_OTHER_OUTER = 17, // [string]
+ GROUP_SECTION_OTHER_INNER = 18, // string
+ GROUP_SECTION_MAIN = 20, // []
}
static Config config;
string path;
bool readed = false;
- ConfigSection[string] sections;
+ ConfigFile[string] configs;
const string pattern = "^( |\\t)*(((\\w(\\w|-)+)(( |\\t)*(=>|=){1}"
~ "( |\\t)*)(?!\\/(\\/|\\*))(([^ >\"'=\\n\\t#;].*?)|(\"(.+)\")"
@@ -37,61 +42,63 @@ private:
/**
* Reading the configuration file
*/
- bool readConfig()
+ bool readConfig(const string configName)
{
File configuration;
try {
configuration = File(this.path, "r");
} catch (Exception e) {
- Log.msg.warning("Unable to open the configuration file " ~ this.path);
- Log.msg.error(e);
+ log.w("Unable to open the configuration file " ~ this.path);
+ log.e(e);
return false;
}
+ if (configName !in this.configs)
+ this.configs[configName] = ConfigFile(configName);
+
auto regular = regex(this.pattern, "m");
// reading from the main section
- string sectionName = "[]";
+ string sectionName = mainSection;
while (!configuration.eof())
{
string line = configuration.readln();
auto match = matchFirst(line, regular);
- if (match)
+
+ if (match.length == 0)
+ continue;
+
+ // if again main section
+ if (match[GROUP_SECTION_MAIN].length)
{
- // if again main section
- if (match[GROUP_SECTION_MAIN].length)
- {
- sectionName = match[GROUP_SECTION_MAIN];
- continue;
- }
- if (match[GROUP_SECTION_OTHER_OUTER].length)
- {
- sectionName = match[GROUP_SECTION_OTHER_INNER];
- continue;
- }
-
- int group = GROUP_VALUE_1;
-
- if (match[group][0] == '\"')
- group = GROUP_VALUE_2;
- else if (match[group][0] == '\'')
- group = GROUP_VALUE_3;
-
- if (sectionName !in this.sections)
- this.sections[sectionName] = ConfigSection(sectionName);
-
- this.sections[sectionName].add(ConfigParameter(match[GROUP_PROPERTY], match[group]));
+ sectionName = match[GROUP_SECTION_MAIN];
+ continue;
}
+ if (match[GROUP_SECTION_OTHER_OUTER].length)
+ {
+ sectionName = match[GROUP_SECTION_OTHER_INNER];
+ continue;
+ }
+
+ int group = GROUP_VALUE_1;
+
+ if (match[group][0] == '\"')
+ group = GROUP_VALUE_2;
+ else if (match[group][0] == '\'')
+ group = GROUP_VALUE_3;
+
+ this.configs[configName].add(sectionName, ConfigParameter(match[GROUP_PARAMETER], match[group]));
}
try {
configuration.close();
this.readed = true;
} catch (Exception e) {
- Log.msg.warning("Unable to close the configuration file " ~ this.path);
- Log.msg.error(e);
+ log.w("Unable to close the configuration file " ~ this.path);
+ log.e(e);
+ this.configs.remove(configName);
this.readed = false;
}
@@ -117,38 +124,123 @@ public:
* Read the configuration file
* Params:
* path = the path to the configuration file
+ * configName = a specific name to bind to the configuration file (default file name)
+ * Returns: `true` if the file was read successfully
*/
- bool read(string path)
+ bool read(string path, string configName = "")
{
this.path = path;
if (!path.exists)
throw new Exception("The configuration file does not exist: " ~ path);
- return readConfig();
+ if (configName.length == 0)
+ configName = path.baseName();
+ if (configName in configs)
+ throw new Exception("The configuration file with this name has already been read");
+ return readConfig(configName);
+ }
+
+ /**
+ * Accessing the read configuration file
+ * Params:
+ * configName = specific name to bind to the configuration file
+ * (if the read files are > 1, then specify a specific name, otherwise default file name)
+ * Returns: configuration file object ConfigFile
+ */
+ @property ConfigFile configFile(string configName = "")
+ {
+ if (configName.length == 0)
+ {
+ if (configs.length == 1)
+ return configs[configs.byKey.front];
+ else
+ throw new Exception("You must explicitly specify the name of the configuration file");
+ }
+
+ return configName in configs ? configs[configName] : ConfigFile(configName);
+ }
+
+ /**
+ * Get the read configuration file
+ * Params:
+ * configName = specific name to bind to the configuration file
+ * (if the read files are > 1, then specify a specific name, otherwise default file name)
+ * Returns: configuration file object ConfigFile
+ */
+ alias cf = configFile;
+
+ @property ConfigFile opIndex(string configName = "")
+ {
+ if (configName.length == 0)
+ {
+ if (configs.length == 1)
+ return configs[configs.byKey.front];
+ else
+ throw new Exception("More than one configuration file has been read. "
+ ~ "It is necessary to specify the name of a specific");
+ }
+
+ return configName in configs ? configs[configName] : ConfigFile(configName);
+ }
+}
+
+struct ConfigFile
+{
+ private string name;
+ private ConfigSection[string] sections;
+
+ @property bool exist()
+ {
+ return this.sections.length > 0;
}
/**
* Get the section
* Params:
- * section = section name (default main "[]")
+ * section = section name (default main section)
+ * Returns: the object of the configuration file section ConfigSection
*/
- @property ConfigSection sectionName(string section = "[]")
+ @property ConfigSection sectionName(string section = mainSection)
{
- return section in sections ? sections[section] : ConfigSection();
+ if (!this.exist)
+ throw new Exception("The configuration file does not exist");
+ if (section == mainSection && sections.length == 1)
+ return sections[sections.byKey.front];
+ if (section.length == 0)
+ section = mainSection;
+ return section in sections ? sections[section] : ConfigSection(section);
}
/**
- * Section name
- *
* Get the section
* Params:
- * section = section name (default main "[]")
+ * section = section name (default main section)
+ * Returns: the object of the configuration file section ConfigSection
*/
alias sn = sectionName;
+
+ private void add(string sectionName, ConfigParameter parameter)
+ {
+ if (sectionName !in this.sections)
+ this.sections[sectionName] = ConfigSection(sectionName);
+
+ this.sections[sectionName].add(parameter);
+ }
+
+ @property ConfigSection opIndex(string section = mainSection)
+ {
+ if (!this.exist)
+ throw new Exception("The configuration file does not exist");
+ if (section == mainSection && sections.length == 1)
+ return sections[sections.byKey.front];
+ if (section.length == 0)
+ section = mainSection;
+ return section in sections ? sections[section] : ConfigSection(section);
+ }
}
struct ConfigSection
{
- private string name = "[]";
+ private string name = mainSection;
private ConfigParameter[string] parameters;
/**
@@ -164,16 +256,20 @@ struct ConfigSection
* Get the parameter value
* Params:
* key = parameter from the configuration file
- * Returns: the value of the parameter in the PP structure view
+ * Returns: the object of the parameter ConfigParameter
*/
ConfigParameter key(string key)
{
- return key in this.parameters ? this.parameters[key] : ConfigParameter();
+ if (key.length == 0)
+ throw new Exception("The key cannot be empty");
+ if (this.empty)
+ throw new Exception("The selected section has no parameters or does not exist");
+ return key in this.parameters ? this.parameters[key] : ConfigParameter(key);
}
/**
* Get all keys and their values
- * Returns: collection of properties structures PP
+ * Returns: collection of parameters
*/
ConfigParameter[string] keys()
{
@@ -183,14 +279,20 @@ struct ConfigSection
private void add(ConfigParameter parameter)
{
if (parameter.property in parameters)
- Log.msg.warning("The parameter exists but will be overwritten");
+ log.w("The parameter exists but will be overwritten");
this.parameters[parameter.property] = parameter;
}
+
+ ConfigParameter opIndex(string key)
+ {
+ if (key.length == 0)
+ throw new Exception("The key cannot be empty");
+ if (this.empty)
+ throw new Exception("The selected section has no parameters or does not exist");
+ return key in this.parameters ? this.parameters[key] : ConfigParameter(key);
+ }
}
-/**
- * Parameter and its value with the ability to convert to the desired data type
- */
struct ConfigParameter
{
private string property;
@@ -202,7 +304,7 @@ struct ConfigParameter
*/
@property bool empty()
{
- return this.property.length == 0 || this.value.length == 0;
+ return this.value.length == 0;
}
/**
@@ -221,8 +323,8 @@ struct ConfigParameter
try {
return this.value.to!T;
} catch (Exception e) {
- Log.msg.warning("Cannot convert type");
- Log.msg.error(e);
+ log.w("Cannot convert type");
+ log.e(e);
return T.init;
}
}
diff --git a/tests/test.d b/tests/test.d
index 60e75be..a12c32a 100644
--- a/tests/test.d
+++ b/tests/test.d
@@ -1,34 +1,121 @@
import readconf;
+import std.conv;
+
+unittest
+{
+ rc.read("./examples/simple.conf");
+
+ auto configFile = rc.cf;
+ auto mainSection = configFile.sn;
+
+ assert(mainSection.key("parameter1") == "value1");
+ assert(mainSection["parameter2"] == "value2");
+ assert(mainSection.key("parameter3") == "value3");
+ assert(mainSection["parameter4"] == "value4");
+ assert(mainSection.key("_parameter5") == "value5");
+ assert(mainSection["parameter6"] == "value6");
+ assert(mainSection.key("parameter7") == "value7");
+ assert(mainSection["parameter8"] == "value8");
+ assert(mainSection.key("parameter9") == "value9");
+ assert(mainSection["parameter-10"] == "value10");
+ assert(mainSection.key("parameter11") == "value11");
+ assert(mainSection["parameter12_"] == "value12");
+}
+
+unittest
+{
+ rc.read("./examples/sections.conf");
+ auto configFile = rc.cf;
+
+ auto mainSection = configFile.sn;
+ auto firstSection = configFile.sn("first-section");
+ auto secondSection = configFile["second-section"];
+ auto section = configFile["_section"];
+
+ assert(mainSection.key("parameter1") == "value8");
+ assert(mainSection["parameter_2"] == "value2");
+ assert(mainSection["parameter3"] == "value7");
+
+ assert(firstSection["parameter1"] == "value3");
+ assert(firstSection["parameter_2"] == "value4");
+ assert(firstSection["parameter3"] == "value9");
+ assert(firstSection["parameter4"] == "value10");
+
+ assert(secondSection["parameter1"] == "value5");
+ assert(secondSection["parameter_2"] == "value6");
+
+ assert(section["parameter1"] == "value11");
+ assert(section["parameter2"] == "value12");
+}
+
+unittest
+{
+ rc.read("./examples/simple.conf", "simple");
+ rc.read("./examples/sections.conf");
+ rc.read("./examples/comments.conf", "comments");
+
+ auto simpleConfig = rc.cf("simple");
+ auto sectionsConfig = rc["sections.conf"];
+ auto commentsConfig = rc["comments"];
+
+ auto simConMaiSec = simpleConfig.sn;
+
+ assert(simConMaiSec.key("parameter1") == "value1");
+ assert(simConMaiSec["parameter2"] == "value2");
+ assert(simConMaiSec.key("parameter3") == "value3");
+ assert(simConMaiSec["parameter4"] == "value4");
+ assert(simConMaiSec.key("_parameter5") == "value5");
+ assert(simConMaiSec["parameter6"] == "value6");
+ assert(simConMaiSec.key("parameter7") == "value7");
+ assert(simConMaiSec["parameter8"] == "value8");
+ assert(simConMaiSec.key("parameter9") == "value9");
+ assert(simConMaiSec["parameter-10"] == "value10");
+ assert(simConMaiSec.key("parameter11") == "value11");
+ assert(simConMaiSec["parameter12_"] == "value12");
+
+ auto secConMaiSec = sectionsConfig.sn;
+ auto secConFirSec = sectionsConfig.sn("first-section");
+ auto secConSecSec = sectionsConfig["second-section"];
+ auto secConSec = sectionsConfig["_section"];
+
+ assert(secConMaiSec.key("parameter1") == "value8");
+ assert(secConMaiSec["parameter_2"] == "value2");
+ assert(secConMaiSec["parameter3"] == "value7");
+ assert(secConFirSec["parameter1"] == "value3");
+ assert(secConFirSec["parameter_2"] == "value4");
+ assert(secConFirSec["parameter3"] == "value9");
+ assert(secConFirSec["parameter4"] == "value10");
+ assert(secConSecSec["parameter1"] == "value5");
+ assert(secConSecSec["parameter_2"] == "value6");
+ assert(secConSec["parameter1"] == "value11");
+ assert(secConSec["parameter2"] == "value12");
+
+ auto comConMaiSec = commentsConfig.sn;
+
+ assert(comConMaiSec["parameter1"] == "value1");
+ assert(comConMaiSec["parameter2"] == "value2");
+ assert(comConMaiSec["parameter3"] == "value3");
+ assert(comConMaiSec["parameter4"] == "value4");
+ assert(comConMaiSec["parameter5"] == "value5;This will not be a comment");
+ assert(comConMaiSec["parameter6"] == "value6// This will also be a whole value");
+ assert(comConMaiSec["parameter8"] == "//value8");
+ assert(comConMaiSec["parameter9"] == ";value9");
+ assert(comConMaiSec["parameter10"] == "\"value10\"");
+}
unittest
{
rc.read("./tests/settings.conf");
- assert(rc.sn.key("value1") == "text without quotes");
- assert(rc.sn.key("value2") == "Yes!");
- assert(rc.sn.key("value3") == "value in apostrophes");
- assert(rc.sn.key("value4") == "1000");
- assert(rc.sn.key("value5") == "0.000");
- assert(rc.sn.key("value7") == "//path");
- assert(rc.sn.key("value8") == "\"Hey!\"");
- assert(rc.sn("part2").key("value1") == "this value will be in the new section");
- assert(rc.sn("part2").key("value3") == "good value!");
- assert(rc.sn("part3").key("value1") == "-2");
- assert(rc.sn("part3").key("value3") == "100");
+ assert(rc.cf.sn.key("value1") == "text without quotes");
+ assert(rc[][]["value2"] == "Yes!");
+ assert(rc.cf.sn.key("value3") == "value in apostrophes");
+ assert(rc[][]["value4"] == "1000");
+ assert(rc.cf.sn["value5"] == "0.000");
+ assert(rc[][].key("value7") == "//path");
+ assert(rc.cf.sn.key("value8") == "\"Hey!\"");
+ assert(rc[]["part2"]["value1"] == "this value will be in the new section");
+ assert(rc.cf.sn("part2").key("value3") == "good value!");
+ assert(rc[].sn("part3").key("value1") == "-2");
+ assert(rc.cf["part3"]["value3"] == "100");
}
-
-// void main()
-// {
-// import std.stdio;
-// rc.read("./tests/settings.conf");
-
-// foreach (key, param; rc.sn.keys())
-// writefln("%s => %s", key, param);
-
-// writeln(rc.sn.key("value1"));
-
-// foreach (key, param; rc.sn("part2").keys())
-// writefln("%s => %s", key, param);
-
-// writeln(rc.sn("part2").key("value1"));
-// }