1 /** 2 * Structured INI 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.sini; 15 16 import std.algorithm; 17 import std.conv; 18 import std.exception; 19 import std.range; 20 import std.string; 21 import std.traits; 22 23 alias std..string.indexOf indexOf; 24 25 /// Represents the user-defined behavior for handling a node in a 26 /// structured INI file's hierarchy. 27 struct IniHandler(S) 28 { 29 /// User callback for parsing a value at this node. 30 void delegate(S name, S value) leafHandler; 31 32 /// User callback for obtaining a child node from this node. 33 IniHandler delegate(S name) nodeHandler; 34 } 35 36 /// Parse a structured INI from a range of lines, through the given handler. 37 void parseIni(R, H)(R r, H rootHandler) 38 if (isInputRange!R && isSomeString!(ElementType!R)) 39 { 40 auto currentHandler = rootHandler; 41 42 size_t lineNumber; 43 while (!r.empty) 44 { 45 lineNumber++; 46 47 auto line = r.front.chomp().stripLeft(); 48 scope(success) r.popFront(); 49 if (line.empty) 50 continue; 51 if (line[0] == '#' || line[0] == ';') 52 continue; 53 54 if (line[0] == '[') 55 { 56 line = line.stripRight(); 57 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 58 auto section = line[1..$-1]; 59 60 currentHandler = rootHandler; 61 foreach (segment; section.split(".")) 62 currentHandler = currentHandler.nodeHandler 63 .enforce("This group may not have any nodes.") 64 (segment); 65 } 66 else 67 { 68 auto pos = line.indexOf('='); 69 enforce(pos > 0, "Malformed value line (no '=')"); 70 auto name = line[0..pos].strip; 71 auto handler = currentHandler; 72 auto segments = name.split("."); 73 enforce(segments.length, "Malformed value line (empty name)"); 74 foreach (segment; segments[0..$-1]) 75 handler = handler.nodeHandler 76 .enforce("This group may not have any nodes.") 77 (segment); 78 handler.leafHandler 79 .enforce("This group may not have any values.") 80 (segments[$-1], line[pos+1..$].strip); 81 } 82 } 83 } 84 85 /// Helper which creates an INI handler out of delegates. 86 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null) 87 { 88 return IniHandler!S(leafHandler, nodeHandler); 89 } 90 91 unittest 92 { 93 int count; 94 95 parseIni 96 ( 97 q"< 98 s.n1=v1 99 [s] 100 n2=v2 101 >".splitLines(), 102 iniHandler 103 ( 104 null, 105 (in char[] name) 106 { 107 assert(name == "s"); 108 return iniHandler 109 ( 110 (in char[] name, in char[] value) 111 { 112 assert(name .length==2 && name [0] == 'n' 113 && value.length==2 && value[0] == 'v' 114 && name[1] == value[1]); 115 count++; 116 } 117 ); 118 } 119 ) 120 ); 121 122 assert(count==2); 123 } 124 125 /// Alternative API for IniHandler, where each leaf is a node 126 struct IniTraversingHandler(S) 127 { 128 /// User callback for parsing a value at this node. 129 void delegate(S value) leafHandler; 130 131 /// User callback for obtaining a child node from this node. 132 IniTraversingHandler delegate(S name) nodeHandler; 133 134 private IniHandler!S conv() 135 { 136 // Don't reference "this" from a lambda, 137 // as it can be a temporary on the stack 138 IniTraversingHandler thisCopy = this; 139 return IniHandler!S 140 ( 141 (S name, S value) 142 { 143 thisCopy 144 .nodeHandler 145 .enforce("This group may not have any nodes.") 146 (name) 147 .leafHandler 148 .enforce("This group may not have a value.") 149 (value); 150 }, 151 (S name) 152 { 153 return thisCopy 154 .nodeHandler 155 .enforce("This group may not have any nodes.") 156 (name) 157 .conv(); 158 } 159 ); 160 } 161 } 162 163 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v) 164 { 165 static if (!is(U == Unqual!U)) 166 return makeIniHandler!S(*cast(Unqual!U*)&v); 167 else 168 static if (is(U == struct)) 169 return IniTraversingHandler!S 170 ( 171 null, 172 delegate IniTraversingHandler!S (S name) 173 { 174 bool found; 175 foreach (i, field; v.tupleof) 176 { 177 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 178 if (name == fieldName) 179 { 180 static if (is(typeof(makeIniHandler!S(v.tupleof[i])))) 181 return makeIniHandler!S(v.tupleof[i]); 182 else 183 throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof); 184 } 185 } 186 static if (is(ReturnType!(v.parseSection))) 187 return v.parseSection(name); 188 else 189 throw new Exception("Unknown field " ~ to!string(name)); 190 } 191 ); 192 else 193 static if (isAssociativeArray!U) 194 return IniTraversingHandler!S 195 ( 196 null, 197 (S name) 198 { 199 alias K = typeof(v.keys[0]); 200 auto key = to!K(name); 201 auto pField = key in v; 202 if (!pField) 203 { 204 v[key] = typeof(v[key]).init; 205 pField = key in v; 206 } 207 else 208 throw new Exception("Duplicate value: " ~ to!string(name)); 209 return makeIniHandler!S(*pField); 210 } 211 ); 212 else 213 static if (is(typeof(to!U(string.init)))) 214 return IniTraversingHandler!S 215 ( 216 (S value) 217 { 218 v = to!U(value); 219 } 220 ); 221 else 222 static assert(false, "Can't parse " ~ U.stringof); 223 } 224 225 /// Parse structured INI lines from a range of strings, into a user-defined struct. 226 T parseIni(T, R)(R r) 227 if (isInputRange!R && isSomeString!(ElementType!R)) 228 { 229 T result; 230 r.parseIniInto(result); 231 return result; 232 } 233 234 /// ditto 235 void parseIniInto(R, T)(R r, ref T result) 236 if (isInputRange!R && isSomeString!(ElementType!R)) 237 { 238 parseIni(r, makeIniHandler!(ElementType!R)(result).conv()); 239 } 240 241 unittest 242 { 243 static struct File 244 { 245 struct S 246 { 247 string n1, n2; 248 int[string] a; 249 } 250 S s; 251 } 252 253 auto f = parseIni!File 254 ( 255 q"< 256 s.n1=v1 257 s.a.foo=1 258 [s] 259 n2=v2 260 a.bar=2 261 >".dup.splitLines() 262 ); 263 264 assert(f.s.n1=="v1"); 265 assert(f.s.n2=="v2"); 266 assert(f.s.a==["foo":1, "bar":2]); 267 } 268 269 unittest 270 { 271 static struct Custom 272 { 273 struct Section 274 { 275 string name; 276 string[string] values; 277 } 278 Section[] sections; 279 280 auto parseSection(wstring name) 281 { 282 sections.length++; 283 auto p = §ions[$-1]; 284 p.name = to!string(name); 285 return makeIniHandler!wstring(p.values); 286 } 287 } 288 289 auto c = parseIni!Custom 290 ( 291 q"< 292 [one] 293 a=a 294 [two] 295 b=b 296 >"w.splitLines() 297 ); 298 299 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 300 } 301 302 // *************************************************************************** 303 304 deprecated alias StructuredIniHandler = IniHandler; 305 deprecated alias parseStructuredIni = parseIni; 306 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler; 307 deprecated alias makeStructuredIniHandler = makeIniHandler; 308 309 // *************************************************************************** 310 311 /// Convenience function to load a struct from an INI file. 312 /// Returns .init if the file does not exist. 313 S loadIni(S)(string fileName) 314 { 315 S s; 316 317 import std.file; 318 if (fileName.exists) 319 s = fileName 320 .readText() 321 .splitLines() 322 .parseIni!S(); 323 324 return s; 325 } 326 327 /// As above, though loads several INI files 328 /// (duplicate values appearing in later INI files 329 /// override any values from earlier files). 330 S loadInis(S)(in char[][] fileNames) 331 { 332 S s; 333 334 import std.file; 335 s = fileNames 336 .map!(fileName => 337 fileName.exists ? 338 fileName 339 .readText() 340 .splitLines() 341 : 342 null 343 ) 344 .joiner(["[]"]) 345 .parseIni!S(); 346 347 return s; 348 } 349 350 // *************************************************************************** 351 352 /// Simple convenience formatter for writing INI files. 353 struct IniWriter(O) 354 { 355 O writer; 356 357 void startSection(string name) 358 { 359 writer.put('[', name, "]\n"); 360 } 361 362 void writeValue(string name, string value) 363 { 364 writer.put(name, '=', value, '\n'); 365 } 366 } 367 368 /// Insert a blank line before each section 369 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); }