diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b8027f7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## [0.2.0]() + +26.03.2023 + +### 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 diff --git a/README.md b/README.md index 58211ef..ca0752b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ 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 + ## Quick start The `settings.conf` file (see the [tests](tests/)): @@ -16,27 +23,36 @@ import std.stdio; void main() { - Config.file.read("./settings.conf"); + rc.read("./settings.conf"); - foreach (key, param; Config.file.keys()) + foreach (key, param; rc.sn.keys()) writefln("%s => %s", key, param); - writeln(Config.file.key("value1")); + writeln(rc.sn.key("value1")); + + foreach (key, param; rc.sn("part2").keys()) + writefln("%s => %s", key, param); + + writeln(rc.sn("part2").key("value1")); } ``` Result: ``` -value1 => This is the full value -value2 => Take the value in quotation marks -value3 => Or take in apostrophes -value4 => You can also comment -value5 => So you can also comment -value6 => "And you can even do that!" -value7 => 1234567890 -value8 => 12345.67890 -value9 => You can use large margins -value12 => //path -This is the full value +value1 => text without quotes +value2 => Yes! +value3 => value in apostrophes +value4 => 1000 +value5 => 0.000 +value7 => //path +value8 => "Hey!" +text without quotes +value1 => this value will be in the new section +value3 => good value! +this value will be in the new section ``` + +## Dub + +Add a dependency on `"readconf": "~>0.2.0"` diff --git a/img/matches.png b/img/matches.png index c3c3695..9ca06cd 100644 Binary files a/img/matches.png and b/img/matches.png differ diff --git a/source/readconf.d b/source/readconf.d index 42cdcf8..6899641 100644 --- a/source/readconf.d +++ b/source/readconf.d @@ -6,72 +6,53 @@ import std.regex; import std.meta; import singlog; +/** + * Read config object + */ +alias rc = Config.file; + class Config { private: + enum { + GROUP_PROPERTY = 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, // "[]" + } + static Config config; string path; - PP[string] properties; bool readed = false; + ConfigSection[string] sections; - /** - * Parameter and its value with the ability to convert to the desired data type - */ - struct PP - { - private string property; - private string value; - - /** - * Checking for the presence of a parameter - * Returns: true if the parameter is missing, otherwise false - */ - @property bool empty() - { - return this.property.length == 0; - } - - /** - * Get a string representation of the value - * Returns: default string value - */ - @property string toString() const - { - return value; - } - - alias toString this; - - auto opCast(T)() const - { - try { - return this.value.to!T; - } catch (Exception e) { - Log.msg.error("Cannot convert type"); - Log.msg.warning(e); - return T.init; - } - } - } + const string pattern = "^( |\\t)*(((\\w(\\w|-)+)(( |\\t)*(=>|=){1}" + ~ "( |\\t)*)(?!\\/(\\/|\\*))(([^ >\"'=\\n\\t#;].*?)|(\"(.+)\")" + ~ "|('(.+)')){1})|(\\[(\\w(\\w|-)+)\\])|(\\[\\]))( |\\t)*" + ~ "(( |\\t)(#|;|\\/\\/|\\/\\*).*)?$"; /** * Reading the configuration file */ - void readConfig() + bool readConfig() { File configuration; try { configuration = File(this.path, "r"); } catch (Exception e) { - Log.msg.error("Unable to open the configuration file " ~ this.path); - Log.msg.warning(e); - return; + Log.msg.warning("Unable to open the configuration file " ~ this.path); + Log.msg.error(e); + return false; } - string pattern = "^ *(\\w+)(( +=> +)|( += +))(?!\\/\\/)(([^ >\"'\\n#;].*?)|" - ~ "(\"(.+?)\")|('(.+?)')){1} *( #.*?)?( ;.*?)?( \\/\\/.*)?$"; - auto regular = regex(pattern, "m"); + auto regular = regex(this.pattern, "m"); + + // reading from the main section + string sectionName = "[]"; while (!configuration.eof()) { @@ -79,26 +60,42 @@ private: auto match = matchFirst(line, regular); if (match) { - int group = 5; - if (match[group].length) + // if again main section + if (match[GROUP_SECTION_MAIN].length) { - if (match[group][0] == '\'') - group = 10; - else if (match[group][0] == '\"') - group = 8; + sectionName = match[GROUP_SECTION_MAIN]; + continue; } - this.properties[match[1]] = PP(match[1], match[group]); - } + 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])); + } } try { configuration.close(); this.readed = true; } catch (Exception e) { - Log.msg.error("Unable to close the configuration file " ~ this.path); - Log.msg.warning(e); - return; + Log.msg.warning("Unable to close the configuration file " ~ this.path); + Log.msg.error(e); + this.readed = false; } + + return this.readed; } this() {} @@ -121,15 +118,46 @@ public: * Params: * path = the path to the configuration file */ - void read(string path) + bool read(string path) { this.path = path; if (!path.exists) - { - Log.msg.error("The configuration file does not exist: " ~ path); - return; - } - readConfig(); + throw new Exception("The configuration file does not exist: " ~ path); + return readConfig(); + } + + /** + * Get the section + * Params: + * section = section name (default main "[]") + */ + @property ConfigSection sectionName(string section = "[]") + { + return section in sections ? sections[section] : ConfigSection(); + } + + /** + * Section name + * + * Get the section + * Params: + * section = section name (default main "[]") + */ + alias sn = sectionName; +} + +struct ConfigSection +{ + private string name = "[]"; + private ConfigParameter[string] parameters; + + /** + * Checking for the presence of a partition + * Returns: true if the parameter is missing, otherwise false + */ + @property bool empty() + { + return this.parameters.length == 0; } /** @@ -138,20 +166,64 @@ public: * key = parameter from the configuration file * Returns: the value of the parameter in the PP structure view */ - PP key(string key) + ConfigParameter key(string key) { - if (this.readed) - return key in this.properties ? this.properties[key] : PP(); - Log.msg.warning("The configuration file was not read!"); - return PP(); + return key in this.parameters ? this.parameters[key] : ConfigParameter(); } /** * Get all keys and their values * Returns: collection of properties structures PP */ - PP[string] keys() + ConfigParameter[string] keys() { - return this.properties; + return this.parameters; + } + + private void add(ConfigParameter parameter) + { + if (parameter.property in parameters) + Log.msg.warning("The parameter exists but will be overwritten"); + this.parameters[parameter.property] = parameter; + } +} + +/** + * Parameter and its value with the ability to convert to the desired data type + */ +struct ConfigParameter +{ + private string property; + private string value; + + /** + * Checking for the presence of a parameter + * Returns: true if the parameter is missing, otherwise false + */ + @property bool empty() + { + return this.property.length == 0 || this.value.length == 0; + } + + /** + * Get a string representation of the value + * Returns: default string value + */ + @property string toString() const + { + return this.value; + } + + alias toString this; + + auto opCast(T)() const + { + try { + return this.value.to!T; + } catch (Exception e) { + Log.msg.warning("Cannot convert type"); + Log.msg.error(e); + return T.init; + } } } diff --git a/tests/settings.conf b/tests/settings.conf index 0e7ef7d..6d4fb28 100644 --- a/tests/settings.conf +++ b/tests/settings.conf @@ -1,13 +1,23 @@ Such a line will not be read -value1 = This is the full value -value2 = "Take the value in quotation marks" -value3 = 'Or take in apostrophes' -value4 => You can also comment // Another separator and comment -value5 => 'So you can also comment' # Yeah! -value6 => '"And you can even do that!"' ; He-he;) -value7 = 1234567890 # decimal value -value8 => 12345.67890 ; float value -value9 => You can use large margins -value10 = // But a line without a value will not be read -value11 = //path # not working -value12 = "//path" // nice way (or '//path') +The main section is set by default +value1=text without quotes +value2=>"value in quotation marks" +value3=>'value in apostrophes' ; and here is the first comment + value4 => 1000 // free space in spaces and tabs + value5 => 0.000 /* different form of commenting +value6 = // the string will not be read because the parameter value is missing +value7 = '' ; limiting empty characters will also not be taken into account +[part2] ; a new section! +value1 = this value will be in the new section +value 2 = 200 ; The parameter name is incorrect +value3 => good value! # and another way of commenting + +[] ; Make the main section again! +value7 => '//path' # and now the value7 will be read! +value2 = Yes! /* We will also redefine the value2 +value8 => '"Hey!"' # you can separate the values in quotation marks or apostrophes + +[part3] // and another section +value1=>-2 ; so you can +value2=3// but it will not work to leave a comment +value3=>100 //you need space from the value by at least 1 character diff --git a/tests/test.d b/tests/test.d index 280c692..60e75be 100644 --- a/tests/test.d +++ b/tests/test.d @@ -2,30 +2,33 @@ import readconf; unittest { - Config.file.read("./tests/settings.conf"); + rc.read("./tests/settings.conf"); - assert(Config.file.key("value1") == "This is the full value"); - assert(Config.file.key("value2") == "Take the value in quotation marks"); - assert(Config.file.key("value3") == "Or take in apostrophes"); - assert(Config.file.key("value4") == "You can also comment"); - assert(Config.file.key("value5") == "So you can also comment"); - assert(Config.file.key("value6") == "\"And you can even do that!\""); - assert(Config.file.key("value7") == "1234567890"); - assert(Config.file.key("value8") == "12345.67890"); - assert(Config.file.key("value9") == "You can use large margins"); - assert(Config.file.key("value10").empty); - assert(Config.file.key("value11").empty); - assert(Config.file.key("value12") == "//path"); + 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"); } - // void main() // { // import std.stdio; -// Config.file.read("./tests/settings.conf"); +// rc.read("./tests/settings.conf"); -// foreach (key, param; Config.file.keys()) +// foreach (key, param; rc.sn.keys()) // writefln("%s => %s", key, param); -// writeln(Config.file.key("value1")); +// writeln(rc.sn.key("value1")); + +// foreach (key, param; rc.sn("part2").keys()) +// writefln("%s => %s", key, param); + +// writeln(rc.sn("part2").key("value1")); // }