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 import ae.utils.exception; 24 25 alias std..string.indexOf indexOf; 26 27 /// Represents the user-defined behavior for handling a node in a 28 /// structured INI file's hierarchy. 29 struct IniHandler(S) 30 { 31 /// User callback for parsing a value at this node. 32 void delegate(S name, S value) leafHandler; 33 34 /// User callback for obtaining a child node from this node. 35 IniHandler delegate(S name) nodeHandler; 36 } 37 38 /// Parse a structured INI from a range of lines, through the given handler. 39 void parseIni(R, H)(R r, H rootHandler) 40 if (isInputRange!R && isSomeString!(ElementType!R)) 41 { 42 auto currentHandler = rootHandler; 43 44 size_t lineNumber; 45 while (!r.empty) 46 { 47 lineNumber++; 48 mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)})); 49 50 auto line = r.front.chomp().stripLeft(); 51 scope(success) r.popFront(); 52 if (line.empty) 53 continue; 54 if (line[0] == '#' || line[0] == ';') 55 continue; 56 57 if (line[0] == '[') 58 { 59 line = line.stripRight(); 60 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 61 auto section = line[1..$-1]; 62 63 currentHandler = rootHandler; 64 foreach (segment; section.split(".")) 65 currentHandler = currentHandler.nodeHandler 66 .enforce("This group may not have any nodes.") 67 (segment); 68 } 69 else 70 { 71 auto pos = line.indexOf('='); 72 enforce(pos > 0, "Malformed value line (no '=')"); 73 auto name = line[0..pos].strip; 74 auto handler = currentHandler; 75 auto segments = name.split("."); 76 enforce(segments.length, "Malformed value line (empty name)"); 77 foreach (segment; segments[0..$-1]) 78 handler = handler.nodeHandler 79 .enforce("This group may not have any nodes.") 80 (segment); 81 handler.leafHandler 82 .enforce("This group may not have any values.") 83 (segments[$-1], line[pos+1..$].strip); 84 } 85 } 86 } 87 88 /// Helper which creates an INI handler out of delegates. 89 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null) 90 { 91 return IniHandler!S(leafHandler, nodeHandler); 92 } 93 94 unittest 95 { 96 int count; 97 98 parseIni 99 ( 100 q"< 101 s.n1=v1 102 [s] 103 n2=v2 104 >".splitLines(), 105 iniHandler 106 ( 107 null, 108 (in char[] name) 109 { 110 assert(name == "s"); 111 return iniHandler 112 ( 113 (in char[] name, in char[] value) 114 { 115 assert(name .length==2 && name [0] == 'n' 116 && value.length==2 && value[0] == 'v' 117 && name[1] == value[1]); 118 count++; 119 } 120 ); 121 } 122 ) 123 ); 124 125 assert(count==2); 126 } 127 128 /// Alternative API for IniHandler, where each leaf is a node 129 struct IniTraversingHandler(S) 130 { 131 /// User callback for parsing a value at this node. 132 void delegate(S value) leafHandler; 133 134 /// User callback for obtaining a child node from this node. 135 IniTraversingHandler delegate(S name) nodeHandler; 136 137 private IniHandler!S conv() 138 { 139 // Don't reference "this" from a lambda, 140 // as it can be a temporary on the stack 141 IniTraversingHandler thisCopy = this; 142 return IniHandler!S 143 ( 144 (S name, S value) 145 { 146 thisCopy 147 .nodeHandler 148 .enforce("This group may not have any nodes.") 149 (name) 150 .leafHandler 151 .enforce("This group may not have a value.") 152 (value); 153 }, 154 (S name) 155 { 156 return thisCopy 157 .nodeHandler 158 .enforce("This group may not have any nodes.") 159 (name) 160 .conv(); 161 } 162 ); 163 } 164 } 165 166 enum isNestingType(T) = isAssociativeArray!T || is(T == struct); 167 168 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v) 169 { 170 static if (!is(U == Unqual!U)) 171 return makeIniHandler!S(*cast(Unqual!U*)&v); 172 else 173 static if (is(typeof((ref U v){alias V=typeof(v[S.init]); v[S.init] = V.init; *(S.init in v) = V.init;}))) 174 return IniTraversingHandler!S 175 ( 176 null, 177 (S name) 178 { 179 auto pField = name in v; 180 if (!pField) 181 { 182 v[name] = typeof(v[name]).init; 183 pField = name in v; 184 } 185 else 186 static if (!isNestingType!U) 187 throw new Exception("Duplicate value: " ~ to!string(name)); 188 return makeIniHandler!S(*pField); 189 } 190 ); 191 else 192 static if (is(U == struct)) 193 return IniTraversingHandler!S 194 ( 195 null, 196 delegate IniTraversingHandler!S (S name) 197 { 198 bool found; 199 foreach (i, field; v.tupleof) 200 { 201 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 202 if (name == fieldName) 203 { 204 static if (is(typeof(makeIniHandler!S(v.tupleof[i])))) 205 return makeIniHandler!S(v.tupleof[i]); 206 else 207 throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof); 208 } 209 } 210 static if (is(ReturnType!(v.parseSection))) 211 return v.parseSection(name); 212 else 213 throw new Exception("Unknown field " ~ to!string(name)); 214 } 215 ); 216 else 217 static if (isAssociativeArray!U) 218 return IniTraversingHandler!S 219 ( 220 null, 221 (S name) 222 { 223 alias K = typeof(v.keys[0]); 224 auto key = to!K(name); 225 auto pField = key in v; 226 if (!pField) 227 { 228 v[key] = typeof(v[key]).init; 229 pField = key in v; 230 } 231 else 232 static if (!isNestingType!U) 233 throw new Exception("Duplicate value: " ~ to!string(name)); 234 return makeIniHandler!S(*pField); 235 } 236 ); 237 else 238 static if (is(typeof(to!U(string.init)))) 239 return IniTraversingHandler!S 240 ( 241 (S value) 242 { 243 v = to!U(value); 244 } 245 ); 246 else 247 static if (is(U V : V*)) 248 { 249 static if (is(typeof(v = new V))) 250 if (!v) 251 v = new V; 252 return makeIniHandler!S(*v); 253 } 254 else 255 static assert(false, "Can't parse " ~ U.stringof); 256 } 257 258 /// Parse structured INI lines from a range of strings, into a user-defined struct. 259 T parseIni(T, R)(R r) 260 if (isInputRange!R && isSomeString!(ElementType!R)) 261 { 262 T result; 263 r.parseIniInto(result); 264 return result; 265 } 266 267 /// ditto 268 void parseIniInto(R, T)(R r, ref T result) 269 if (isInputRange!R && isSomeString!(ElementType!R)) 270 { 271 parseIni(r, makeIniHandler!(ElementType!R)(result).conv()); 272 } 273 274 unittest 275 { 276 static struct File 277 { 278 struct S 279 { 280 string n1, n2; 281 int[string] a; 282 } 283 S s; 284 } 285 286 auto f = parseIni!File 287 ( 288 q"< 289 s.n1=v1 290 s.a.foo=1 291 [s] 292 n2=v2 293 a.bar=2 294 >".dup.splitLines() 295 ); 296 297 assert(f.s.n1=="v1"); 298 assert(f.s.n2=="v2"); 299 assert(f.s.a==["foo":1, "bar":2]); 300 } 301 302 unittest 303 { 304 static struct Custom 305 { 306 struct Section 307 { 308 string name; 309 string[string] values; 310 } 311 Section[] sections; 312 313 auto parseSection(wstring name) 314 { 315 sections.length++; 316 auto p = §ions[$-1]; 317 p.name = to!string(name); 318 return makeIniHandler!wstring(p.values); 319 } 320 } 321 322 auto c = parseIni!Custom 323 ( 324 q"< 325 [one] 326 a=a 327 [two] 328 b=b 329 >"w.splitLines() 330 ); 331 332 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 333 } 334 335 version(unittest) static import ae.utils.aa; 336 337 unittest 338 { 339 import ae.utils.aa; 340 341 auto o = parseIni!(OrderedMap!(string, string)) 342 ( 343 q"< 344 b=b 345 a=a 346 >".splitLines() 347 ); 348 349 assert(o["a"]=="a" && o["b"] == "b"); 350 } 351 352 unittest 353 { 354 import ae.utils.aa; 355 356 static struct S { string x; } 357 358 auto o = parseIni!(OrderedMap!(string, S)) 359 ( 360 q"< 361 b.x=b 362 [a] 363 x=a 364 >".splitLines() 365 ); 366 367 assert(o["a"].x == "a" && o["b"].x == "b"); 368 } 369 370 unittest 371 { 372 static struct S { string x, y; } 373 374 auto r = parseIni!(S[string]) 375 ( 376 q"< 377 a.x=x 378 [a] 379 y=y 380 >".splitLines() 381 ); 382 383 assert(r["a"].x == "x" && r["a"].y == "y"); 384 } 385 386 unittest 387 { 388 static struct S { string x, y; } 389 static struct T { S* s; } 390 391 { 392 T t; 393 parseIniInto(["s.x=v"], t); 394 assert(t.s.x == "v"); 395 } 396 397 { 398 S s = {"x"}; T t = {&s}; 399 parseIniInto(["s.y=v"], t); 400 assert(s.x == "x"); 401 assert(s.y == "v"); 402 } 403 } 404 405 // *************************************************************************** 406 407 deprecated alias StructuredIniHandler = IniHandler; 408 deprecated alias parseStructuredIni = parseIni; 409 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler; 410 deprecated alias makeStructuredIniHandler = makeIniHandler; 411 412 // *************************************************************************** 413 414 /// Convenience function to load a struct from an INI file. 415 /// Returns .init if the file does not exist. 416 S loadIni(S)(string fileName) 417 { 418 S s; 419 420 import std.file; 421 if (fileName.exists) 422 s = fileName 423 .readText() 424 .splitLines() 425 .parseIni!S(); 426 427 return s; 428 } 429 430 /// As above, though loads several INI files 431 /// (duplicate values appearing in later INI files 432 /// override any values from earlier files). 433 S loadInis(S)(in char[][] fileNames) 434 { 435 S s; 436 437 import std.file; 438 s = fileNames 439 .map!(fileName => 440 fileName.exists ? 441 fileName 442 .readText() 443 .splitLines() 444 : 445 null 446 ) 447 .joiner(["[]"]) 448 .parseIni!S(); 449 450 return s; 451 } 452 453 // *************************************************************************** 454 455 /// Simple convenience formatter for writing INI files. 456 struct IniWriter(O) 457 { 458 O writer; 459 460 void startSection(string name) 461 { 462 writer.put('[', name, "]\n"); 463 } 464 465 void writeValue(string name, string value) 466 { 467 writer.put(name, '=', value, '\n'); 468 } 469 } 470 471 /// Insert a blank line before each section 472 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); }