├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── source └── inifiled.d └── test ├── dscanner.ini └── filename.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | Test: 13 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - windows-latest 21 | - ubuntu-latest 22 | - macOS-latest 23 | compiler: 24 | - 'dmd-latest' 25 | - 'ldc-latest' 26 | - 'dmd-beta' 27 | - 'ldc-beta' # the tests crash 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Install compiler 34 | uses: dlang-community/setup-dlang@v1 35 | with: 36 | compiler: ${{ matrix.compiler }} 37 | 38 | - name: Test 39 | run: | 40 | dub test 41 | 42 | Skip: 43 | if: "contains(github.event.head_commit.message, '[skip ci]')" 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Skip CI 🚫 47 | run: echo skip CI 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Libraries 8 | *.lib 9 | *.a 10 | 11 | # Shared objects (inc. Windows DLLs) 12 | *.dll 13 | *.so 14 | *.so.* 15 | *.dylib 16 | 17 | # Executables 18 | *.exe 19 | *.out 20 | *.app 21 | *.i*86 22 | *.x86_64 23 | *.hex 24 | 25 | # DUB 26 | .dub 27 | inifiled-test-library 28 | test/filenameTmp.ini 29 | test/bikeShed.ini 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | 4 | d: 5 | - dmd-nightly 6 | - dmd-beta 7 | - dmd 8 | - ldc-beta 9 | - ldc 10 | - dmd-2.072.2 11 | - dmd-2.073.2 12 | - dmd-2.074.1 13 | 14 | script: 15 | - dub test --compiler=${DC} 16 | 17 | notifications: 18 | email: false 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | inifile-D 2 | ========= 3 | 4 | A compile time ini file parser and writter generator for D. 5 | inifile.d takes annotated structs and create ini file parser and writer. 6 | The ini file format always comments and section and to some degree nesting. 7 | 8 | Example 9 | ------- 10 | 11 | ```d 12 | import initest; 13 | 14 | import inifiled; 15 | 16 | @INI("A child must have a parent") 17 | struct Child { 18 | @INI("The firstname of the child") string firstname; 19 | 20 | @INI("The age of the child") int age; 21 | } 22 | 23 | @INI("A Spouse") 24 | struct Spouse { 25 | @INI("The age of the spouse") int age; 26 | 27 | // Nesting 28 | @INI("The House of the spouse") House house; 29 | } 30 | 31 | @INI("A Dog") 32 | struct Dog { 33 | @INI("The name of the Dog") string name; 34 | } 35 | 36 | @INI("A Person") 37 | struct Person { 38 | @INI("The lastname of the Person") string lastname; 39 | 40 | @INI("The height of the Person") float height; 41 | 42 | @INI("Some strings with a very long long INI description that is longer" ~ 43 | " than eigthy lines hopefully." 44 | ) string[] someStrings; 45 | 46 | int dontShowThis; 47 | 48 | // REGARD the nesting 49 | @INI("A Spouse") Spouse spouse; 50 | 51 | // REGARD the nesting 52 | @INI("The family dog") Dog dog; 53 | } 54 | 55 | @INI("A House") 56 | struct House { 57 | @INI("Number of Rooms") uint rooms; 58 | 59 | @INI("Number of Floors") uint floors; 60 | } 61 | ``` 62 | 63 | ```d 64 | import initest; 65 | import inifiled; 66 | 67 | import std.string; 68 | 69 | void main() { 70 | Person p; 71 | p.lastname = "Mike"; 72 | p.height = 181.7; 73 | 74 | p.someStrings ~= "Hello"; 75 | p.someStrings ~= "World"; 76 | 77 | p.spouse.firstname = "Molly"; 78 | p.spouse.age = 72; 79 | 80 | p.spouse.house.rooms = 5; 81 | p.spouse.house.floors = 2; 82 | 83 | p.dog.name = "Wuff"; 84 | 85 | // Here the ini file is written 86 | writeINIFile(p, "filename.ini"); 87 | 88 | // Here the ini file is read 89 | Person p2; 90 | readINIFile(p2, "filename.ini"); 91 | 92 | // They should be the same 93 | assert(p == p2, format("%s\n%s", p, p2)); 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inifiled", 3 | "description": "Compile Time ini file Reader and Writer Generator", 4 | "homepage": "https://github.com/burner/inifile-D", 5 | "copyright": "Copyright © 2014, burner", 6 | "authors": ["Robert burner Schadek"], 7 | "license": "LGPL3", 8 | "importPaths": ["source"], 9 | "sourcePaths": ["source"], 10 | "configurations": [ 11 | { 12 | "name": "library-quiet", 13 | "targetType": "library" 14 | }, 15 | { 16 | "name": "library-verbose", 17 | "targetType": "library", 18 | "versions": ["debugLogs"] 19 | }, 20 | { 21 | "name": "unittest", 22 | "targetType": "library", 23 | "versions": ["debugLogs"] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /source/inifiled.d: -------------------------------------------------------------------------------- 1 | module inifiled; 2 | 3 | import std.conv : to; 4 | import std.format : format, formattedWrite; 5 | import std.math : isClose, isNaN; 6 | import std.range : isInputRange, isOutputRange, ElementType; 7 | import std.string : indexOf, lastIndexOf, split, strip, stripRight; 8 | import std.traits : getUDAs, hasUDA, fullyQualifiedName, isArray, isBasicType 9 | , isSomeString, isArray; 10 | 11 | string genINIparser(T)() { 12 | return ""; 13 | } 14 | 15 | struct INI { 16 | @safe: 17 | string msg; 18 | string name; 19 | 20 | static INI opCall(string s, string name = null) pure { 21 | INI ret; 22 | ret.msg = s; 23 | ret.name = name; 24 | 25 | return ret; 26 | } 27 | } 28 | 29 | private INI getINI(T)() pure @trusted { 30 | foreach(it; __traits(getAttributes, T)) { 31 | static if(is(it == INI)) { 32 | return INI(null, null); 33 | } 34 | static if(is(typeof(it) == INI)) { 35 | return it; 36 | } 37 | } 38 | assert(false); 39 | } 40 | 41 | private INI getINI(T, string mem)() @trusted { 42 | foreach(it; __traits(getAttributes, __traits(getMember, T, mem))) { 43 | static if(is(it == INI)) { 44 | return INI(null, null); 45 | } 46 | static if(is(typeof(it) == INI)) { 47 | return it; 48 | } 49 | } 50 | assert(false, mem); 51 | } 52 | 53 | private pure string getTypeName(T)() @trusted { 54 | return fullyQualifiedName!T; 55 | } 56 | 57 | void readINIFile(T)(ref T t, string filename) { 58 | import std.stdio : File; 59 | auto iFile = File(filename, "r"); 60 | auto iRange = iFile.byLine(); 61 | readINIFileImpl(t, iRange); 62 | } 63 | 64 | private pure bool isSection(T)(T line) @safe nothrow { 65 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 66 | 67 | bool f; 68 | bool b; 69 | 70 | foreach(it; line) { 71 | if(it == ' ' || it == '\t') { 72 | continue; 73 | } else if(it == '[') { 74 | f = true; 75 | break; 76 | } else { 77 | break; 78 | } 79 | } 80 | 81 | foreach_reverse(it; line) { 82 | if(it == ' ' || it == '\t') { 83 | continue; 84 | } else if(it == ']') { 85 | b = true; 86 | break; 87 | } else { 88 | break; 89 | } 90 | } 91 | 92 | return f && b; 93 | } 94 | 95 | @safe pure unittest { 96 | assert(isSection("[initest.Person]")); 97 | assert(isSection(" [initest.Person]")); 98 | assert(isSection(" [initest.Person] ")); 99 | assert(!isSection(";[initest.Person] ")); 100 | } 101 | 102 | private pure string getSection(T)(T line) @safe { 103 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 104 | return getTimpl!('[',']')(line); 105 | } 106 | 107 | private pure string getValue(T)(T line) @safe { 108 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 109 | return getTimpl!('"','"')(line); 110 | } 111 | 112 | private pure string getValueArray(T)(T line) @safe { 113 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 114 | return getTimpl!('"','"')(line); 115 | } 116 | 117 | @safe pure unittest { 118 | assert(getValue("firstname=\"Foo\"") == "Foo"); 119 | assert(getValue("firstname=\"Foo\",\"Bar\"") == "Foo\",\"Bar"); 120 | } 121 | 122 | private pure string getKey(T)(T line) @safe { 123 | import std.exception : enforce; 124 | 125 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 126 | 127 | ptrdiff_t eq = line.indexOf('='); 128 | enforce(eq != -1, "key value pair needs equal sign"); 129 | 130 | return line[0 .. eq].strip(); 131 | } 132 | 133 | @safe pure unittest { 134 | assert(getKey("firstname=\"Foo\"") == "firstname"); 135 | assert(getKey("lastname =\"Foo\",\"Bar\"") == "lastname"); 136 | } 137 | 138 | private pure string getTimpl(char l, char r, T)(T line) @safe { 139 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 140 | 141 | ptrdiff_t l = line.indexOf(l); 142 | ptrdiff_t r = line.lastIndexOf(r); 143 | 144 | assert(l+1 < line.length, format("l+1 %u line %u", l+1, line.length)); 145 | return line[l+1 .. r].idup; 146 | } 147 | 148 | private pure bool isKeyValue(T)(T line) @safe { 149 | static assert(isInputRange!T, T.stringof ~ " is not an InputRange"); 150 | 151 | ptrdiff_t idx = line.indexOf('='); 152 | return idx != -1; 153 | } 154 | 155 | @safe pure unittest { 156 | assert(getSection("[initest.Person]") == "initest.Person", 157 | getSection("[initest.Person]")); 158 | assert(getSection(" [initest.Person]") == "initest.Person", 159 | getSection("[initest.Person]")); 160 | assert(getSection(" [initest.Person] ") == "initest.Person", 161 | getSection("[initest.Person]")); 162 | assert(getSection("[initest.Person] ") == "initest.Person", 163 | getSection("[initest.Person]")); 164 | 165 | assert(getValue("\"initest.Person\"") == "initest.Person", 166 | getValue("\"initest.Person\"")); 167 | assert(getValue(" \"initest.Person\"") == "initest.Person", 168 | getValue("\"initest.Person\"")); 169 | assert(getValue(" \"initest.Person\" ") == "initest.Person", 170 | getValue("\"initest.Person\"")); 171 | assert(getValue("\"initest.Person\" ") == "initest.Person", 172 | getValue("\"initest.Person\"")); 173 | } 174 | 175 | private string buildSectionParse(T)() @safe { 176 | import std.array : join; 177 | string[] ret; 178 | 179 | foreach(it; __traits(allMembers, T)) { 180 | if(hasUDA!(__traits(getMember, T, it), INI) 181 | && !isBasicType!(typeof(__traits(getMember, T, it))) 182 | && !isSomeString!(typeof(__traits(getMember, T, it))) 183 | && !isArray!(typeof(__traits(getMember, T, it)))) 184 | { 185 | alias MemberType = typeof(__traits(getMember, T, it)); 186 | static if(__traits(compiles, getINI!(MemberType))) { 187 | const name = getINI!(MemberType).name is null 188 | ? fullyQualifiedName!(typeof(__traits(getMember, T, it))) 189 | : getINI!(MemberType).name; 190 | } else { 191 | const name = fullyQualifiedName!(typeof(__traits(getMember, T, it))); 192 | } 193 | ret ~= ("case \"%s\": { line = readINIFileImpl" ~ 194 | "(t.%s, input, depth+1); } ").format(name,it); 195 | } 196 | } 197 | 198 | // Avoid DMD switch fallthrough warnings 199 | if(ret.length) { 200 | return "switch(getSection(line)) { // " ~ fullyQualifiedName!T ~ "\n" ~ 201 | ret.join("goto case; \n") ~ "goto default;\n default: return line;\n}\n"; 202 | } else { 203 | return "return line;"; 204 | } 205 | } 206 | 207 | private string buildValueParse(T)() @safe { 208 | string ret = "switch(getKey(line)) { // " ~ fullyQualifiedName!T ~ "\n"; 209 | 210 | foreach(it; __traits(allMembers, T)) { 211 | if(hasUDA!(__traits(getMember, T, it), INI) && (isBasicType!(typeof(__traits(getMember, T, it))) 212 | || isSomeString!(typeof(__traits(getMember, T, it))))) 213 | { 214 | const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name; 215 | ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))(" 216 | ~ "getValue(line)); break; }\n").format(name, it, it); 217 | } else if(hasUDA!(__traits(getMember, T, it), INI) 218 | && isArray!(typeof(__traits(getMember, T, it)))) 219 | { 220 | const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name; 221 | ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))(" 222 | ~ "getValueArray(line).split(',')); break; }\n").format(name, it, it); 223 | } 224 | } 225 | 226 | return ret ~ "default: break;\n}\n"; 227 | } 228 | 229 | private string readINIFileImpl(T,IRange)(ref T t, ref IRange input, int depth = 0) 230 | { 231 | static assert(isInputRange!IRange, IRange.stringof ~ " is not an InputRange"); 232 | 233 | import std.algorithm.searching : endsWith, startsWith; 234 | 235 | debug version(debugLogs) { 236 | import std.stdio : writefln; 237 | } 238 | debug version(debugLogs) { 239 | writefln("%*s%d %s %x", depth, "", __LINE__, fullyQualifiedName!(typeof(t)), 240 | cast(void*)&input); 241 | } 242 | string line; 243 | bool isMultiLine; 244 | while(!input.empty()) { 245 | immutable bool wasMultiLine = isMultiLine; 246 | auto currentLine = input.front.stripRight; 247 | isMultiLine = currentLine.endsWith(`\`); 248 | // remove backslash if existent 249 | if(isMultiLine) { 250 | currentLine = currentLine[0 .. $ - 1]; 251 | } 252 | 253 | if(wasMultiLine) { 254 | line ~= currentLine; 255 | } else { 256 | line = currentLine.idup; 257 | } 258 | 259 | input.popFront(); 260 | 261 | if(line.startsWith(";") || isMultiLine) { 262 | continue; 263 | } 264 | debug version(debugLogs) { 265 | writefln("%*s%d %s %s %b", depth, "", __LINE__, line, fullyQualifiedName!T, 266 | isSection(line)); 267 | } 268 | 269 | static if(hasUDA!(T, INI)) { 270 | const name = getINI!T().name is null ? fullyQualifiedName!T : getINI!T().name; 271 | } else { 272 | const name = fullyQualifiedName!T; 273 | } 274 | if(isSection(line) && getSection(line) != name) { 275 | debug version(debugLogs) { 276 | pragma(msg, buildSectionParse!(T)); 277 | writefln("%*s%d %s", depth, "", __LINE__, getSection(line)); 278 | writefln("%*s%d %x", depth, "", __LINE__, 279 | cast(void*)&input); 280 | } 281 | 282 | mixin(buildSectionParse!(T)); 283 | } else if(isKeyValue(line)) { 284 | debug version(debugLogs) { 285 | pragma(msg, buildValueParse!(T)); 286 | writefln("%*s%d %s %s", depth, "", __LINE__, getKey(line), 287 | getValue(line)); 288 | } 289 | 290 | mixin(buildValueParse!(T)); 291 | } 292 | } 293 | 294 | return line; 295 | } 296 | 297 | private void writeComment(ORange,IRange)(ORange orange, IRange irange) @trusted { 298 | static assert(isOutputRange!(ORange, ElementType!IRange) 299 | , ORange.stringf ~ " is not and OutputRange for " 300 | ~ ElementType!(IRange).stringof); 301 | static assert(isInputRange!IRange, IRange.stringof 302 | ~ " is not an InputRange"); 303 | size_t idx = 0; 304 | foreach(it; irange) { 305 | if(idx % 77 == 0) { 306 | orange.put("; "); 307 | } 308 | orange.put(it); 309 | 310 | if((idx+1) % 77 == 0) { 311 | orange.put('\n'); 312 | } 313 | 314 | ++idx; 315 | } 316 | orange.put('\n'); 317 | } 318 | 319 | private void writeValue(ORange,T)(ORange orange, string name, T value) @trusted { 320 | static assert(isOutputRange!(ORange, string) 321 | , ORange.stringf ~ " is not and OutputRange for strings"); 322 | static if(isArray!T && !isSomeString!T) { 323 | orange.formattedWrite("%s=\"", name); 324 | foreach(idx, it; value) { 325 | if(idx != 0) { 326 | orange.put(','); 327 | } 328 | orange.formattedWrite("%s", it); 329 | } 330 | orange.formattedWrite("\""); 331 | } else { 332 | orange.formattedWrite("%s=\"%s\"\n", name, value); 333 | } 334 | } 335 | 336 | private string removeFromLastPoint(string input) pure @safe { 337 | ptrdiff_t lDot = input.lastIndexOf('.'); 338 | return lDot != -1 && lDot+1 != input.length 339 | ? input[lDot+1 .. $] 340 | : input; 341 | } 342 | 343 | private void writeValues(ORange,T)(ORange oRange, string name, T value) @trusted { 344 | static assert(isOutputRange!(ORange, string) 345 | , ORange.stringf ~ " is not and OutputRange for strings"); 346 | static if(isSomeString!(ElementType!T) || isBasicType!(ElementType!T)) { 347 | oRange.formattedWrite("%s=\"", removeFromLastPoint(name)); 348 | foreach(idx, it; value) { 349 | if(idx != 0) { 350 | oRange.put(','); 351 | } 352 | oRange.formattedWrite("%s", it); 353 | } 354 | oRange.put('"'); 355 | oRange.put('\n'); 356 | } else { 357 | for(size_t i = 0; i < value.length; ++i) { 358 | oRange.formattedWrite("[%s]\n", name); 359 | writeINIFileImpl(value[i], oRange, false); 360 | } 361 | } 362 | } 363 | 364 | void writeINIFile(T)(ref T t, string filename) @trusted { 365 | import std.stdio : File; 366 | auto oFile = File(filename, "w"); 367 | auto oRange = oFile.lockingTextWriter(); 368 | writeINIFileImpl(t, oRange, true); 369 | } 370 | 371 | void writeINIFileImpl(T,ORange)(ref T t, ORange oRange, bool section) 372 | @trusted 373 | { 374 | if(hasUDA!(T, INI) && section) { 375 | writeComment(oRange, getINI!T().msg); 376 | } 377 | 378 | if(section) { 379 | if(hasUDA!(T, INI)) { 380 | auto ini = getINI!(T); 381 | if(ini.name !is null) { 382 | oRange.formattedWrite("[%s]\n", ini.name); 383 | } else { 384 | oRange.formattedWrite("[%s]\n", getTypeName!T); 385 | } 386 | } else { 387 | oRange.formattedWrite("[%s]\n", getTypeName!T); 388 | } 389 | } 390 | 391 | foreach(it; __traits(allMembers, T)) { 392 | if(hasUDA!(__traits(getMember, T, it), INI)) { 393 | static if(isBasicType!(typeof(__traits(getMember, T, it))) || 394 | isSomeString!(typeof(__traits(getMember, T, it)))) 395 | { 396 | const ini = getINI!(T,it); 397 | const name = ini.name is null ? it : ini.name; 398 | writeComment(oRange, ini.msg); 399 | writeValue(oRange, name, __traits(getMember, t, it)); 400 | } else static if(isArray!(typeof(__traits(getMember, T, it)))) { 401 | const ini = getINI!(T,it); 402 | const name = getTypeName!T ~ "." ~ (ini.name is null ? it : ini.name); 403 | writeComment(oRange, ini.msg); 404 | writeValues(oRange, name, __traits(getMember, t, it)); 405 | } else static if(hasUDA!(__traits(getMember, t, it),INI)) { 406 | writeINIFileImpl(__traits(getMember, t, it), oRange, true); 407 | } 408 | } 409 | } 410 | } 411 | 412 | version(unittest) { 413 | @INI("A child must have a parent") 414 | struct Child { 415 | @INI("The firstname of the child") 416 | string firstname; 417 | 418 | @INI("The age of the child") 419 | int age; 420 | 421 | bool opEquals(Child other) { 422 | return this.firstname == other.firstname 423 | && this.age == other.age; 424 | } 425 | } 426 | 427 | @INI("A Spouse") 428 | struct Spouse { 429 | @INI("The firstname of the spouse") 430 | string firstname; 431 | 432 | @INI("The age of the spouse") 433 | int age; 434 | 435 | @INI("The House of the spouse") 436 | House house; 437 | 438 | bool opEquals(Spouse other) { 439 | return this.firstname == other.firstname 440 | && this.age == other.age 441 | && this.house == other.house; 442 | } 443 | } 444 | 445 | @INI("A Dog") 446 | struct Dog { 447 | @INI("The name of the Dog") 448 | string name; 449 | 450 | @INI("The food consumed") 451 | float kg; 452 | 453 | bool opEquals(Dog other) { 454 | return this.name == other.name 455 | && (isClose(this.kg, other.kg) 456 | || (isNaN(this.kg) && isNaN(other.kg)) 457 | ); 458 | } 459 | } 460 | 461 | @INI("A Person") 462 | struct Person { 463 | @INI("The firstname of the Person") 464 | string firstname; 465 | 466 | @INI("The lastname of the Person") 467 | string lastname; 468 | 469 | @INI("The age of the Person") 470 | int age; 471 | 472 | @INI("The height of the Person") 473 | float height; 474 | 475 | @INI("Some strings with a very long long INI description that is longer" ~ 476 | " than eigthy lines hopefully." 477 | ) 478 | string[] someStrings = [":::60180", "asd"]; 479 | 480 | @INI("Some ints") 481 | int[] someInts; 482 | 483 | int dontShowThis; 484 | 485 | @INI("A Spouse") 486 | Spouse spouse; 487 | 488 | @INI("The family dog") 489 | Dog dog; 490 | 491 | bool opEquals(Person other) { 492 | import std.algorithm.comparison : equal; 493 | return this.firstname == other.firstname 494 | && this.lastname == other.lastname 495 | && this.age == other.age 496 | && (isClose(this.height, other.height) 497 | || (isNaN(this.height) && isNaN(other.height))) 498 | && equal(this.someStrings, other.someStrings) 499 | && equal(this.someInts, other.someInts) 500 | && this.spouse == other.spouse 501 | && this.dog == other.dog; 502 | } 503 | } 504 | 505 | @INI("A House") 506 | struct House { 507 | @INI("Number of Rooms") 508 | uint rooms; 509 | 510 | @INI("Number of Floors") 511 | uint floors; 512 | 513 | bool opEquals(House other) { 514 | return this.rooms == other.rooms 515 | && this.floors == other.floors; 516 | } 517 | } 518 | } 519 | 520 | unittest { 521 | import std.stdio : writefln; 522 | Person p; 523 | p.firstname = "Foo"; 524 | p.lastname = "Bar"; 525 | p.age = 1337; 526 | p.height = 7331.0; 527 | 528 | p.someStrings ~= "Hello"; 529 | p.someStrings ~= "World"; 530 | 531 | p.someInts ~= [1,2]; 532 | 533 | p.spouse.firstname = "World"; 534 | p.spouse.age = 72; 535 | 536 | p.spouse.house.rooms = 5; 537 | p.spouse.house.floors = 2; 538 | 539 | p.dog.name = "Wuff"; 540 | p.dog.kg = 3.14; 541 | 542 | Person p2; 543 | readINIFile(p2, "test/filename.ini"); 544 | writefln("\n%s\n", p2); 545 | writeINIFile(p2, "test/filenameTmp.ini"); 546 | 547 | Person p3; 548 | readINIFile(p3, "test/filenameTmp.ini"); 549 | 550 | if(p2 != p3) { 551 | writefln("\n%s\n%s", p2, p3); 552 | writefln("Spouse equal %b", p2.spouse == p3.spouse); 553 | writefln("Dog equal %b", p2.dog == p3.dog); 554 | assert(false); 555 | } 556 | if(p != p3) { 557 | writefln("\n%s\n%s", p, p3); 558 | writefln("Spouse equal %b", p.spouse == p3.spouse); 559 | writefln("Dog equal %b", p.dog == p3.dog); 560 | assert(false); 561 | } 562 | if(p != p2) { 563 | writefln("\n%s\n%s", p, p2); 564 | writefln("Spouse equal %b", p.spouse == p2.spouse); 565 | writefln("Dog equal %b", p.dog == p2.dog); 566 | assert(false); 567 | } 568 | } 569 | 570 | version(unittest) { 571 | enum Check : string { disabled = "disabled"} 572 | 573 | @INI 574 | struct StaticAnalysisConfig { 575 | @INI 576 | string style_check = Check.disabled; 577 | 578 | @INI 579 | ModuleFilters filters; 580 | 581 | @INI @ConfuseTheParser 582 | string multi_line; 583 | } 584 | 585 | private template ModuleFiltersMixin(A) { 586 | const string ModuleFiltersMixin = () { 587 | string s; 588 | foreach (mem; __traits(allMembers, StaticAnalysisConfig)) 589 | static if( 590 | is( 591 | typeof(__traits(getMember, StaticAnalysisConfig, mem)) 592 | == string 593 | ) 594 | ) { 595 | s ~= `@INI string[] ` ~ mem ~ ";\n"; 596 | } 597 | 598 | return s; 599 | }(); 600 | } 601 | 602 | @INI 603 | struct ModuleFilters { mixin(ModuleFiltersMixin!int); } 604 | } 605 | 606 | unittest { 607 | StaticAnalysisConfig config; 608 | readINIFile(config, "test/dscanner.ini"); 609 | assert(config.style_check == "disabled"); 610 | assert(config.multi_line == `+std.algorithm -std.foo `); 611 | assert(config.filters.style_check == ["+std.algorithm"]); 612 | } 613 | 614 | version(unittest) { 615 | struct ConfuseTheParser; 616 | } 617 | 618 | unittest { 619 | @INI("Reactor Configuration", "reactorConfig") 620 | struct NukeConfig 621 | { 622 | @INI double a; 623 | @INI double b; 624 | @INI double c; 625 | } 626 | 627 | @INI("General Configuration", "general") 628 | struct Configuration 629 | { 630 | @INI("Color of the bikeshed", "shedColor") string shedC; 631 | @INI @ConfuseTheParser NukeConfig nConfig; 632 | } 633 | 634 | Configuration c = Configuration("blue"); 635 | c.nConfig.a = 1; 636 | c.nConfig.b = 2; 637 | c.nConfig.c = 3; 638 | c.writeINIFile("test/bikeShed.ini"); 639 | Configuration c2; 640 | readINIFile(c2, "test/bikeShed.ini"); 641 | assert(c2.shedC == "blue"); 642 | assert(c2.nConfig.a == 1); 643 | assert(c2.nConfig.b == 2); 644 | assert(c2.nConfig.c == 3); 645 | } 646 | -------------------------------------------------------------------------------- /test/dscanner.ini: -------------------------------------------------------------------------------- 1 | [inifiled.StaticAnalysisConfig] 2 | style_check="disabled" 3 | multi_line= "+std.algorithm \ 4 | -std.foo \ 5 | " 6 | [inifiled.ModuleFilters] 7 | style_check = "+std.algorithm" 8 | -------------------------------------------------------------------------------- /test/filename.ini: -------------------------------------------------------------------------------- 1 | ; A Person 2 | [inifiled.Person] 3 | ; The firstname of the Person 4 | firstname="Foo" 5 | ; The lastname of the Person 6 | lastname="Bar" 7 | ; The age of the Person 8 | age="1337" 9 | ; The height of the Person 10 | height="7331" 11 | ; Some strings with a very long long INI description that is longer than eigthy 12 | ; lines hopefully. 13 | someStrings=":::60180,asd,Hello,World" 14 | ; Some ints 15 | someInts="1,2" 16 | ; A Spouse 17 | [inifiled.Spouse] 18 | ; The firstname of the spouse 19 | firstname="World" 20 | ; The age of the spouse 21 | age="72" 22 | ; A House 23 | [inifiled.House] 24 | ; Number of Rooms 25 | rooms="5" 26 | ; Number of Floors 27 | floors="2" 28 | ; A Dog 29 | [inifiled.Dog] 30 | ; The name of the Dog 31 | name="Wuff" 32 | ; The food consumed 33 | kg="3.14" 34 | --------------------------------------------------------------------------------