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.aa : getOrAdd; 24 import ae.utils.exception; 25 import ae.utils.meta : boxVoid, unboxVoid; 26 27 alias std..string.indexOf indexOf; 28 29 /// Represents the user-defined behavior for handling a node in a 30 /// structured INI file's hierarchy. 31 struct IniHandler(S) 32 { 33 /// User callback for parsing a value at this node. 34 void delegate(S value) leafHandler; 35 36 /// User callback for obtaining a child node from this node. 37 IniHandler delegate(S name) nodeHandler; 38 } 39 40 struct IniLine(S) 41 { 42 enum Type 43 { 44 empty, 45 section, 46 value 47 } 48 49 Type type; 50 S name; // section or value 51 S value; 52 } 53 54 IniLine!S lexIniLine(S)(S line) 55 if (isSomeString!S) 56 { 57 IniLine!S result; 58 59 line = line.chomp().stripLeft(); 60 if (line.empty) 61 return result; 62 if (line[0] == '#' || line[0] == ';') 63 return result; 64 65 if (line[0] == '[') 66 { 67 line = line.stripRight(); 68 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 69 result.type = result.Type.section; 70 result.name = line[1..$-1]; 71 } 72 else 73 { 74 auto pos = line.indexOf('='); 75 enforce(pos > 0, "Malformed value line (no '=')"); 76 result.type = result.Type.value; 77 result.name = line[0..pos].strip; 78 result.value = line[pos+1..$].strip; 79 } 80 return result; 81 } 82 83 /// Evaluates to `true` if H is a valid INI handler for a string type S. 84 enum isIniHandler(H, S) = 85 is(typeof((H handler, S s) { handler.nodeHandler(s); handler.leafHandler(s); })); 86 87 /// Parse a structured INI from a range of lines, through the given handler. 88 void parseIni(R, H)(R r, H rootHandler) 89 if (isInputRange!R && isSomeString!(ElementType!R) && isIniHandler!(H, ElementType!R)) 90 { 91 auto currentHandler = rootHandler; 92 93 size_t lineNumber; 94 while (!r.empty) 95 { 96 lineNumber++; 97 mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)})); 98 99 scope(success) r.popFront(); 100 auto line = lexIniLine(r.front); 101 final switch (line.type) 102 { 103 case line.Type.empty: 104 break; 105 case line.Type.section: 106 currentHandler = rootHandler; 107 foreach (segment; line.name.split(".")) 108 currentHandler = currentHandler.nodeHandler 109 .enforce("This group may not have any nodes.") 110 (segment); 111 break; 112 case line.Type.value: 113 { 114 auto handler = currentHandler; 115 auto segments = line.name.split("."); 116 enforce(segments.length, "Malformed value line (empty name)"); 117 enforce(handler.nodeHandler, "This group may not have any nodes."); 118 while (segments.length > 1) 119 { 120 auto next = handler.nodeHandler(segments[0]); 121 if (!next.nodeHandler) 122 break; 123 handler = next; 124 segments = segments[1..$]; 125 } 126 handler.nodeHandler 127 .enforce("This group may not have any nodes.") 128 (segments.join(".")) 129 .leafHandler 130 .enforce("This group may not have any values.") 131 (line.value); 132 break; 133 } 134 } 135 } 136 } 137 138 /// Helper which creates an INI handler out of delegates. 139 IniHandler!S iniHandler(S)(void delegate(S) leafHandler, IniHandler!S delegate(S) nodeHandler = null) 140 { 141 return IniHandler!S(leafHandler, nodeHandler); 142 } 143 144 /// Alternative API for IniHandler, where each leaf accepts name/value 145 /// pairs instead of single values. 146 struct IniThickLeafHandler(S) 147 { 148 /// User callback for parsing a value at this node. 149 void delegate(S name, S value) leafHandler; 150 151 /// User callback for obtaining a child node from this node. 152 IniThickLeafHandler delegate(S name) nodeHandler; 153 154 private IniHandler!S conv(S currentName = null) 155 { 156 // Don't reference "this" from a lambda, 157 // as it can be a temporary on the stack 158 IniThickLeafHandler self = this; 159 return IniHandler!S 160 ( 161 !currentName || !self.leafHandler ? null : 162 (S value) 163 { 164 self.leafHandler(currentName, value); 165 }, 166 (currentName ? !self.nodeHandler : !self.nodeHandler && !self.leafHandler) ? null : 167 (S name) 168 { 169 if (!currentName) 170 return self.conv(name); 171 else 172 return self.nodeHandler(currentName).conv(name); 173 } 174 ); 175 } 176 } 177 178 /// Helper which creates an IniThinkLeafHandler. 179 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniThickLeafHandler!S delegate(S) nodeHandler = null) 180 { 181 return IniThickLeafHandler!S(leafHandler, nodeHandler).conv(null); 182 } 183 184 185 unittest 186 { 187 int count; 188 189 parseIni 190 ( 191 q"< 192 s.n1=v1 193 [s] 194 n2=v2 195 >".splitLines(), 196 iniHandler 197 ( 198 null, 199 (in char[] name) 200 { 201 assert(name == "s"); 202 return iniHandler 203 ( 204 (in char[] name, in char[] value) 205 { 206 assert(name .length==2 && name [0] == 'n' 207 && value.length==2 && value[0] == 'v' 208 && name[1] == value[1]); 209 count++; 210 } 211 ); 212 } 213 ) 214 ); 215 216 assert(count==2); 217 } 218 219 enum isNestingType(T) = isAssociativeArray!T || is(T == struct); 220 221 private enum isAALike(U, S) = is(typeof( 222 (ref U v) 223 { 224 alias K = typeof(v.keys[0]); 225 alias V = typeof(v[K.init]); 226 } 227 )); 228 229 IniHandler!S makeIniHandler(S = string, U)(ref U v) 230 { 231 static if (!is(U == Unqual!U)) 232 return makeIniHandler!S(*cast(Unqual!U*)&v); 233 else 234 static if (isAALike!(U, S)) 235 return IniHandler!S 236 ( 237 null, 238 (S name) 239 { 240 alias K = typeof(v.keys[0]); 241 alias V = typeof(v[K.init]); 242 243 auto key = name.to!K; 244 245 auto update(T)(T delegate(ref V) dg) 246 { 247 static if (!isNestingType!U) 248 if (key in v) 249 throw new Exception("Duplicate value: " ~ to!string(name)); 250 return dg(v.getOrAdd(key)); 251 } 252 253 // To know if the value handler will accept leafs or nodes requires constructing the handler. 254 // To construct the handler we must have a pointer to the object it will handle. 255 // To have a pointer to the object means to allocate it in the AA... 256 // but, we can't do that until we know it's going to be written to. 257 // So, introspect what the handler for this type can handle at compile-time instead. 258 enum dummyHandlerCaps = { 259 V dummy; 260 auto h = makeIniHandler!S(dummy); 261 return [ 262 h.leafHandler !is null, 263 h.nodeHandler !is null, 264 ]; 265 }(); 266 267 return IniHandler!S 268 ( 269 !dummyHandlerCaps[0] ? null : (S value) => update((ref V v) => makeIniHandler!S(v).leafHandler(value)), 270 !dummyHandlerCaps[1] ? null : (S name2) => update((ref V v) => makeIniHandler!S(v).nodeHandler(name2)), 271 ); 272 } 273 ); 274 else 275 static if (isAssociativeArray!U) 276 static assert(false, "Unsupported associative array type " ~ U.stringof); 277 else 278 static if (is(U == struct)) 279 return IniHandler!S 280 ( 281 null, 282 delegate IniHandler!S (S name) 283 { 284 foreach (i, ref field; v.tupleof) 285 { 286 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 287 if (name == fieldName) 288 { 289 static if (is(typeof(makeIniHandler!S(v.tupleof[i])))) 290 return makeIniHandler!S(v.tupleof[i]); 291 else 292 throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof); 293 } 294 } 295 static if (is(ReturnType!(v.parseSection))) 296 return v.parseSection(name); 297 else 298 throw new Exception("Unknown field " ~ to!string(name)); 299 } 300 ); 301 else 302 static if (is(typeof(to!U(string.init)))) 303 return IniHandler!S 304 ( 305 (S value) 306 { 307 v = to!U(value); 308 } 309 ); 310 else 311 static if (is(U V : V*)) 312 { 313 static if (is(typeof(v = new V))) 314 if (!v) 315 v = new V; 316 return makeIniHandler!S(*v); 317 } 318 else 319 static assert(false, "Can't parse " ~ U.stringof); 320 } 321 322 /// Parse structured INI lines from a range of strings, into a user-defined struct. 323 T parseIni(T, R)(R r) 324 if (isInputRange!R && isSomeString!(ElementType!R)) 325 { 326 T result; 327 r.parseIniInto(result); 328 return result; 329 } 330 331 /// ditto 332 void parseIniInto(R, T)(R r, ref T result) 333 if (isInputRange!R && isSomeString!(ElementType!R)) 334 { 335 parseIni(r, makeIniHandler!(ElementType!R)(result)); 336 } 337 338 unittest 339 { 340 static struct File 341 { 342 struct S 343 { 344 string n1, n2; 345 int[string] a; 346 } 347 S s; 348 } 349 350 auto f = parseIni!File 351 ( 352 q"< 353 s.n1=v1 354 s.a.foo=1 355 [s] 356 n2=v2 357 a.bar=2 358 >".dup.splitLines() 359 ); 360 361 assert(f.s.n1=="v1"); 362 assert(f.s.n2=="v2"); 363 assert(f.s.a==["foo":1, "bar":2]); 364 } 365 366 unittest 367 { 368 static struct Custom 369 { 370 struct Section 371 { 372 string name; 373 string[string] values; 374 } 375 Section[] sections; 376 377 auto parseSection(wstring name) 378 { 379 sections.length++; 380 auto p = §ions[$-1]; 381 p.name = to!string(name); 382 return makeIniHandler!wstring(p.values); 383 } 384 } 385 386 auto c = parseIni!Custom 387 ( 388 q"< 389 [one] 390 a=a 391 [two] 392 b=b 393 >"w.splitLines() 394 ); 395 396 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 397 } 398 399 version(unittest) static import ae.utils.aa; 400 401 unittest 402 { 403 import ae.utils.aa; 404 405 alias M = OrderedMap!(string, string); 406 static assert(isAALike!(M, string)); 407 408 auto o = parseIni!M 409 ( 410 q"< 411 b=b 412 a=a 413 >".splitLines() 414 ); 415 416 assert(o["a"]=="a" && o["b"] == "b"); 417 } 418 419 unittest 420 { 421 import ae.utils.aa; 422 423 static struct S { string x; } 424 alias M = OrderedMap!(string, S); 425 static assert(isAALike!(M, string)); 426 427 auto o = parseIni!M 428 ( 429 q"< 430 b.x=b 431 [a] 432 x=a 433 >".splitLines() 434 ); 435 436 assert(o["a"].x == "a" && o["b"].x == "b"); 437 } 438 439 unittest 440 { 441 static struct S { string x, y; } 442 443 auto r = parseIni!(S[string]) 444 ( 445 q"< 446 a.x=x 447 [a] 448 y=y 449 >".splitLines() 450 ); 451 452 assert(r["a"].x == "x" && r["a"].y == "y"); 453 } 454 455 unittest 456 { 457 static struct S { string x, y; } 458 static struct T { S* s; } 459 460 { 461 T t; 462 parseIniInto(["s.x=v"], t); 463 assert(t.s.x == "v"); 464 } 465 466 { 467 S s = {"x"}; T t = {&s}; 468 parseIniInto(["s.y=v"], t); 469 assert(s.x == "x"); 470 assert(s.y == "v"); 471 } 472 } 473 474 unittest 475 { 476 auto r = parseIni!(string[string]) 477 ( 478 q"< 479 a.b.c=d.e.f 480 >".splitLines() 481 ); 482 483 assert(r == ["a.b.c" : "d.e.f"]); 484 } 485 486 // *************************************************************************** 487 488 deprecated alias parseStructuredIni = parseIni; 489 deprecated alias makeStructuredIniHandler = makeIniHandler; 490 491 // *************************************************************************** 492 493 /// Convenience function to load a struct from an INI file. 494 /// Returns .init if the file does not exist. 495 S loadIni(S)(string fileName) 496 { 497 S s; 498 499 import std.file; 500 if (fileName.exists) 501 s = fileName 502 .readText() 503 .splitLines() 504 .parseIni!S(); 505 506 return s; 507 } 508 509 /// As above, though loads several INI files 510 /// (duplicate values appearing in later INI files 511 /// override any values from earlier files). 512 S loadInis(S)(in char[][] fileNames) 513 { 514 S s; 515 516 import std.file; 517 s = fileNames 518 .map!(fileName => 519 fileName.exists ? 520 fileName 521 .readText() 522 .splitLines() 523 : 524 null 525 ) 526 .joiner(["[]"]) 527 .parseIni!S(); 528 529 return s; 530 } 531 532 // *************************************************************************** 533 534 /// Simple convenience formatter for writing INI files. 535 struct IniWriter(O) 536 { 537 O writer; 538 539 void startSection(string name) 540 { 541 writer.put('[', name, "]\n"); 542 } 543 544 void writeValue(string name, string value) 545 { 546 writer.put(name, '=', value, '\n'); 547 } 548 } 549 550 /// Insert a blank line before each section 551 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); } 552 553 // *************************************************************************** 554 555 /** 556 Adds or updates a value in an INI file. 557 558 If the value is already in the INI file, then it is updated 559 in-place; otherwise, a new one is added to the matching section. 560 561 Whitespace and comments on other lines are preserved. 562 563 Params: 564 lines = INI file lines (as in parseIni) 565 name = fully-qualified name of the value to update 566 (use `.` to specify section path) 567 value = new value to write 568 */ 569 570 void updateIni(S)(ref S[] lines, S name, S value) 571 { 572 size_t valueLine = size_t.max; 573 S valueLineSection; 574 575 S currentSection = null; 576 auto pathPrefix() { return chain(currentSection, repeat(typeof(name[0])('.'), currentSection is null ? 0 : 1)); } 577 578 size_t bestSectionEnd; 579 S bestSection; 580 bool inBestSection = true; 581 582 foreach (i, line; lines) 583 { 584 auto lex = lexIniLine(line); 585 final switch (lex.type) 586 { 587 case lex.Type.empty: 588 break; 589 case lex.Type.value: 590 if (equal(chain(pathPrefix, lex.name), name)) 591 { 592 valueLine = i; 593 valueLineSection = currentSection; 594 } 595 break; 596 case lex.type.section: 597 if (inBestSection) 598 bestSectionEnd = i; 599 inBestSection = false; 600 601 currentSection = lex.name; 602 if (name.startsWith(pathPrefix) && currentSection.length > bestSection.length) 603 { 604 bestSection = currentSection; 605 inBestSection = true; 606 } 607 break; 608 } 609 } 610 611 if (inBestSection) 612 bestSectionEnd = lines.length; 613 614 S genLine(S section) { return name[section.length ? section.length + 1 : 0 .. $] ~ '=' ~ value; } 615 616 if (valueLine != size_t.max) 617 lines[valueLine] = genLine(valueLineSection); 618 else 619 lines = lines[0..bestSectionEnd] ~ genLine(bestSection) ~ lines[bestSectionEnd..$]; 620 } 621 622 unittest 623 { 624 auto ini = q"< 625 a=1 626 a=2 627 >".splitLines(); 628 updateIni(ini, "a", "3"); 629 struct S { int a; } 630 assert(parseIni!S(ini).a == 3); 631 } 632 633 unittest 634 { 635 auto ini = q"< 636 a=1 637 [s] 638 a=2 639 [t] 640 a=3 641 >".strip.splitLines.map!strip.array; 642 updateIni(ini, "a", "4"); 643 updateIni(ini, "s.a", "5"); 644 updateIni(ini, "t.a", "6"); 645 assert(equal(ini, q"< 646 a=4 647 [s] 648 a=5 649 [t] 650 a=6 651 >".strip.splitLines.map!strip), text(ini)); 652 } 653 654 unittest 655 { 656 auto ini = q"< 657 [s] 658 [t] 659 >".strip.splitLines.map!strip.array; 660 updateIni(ini, "a", "1"); 661 updateIni(ini, "s.a", "2"); 662 updateIni(ini, "t.a", "3"); 663 assert(equal(ini, q"< 664 a=1 665 [s] 666 a=2 667 [t] 668 a=3 669 >".strip.splitLines.map!strip)); 670 } 671 672 void updateIniFile(S)(string fileName, S name, S value) 673 { 674 import std.file, std.stdio, std.utf; 675 auto lines = fileName.exists ? fileName.readText.splitLines : null; 676 updateIni(lines, name, value); 677 lines.map!(l => chain(l.byCodeUnit, only(typeof(S.init[0])('\n')))).joiner.toFile(fileName); 678 } 679 680 unittest 681 { 682 import std.file; 683 enum fn = "temp.ini"; 684 std.file.write(fn, "a=b\n"); 685 scope(exit) remove(fn); 686 updateIniFile(fn, "a", "c"); 687 assert(read(fn) == "a=c\n"); 688 }