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")); -// }