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 import std.typecons : Nullable; 23 24 import ae.utils.aa; // "require" polyfill 25 import ae.utils.array : nonNull; 26 import ae.utils.exception; 27 import ae.utils.meta : boxVoid, unboxVoid; 28 import ae.utils.appender : FastAppender; 29 30 alias std..string.indexOf indexOf; 31 32 /// Represents the user-defined behavior for handling a node in a 33 /// structured INI file's hierarchy. 34 struct IniHandler(S) 35 { 36 /// User callback for parsing a value at this node. 37 void delegate(S value) leafHandler; 38 39 /// User callback for obtaining a child node from this node. 40 IniHandler delegate(S name) nodeHandler; 41 42 /// User callback for indicating that this node is a section. 43 void delegate() sectionHandler; 44 } 45 46 struct IniLine(S) 47 { 48 enum Type 49 { 50 empty, 51 section, 52 value 53 } 54 55 Type type; 56 S name; // section or value 57 S value; 58 } 59 60 IniLine!S lexIniLine(S)(S line) 61 if (isSomeString!S) 62 { 63 IniLine!S result; 64 65 line = line.chomp().stripLeft(); 66 if (line.empty) 67 return result; 68 if (line[0] == '#' || line[0] == ';') 69 return result; 70 71 if (line[0] == '[') 72 { 73 line = line.stripRight(); 74 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 75 result.type = result.Type.section; 76 result.name = line[1..$-1]; 77 } 78 else 79 { 80 auto pos = line.indexOf('='); 81 enforce(pos > 0, "Malformed value line (no '=')"); 82 result.type = result.Type.value; 83 result.name = line[0..pos].strip; 84 result.value = line[pos+1..$].strip; 85 } 86 return result; 87 } 88 89 /// Evaluates to `true` if H is a valid INI handler for a string type S. 90 enum isIniHandler(H, S) = 91 is(typeof((H handler, S s) { handler.nodeHandler(s); handler.leafHandler(s); })); 92 93 /// Parse a structured INI from a range of lines, through the given handler. 94 void parseIni(R, H)(R r, H rootHandler) 95 if (isInputRange!R && isSomeString!(ElementType!R) && isIniHandler!(H, ElementType!R)) 96 { 97 auto currentHandler = rootHandler; 98 99 size_t lineNumber; 100 while (!r.empty) 101 { 102 lineNumber++; 103 mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)})); 104 105 scope(success) r.popFront(); 106 auto line = lexIniLine(r.front); 107 final switch (line.type) 108 { 109 case line.Type.empty: 110 break; 111 case line.Type.section: 112 currentHandler = rootHandler; 113 foreach (segment; line.name.splitter('.')) 114 currentHandler = currentHandler.nodeHandler 115 .enforce("This group may not have any nodes.") 116 (segment); 117 if (currentHandler.sectionHandler) 118 currentHandler.sectionHandler(); 119 break; 120 case line.Type.value: 121 { 122 auto handler = currentHandler; 123 auto segments = line.name.split("."); 124 enforce(segments.length, "Malformed value line (empty name)"); 125 enforce(handler.nodeHandler, "This group may not have any nodes."); 126 while (segments.length > 1) 127 { 128 auto next = handler.nodeHandler(segments[0]); 129 if (!next.nodeHandler) 130 break; 131 handler = next; 132 segments = segments[1..$]; 133 } 134 handler.nodeHandler 135 .enforce("This group may not have any nodes.") 136 (segments.join(".")) 137 .leafHandler 138 .enforce("This group may not have any values.") 139 (line.value); 140 break; 141 } 142 } 143 } 144 } 145 146 /// Helper which creates an INI handler out of delegates. 147 IniHandler!S iniHandler(S)(void delegate(S) leafHandler, IniHandler!S delegate(S) nodeHandler = null) 148 { 149 return IniHandler!S(leafHandler, nodeHandler); 150 } 151 152 /// Alternative API for IniHandler, where each leaf accepts name/value 153 /// pairs instead of single values. 154 struct IniThickLeafHandler(S) 155 { 156 /// User callback for parsing a value at this node. 157 void delegate(S name, S value) leafHandler; 158 159 /// User callback for obtaining a child node from this node. 160 IniThickLeafHandler delegate(S name) nodeHandler; 161 162 private IniHandler!S conv(S currentName = null) 163 { 164 // Don't reference "this" from a lambda, 165 // as it can be a temporary on the stack 166 IniThickLeafHandler self = this; 167 return IniHandler!S 168 ( 169 !currentName || !self.leafHandler ? null : 170 (S value) 171 { 172 self.leafHandler(currentName, value); 173 }, 174 (currentName ? !self.nodeHandler : !self.nodeHandler && !self.leafHandler) ? null : 175 (S name) 176 { 177 if (!currentName) 178 return self.conv(name); 179 else 180 return self.nodeHandler(currentName).conv(name); 181 } 182 ); 183 } 184 } 185 186 /// Helper which creates an IniThinkLeafHandler. 187 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniThickLeafHandler!S delegate(S) nodeHandler = null) 188 { 189 return IniThickLeafHandler!S(leafHandler, nodeHandler).conv(null); 190 } 191 192 193 unittest 194 { 195 int count; 196 197 parseIni 198 ( 199 q"< 200 s.n1=v1 201 [s] 202 n2=v2 203 >".splitLines(), 204 iniHandler 205 ( 206 null, 207 (in char[] name) 208 { 209 assert(name == "s"); 210 return iniHandler 211 ( 212 (in char[] name, in char[] value) 213 { 214 assert(name .length==2 && name [0] == 'n' 215 && value.length==2 && value[0] == 'v' 216 && name[1] == value[1]); 217 count++; 218 } 219 ); 220 } 221 ) 222 ); 223 224 assert(count==2); 225 } 226 227 enum isNestingType(T) = isAssociativeArray!T || is(T == struct); 228 229 private enum isAALike(U, S) = is(typeof( 230 (ref U v) 231 { 232 alias K = typeof(v.keys[0]); 233 alias V = typeof(v[K.init]); 234 } 235 )); 236 237 // Deserializing leaf and node handlers. 238 // Instantiation failure indicates lack of ability to handle leafs/nodes. 239 private 240 { 241 void leafHandler(S, alias v)(S value) 242 { 243 alias U = typeof(v); 244 static if (is(typeof(v = value))) 245 v = value; 246 else 247 v = value.to!U; 248 } 249 250 IniHandler!S nodeHandler(S, alias v)(S name) 251 { 252 alias U = typeof(v); 253 static if (isAALike!(U, S)) 254 { 255 alias K = typeof(v.keys[0]); 256 alias V = typeof(v[K.init]); 257 258 auto key = name.to!K; 259 260 auto update(T)(T delegate(ref V) dg) 261 { 262 static if (!isNestingType!U) 263 if (key in v) 264 throw new Exception("Duplicate value: " ~ to!string(name)); 265 return dg(v.require(key)); 266 } 267 268 // To know if the value handler will accept leafs or nodes requires constructing the handler. 269 // To construct the handler we must have a pointer to the object it will handle. 270 // To have a pointer to the object means to allocate it in the AA... 271 // but, we can't do that until we know it's going to be written to. 272 // So, introspect what the handler for this type can handle at compile-time instead. 273 enum dummyHandlerCaps = { 274 V dummy; 275 static if (is(V == U)) 276 { 277 // Avoid "Functions cannot be interpreted while being compiled" 278 return [ 279 is(typeof(leafHandler!(S, dummy)(S.init))), 280 true, // We can assume that the current function will instantiate successfully 281 ]; 282 } 283 else 284 { 285 auto h = dummy.makeIniHandler!S(); 286 return [ 287 h.leafHandler !is null, 288 h.nodeHandler !is null, 289 ]; 290 } 291 }(); 292 293 return IniHandler!S 294 ( 295 !dummyHandlerCaps[0] ? null : (S value) => update((ref V v) => v.makeIniHandler!S.leafHandler(value)), 296 !dummyHandlerCaps[1] ? null : (S name2) => update((ref V v) => v.makeIniHandler!S.nodeHandler(name2)), 297 () => update((ref V v) {}), 298 ); 299 } 300 else 301 static if (isAssociativeArray!U) 302 static assert(false, "Unsupported associative array type " ~ U.stringof); 303 else 304 static if (is(U == struct)) 305 { 306 foreach (i, ref field; v.tupleof) 307 { 308 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 309 if (name == fieldName) 310 { 311 static if (is(typeof(v.tupleof[i].makeIniHandler!S))) 312 return v.tupleof[i].makeIniHandler!S; 313 else 314 throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof); 315 } 316 } 317 static if (is(ReturnType!(v.parseSection))) 318 return v.parseSection(name); 319 else 320 throw new Exception("Unknown field " ~ to!string(name)); 321 } 322 else 323 static assert(false); 324 } 325 } 326 327 IniHandler!S makeIniHandler(S = string, U)(ref U v) 328 { 329 static if (!is(U == Unqual!U)) 330 return (*cast(Unqual!U*)&v).makeIniHandler!S; 331 else 332 static if (is(U V : V*)) 333 { 334 static if (is(typeof(v = new V))) 335 if (!v) 336 v = new V; 337 return (*v).makeIniHandler!S; 338 } 339 else 340 static if (is(U X == Nullable!X)) 341 { 342 if (v.isNull) 343 v = X.init; 344 return v.get.makeIniHandler!S; 345 } 346 else 347 static if (is(typeof(leafHandler!(S, v))) || is(typeof(nodeHandler!(S, v)))) 348 { 349 IniHandler!S handler; 350 static if (is(typeof(leafHandler!(S, v)))) 351 handler.leafHandler = &leafHandler!(S, v); 352 static if (is(typeof(nodeHandler!(S, v)))) 353 handler.nodeHandler = &nodeHandler!(S, v); 354 return handler; 355 } 356 else 357 static assert(false, "Can't parse " ~ U.stringof); 358 } 359 360 /// Parse structured INI lines from a range of strings, into a user-defined struct. 361 T parseIni(T, R)(R r) 362 if (isInputRange!R && isSomeString!(ElementType!R)) 363 { 364 T result; 365 r.parseIniInto(result); 366 return result; 367 } 368 369 /// ditto 370 void parseIniInto(R, T)(R r, ref T result) 371 if (isInputRange!R && isSomeString!(ElementType!R)) 372 { 373 parseIni(r, result.makeIniHandler!(ElementType!R)); 374 } 375 376 unittest 377 { 378 static struct File 379 { 380 struct S 381 { 382 string n1, n2; 383 int[string] a; 384 } 385 S s; 386 } 387 388 auto f = parseIni!File 389 ( 390 q"< 391 s.n1=v1 392 s.a.foo=1 393 [s] 394 n2=v2 395 a.bar=2 396 >".dup.splitLines() 397 ); 398 399 assert(f.s.n1=="v1"); 400 assert(f.s.n2=="v2"); 401 assert(f.s.a==["foo":1, "bar":2]); 402 } 403 404 unittest 405 { 406 static struct Custom 407 { 408 struct Section 409 { 410 string name; 411 string[string] values; 412 } 413 Section[] sections; 414 415 auto parseSection(wstring name) 416 { 417 sections.length++; 418 auto p = §ions[$-1]; 419 p.name = to!string(name); 420 return p.values.makeIniHandler!wstring(); 421 } 422 } 423 424 auto c = parseIni!Custom 425 ( 426 q"< 427 [one] 428 a=a 429 [two] 430 b=b 431 >"w.splitLines() 432 ); 433 434 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 435 } 436 437 version(unittest) static import ae.utils.aa; 438 439 unittest 440 { 441 import ae.utils.aa; 442 443 alias M = OrderedMap!(string, string); 444 static assert(isAALike!(M, string)); 445 446 auto o = parseIni!M 447 ( 448 q"< 449 b=b 450 a=a 451 >".splitLines() 452 ); 453 454 assert(o["a"]=="a" && o["b"] == "b"); 455 } 456 457 unittest 458 { 459 import ae.utils.aa; 460 461 static struct S { string x; } 462 alias M = OrderedMap!(string, S); 463 static assert(isAALike!(M, string)); 464 465 auto o = parseIni!M 466 ( 467 q"< 468 b.x=b 469 [a] 470 x=a 471 >".splitLines() 472 ); 473 474 assert(o["a"].x == "a" && o["b"].x == "b"); 475 } 476 477 unittest 478 { 479 static struct S { string x, y; } 480 481 auto r = parseIni!(S[string]) 482 ( 483 q"< 484 a.x=x 485 [a] 486 y=y 487 >".splitLines() 488 ); 489 490 assert(r["a"].x == "x" && r["a"].y == "y"); 491 } 492 493 unittest 494 { 495 static struct S { string x, y; } 496 static struct T { S* s; } 497 498 { 499 T t; 500 parseIniInto(["s.x=v"], t); 501 assert(t.s.x == "v"); 502 } 503 504 { 505 S s = {"x"}; T t = {&s}; 506 parseIniInto(["s.y=v"], t); 507 assert(s.x == "x"); 508 assert(s.y == "v"); 509 } 510 } 511 512 unittest 513 { 514 auto r = parseIni!(string[string]) 515 ( 516 q"< 517 a.b.c=d.e.f 518 >".splitLines() 519 ); 520 521 assert(r == ["a.b.c" : "d.e.f"]); 522 } 523 524 unittest 525 { 526 struct S {} 527 auto r = parseIni!(S[string]) 528 ( 529 q"< 530 [a] 531 >".splitLines() 532 ); 533 534 assert(r == ["a" : S()]); 535 } 536 537 unittest 538 { 539 struct S { Nullable!bool a, b, c; } 540 auto r = parseIni!S 541 ( 542 q"< 543 a = true 544 b = false 545 >".splitLines() 546 ); 547 548 assert( r.a.get); 549 assert(!r.b.get); 550 assert( r.c.isNull); 551 } 552 553 // *************************************************************************** 554 555 /// Utility sponge type which absorbs any INI data. 556 /// Can be later deserialized into a concrete type. 557 struct IniFragment(S) 558 { 559 S value; 560 struct Child { S name; IniFragment value; } 561 Child[] children; 562 563 IniHandler!S makeIniHandler(S_)() 564 { 565 static assert(is(S == S_), 566 "IniFragment instantiated with string type " ~ S.stringof ~ 567 ", but being parsed with string type " ~ S_.stringof); 568 return IniHandler!S( 569 (S value) 570 { 571 this.value = value; 572 }, 573 (S name) 574 { 575 children ~= Child(name); 576 return children[$-1].value.makeIniHandler!S; 577 }, 578 ); 579 } 580 581 void visit(IniHandler!S handler) const 582 { 583 if (value) 584 { 585 enforce(handler.leafHandler, "Can't set a value here"); 586 handler.leafHandler(value); 587 } 588 if (children) 589 { 590 enforce(handler.nodeHandler, "Can't use fields here"); 591 foreach (ref child; children) 592 child.value.visit(handler.nodeHandler(child.name)); 593 } 594 } 595 596 void deserializeInto(U)(ref U v) const 597 { 598 visit(v.makeIniHandler!S()); 599 } 600 } 601 602 unittest 603 { 604 static struct File 605 { 606 IniFragment!string v, a, b; 607 } 608 609 auto f = parseIni!File 610 ( 611 q"< 612 v=42 613 a.a=1 614 a.b=17 615 a.d.a=4 616 a.d.b=17 617 [b] 618 b=2 619 c=3 620 d.b=5 621 d.c=6 622 >".splitLines() 623 ); 624 625 static struct S 626 { 627 int a, b, c; 628 int[string] d; 629 } 630 631 S s; int v; 632 f.a.deserializeInto(s); 633 f.b.deserializeInto(s); 634 f.v.deserializeInto(v); 635 assert(s.a == 1); 636 assert(s.b == 2); 637 assert(s.c == 3); 638 assert(s.d == ["a":4,"b":5,"c":6]); 639 assert(v == 42); 640 } 641 642 // *************************************************************************** 643 644 deprecated alias parseStructuredIni = parseIni; 645 deprecated alias makeStructuredIniHandler = makeIniHandler; 646 647 // *************************************************************************** 648 649 /// Convenience function to load a struct from an INI file. 650 /// Returns .init if the file does not exist. 651 S loadIni(S)(string fileName) 652 { 653 S s; 654 655 import std.file; 656 if (fileName.exists) 657 s = fileName 658 .readText() 659 .splitLines() 660 .parseIni!S(); 661 662 return s; 663 } 664 665 /// As above, though loads several INI files 666 /// (duplicate values appearing in later INI files 667 /// override any values from earlier files). 668 S loadInis(S)(in char[][] fileNames) 669 { 670 S s; 671 672 import std.file; 673 s = fileNames 674 .map!(fileName => 675 fileName.exists ? 676 fileName 677 .readText() 678 .splitLines() 679 : 680 null 681 ) 682 .joiner(["[]"]) 683 .parseIni!S(); 684 685 return s; 686 } 687 688 // *************************************************************************** 689 690 /// Simple convenience formatter for writing INI files. 691 struct IniWriter(O) 692 { 693 O writer; 694 695 void startSection(string name) 696 { 697 writer.put('[', name, "]\n"); 698 } 699 700 void writeValue(string name, string value) 701 { 702 writer.put(name, '=', value, '\n'); 703 } 704 } 705 706 /// Insert a blank line before each section 707 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); } 708 709 // *************************************************************************** 710 711 /// Walks a data structure and calls visitor with each field and its path. 712 template visitWithPath(alias visitor, S = string) 713 { 714 void visitWithPath(U)(S[] path, ref U v) 715 { 716 static if (isAALike!(U, S)) 717 foreach (ref vk, ref vv; v) 718 visitWithPath(path ~ vk.to!S, vv); 719 else 720 static if (is(U == struct)) 721 { 722 foreach (i, ref field; v.tupleof) 723 { 724 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 725 visitWithPath(path ~ fieldName, field); 726 } 727 } 728 else 729 static if (is(U V : V*)) 730 { 731 if (v) 732 visitWithPath(path, *v); 733 } 734 else 735 static if (is(typeof(v.to!S()))) 736 visitor(path, v is U.init ? null : v.to!S().nonNull); 737 else 738 static assert(false, "Can't serialize " ~ U.stringof); 739 } 740 } 741 742 /// Formats a data structure as a structured .ini file. 743 S formatIni(S = string, T)( 744 auto ref T value, 745 size_t delegate(S[] path) getSectionLength = (S[] path) => path.length > 1 ? 1 : 0) 746 { 747 IniWriter!(FastAppender!(typeof(S.init[0]))) writer; 748 S[] lastSection; 749 void visitor(S[] path, S value) 750 { 751 if (!value) 752 return; 753 auto sectionLength = getSectionLength(path); 754 if (sectionLength == 0 && lastSection.length != 0) 755 sectionLength = 1; // can't go back to top-level after starting a section 756 enforce(sectionLength < path.length, "Bad section length"); 757 auto section = path[0 .. sectionLength]; 758 if (section != lastSection) 759 { 760 writer.startSection(section.join(".")); 761 lastSection = section; 762 } 763 auto subPath = path[sectionLength .. $]; 764 writer.writeValue(subPath.join("."), value); 765 } 766 767 visitWithPath!(visitor, S)(null, value); 768 return writer.writer.get().prettifyIni; 769 } 770 771 unittest 772 { 773 struct S { int i; S* next; } 774 assert(formatIni(S(1, new S(2))) == q"EOF 775 i=1 776 777 [next] 778 i=2 779 EOF"); 780 } 781 782 unittest 783 { 784 assert(formatIni(["one" : 1]) == q"EOF 785 one=1 786 EOF"); 787 } 788 789 unittest 790 { 791 assert(formatIni(["one" : 1]) == q"EOF 792 one=1 793 EOF"); 794 } 795 796 /** 797 Adds or updates a value in an INI file. 798 799 If the value is already in the INI file, then it is updated 800 in-place; otherwise, a new one is added to the matching section. 801 Setting value to null removes the line if it is present. 802 803 Whitespace and comments on other lines are preserved. 804 805 Params: 806 lines = INI file lines (as in parseIni) 807 name = fully-qualified name of the value to update 808 (use `.` to specify section path) 809 value = new value to write 810 */ 811 812 void updateIni(S)(ref S[] lines, S name, S value) 813 { 814 size_t valueLine = size_t.max; 815 S valueLineSection; 816 817 S currentSection = null; 818 auto pathPrefix() { return chain(currentSection, repeat(typeof(name[0])('.'), currentSection is null ? 0 : 1)); } 819 820 size_t bestSectionEnd; 821 S bestSection; 822 bool inBestSection = true; 823 824 foreach (i, line; lines) 825 { 826 auto lex = lexIniLine(line); 827 final switch (lex.type) 828 { 829 case lex.Type.empty: 830 break; 831 case lex.Type.value: 832 if (equal(chain(pathPrefix, lex.name), name)) 833 { 834 valueLine = i; 835 valueLineSection = currentSection; 836 } 837 break; 838 case lex.type.section: 839 if (inBestSection) 840 bestSectionEnd = i; 841 inBestSection = false; 842 843 currentSection = lex.name; 844 if (name.startsWith(pathPrefix) && currentSection.length > bestSection.length) 845 { 846 bestSection = currentSection; 847 inBestSection = true; 848 } 849 break; 850 } 851 } 852 853 if (inBestSection) 854 bestSectionEnd = lines.length; 855 856 if (value) 857 { 858 S genLine(S section) { return name[section.length ? section.length + 1 : 0 .. $] ~ '=' ~ value; } 859 860 if (valueLine != size_t.max) 861 lines[valueLine] = genLine(valueLineSection); 862 else 863 lines = lines[0..bestSectionEnd] ~ genLine(bestSection) ~ lines[bestSectionEnd..$]; 864 } 865 else 866 if (valueLine != size_t.max) 867 lines = lines[0..valueLine] ~ lines[valueLine+1..$]; 868 } 869 870 unittest 871 { 872 auto ini = q"< 873 a=1 874 a=2 875 >".splitLines(); 876 updateIni(ini, "a", "3"); 877 struct S { int a; } 878 assert(parseIni!S(ini).a == 3); 879 } 880 881 unittest 882 { 883 auto ini = q"< 884 a=1 885 [s] 886 a=2 887 [t] 888 a=3 889 >".strip.splitLines.map!strip.array; 890 updateIni(ini, "a", "4"); 891 updateIni(ini, "s.a", "5"); 892 updateIni(ini, "t.a", "6"); 893 assert(equal(ini, q"< 894 a=4 895 [s] 896 a=5 897 [t] 898 a=6 899 >".strip.splitLines.map!strip), text(ini)); 900 } 901 902 unittest 903 { 904 auto ini = q"< 905 [s] 906 [t] 907 >".strip.splitLines.map!strip.array; 908 updateIni(ini, "a", "1"); 909 updateIni(ini, "s.a", "2"); 910 updateIni(ini, "t.a", "3"); 911 assert(equal(ini, q"< 912 a=1 913 [s] 914 a=2 915 [t] 916 a=3 917 >".strip.splitLines.map!strip)); 918 } 919 920 unittest 921 { 922 auto ini = q"< 923 a=1 924 b=2 925 >".strip.splitLines.map!strip.array; 926 updateIni(ini, "a", null); 927 assert(equal(ini, q"< 928 b=2 929 >".strip.splitLines.map!strip)); 930 } 931 932 void updateIniFile(S)(string fileName, S name, S value) 933 { 934 import std.file, std.stdio, std.utf; 935 auto lines = fileName.exists ? fileName.readText.splitLines : null; 936 updateIni(lines, name, value); 937 lines.map!(l => chain(l.byCodeUnit, only(typeof(S.init[0])('\n')))).joiner.toFile(fileName); 938 } 939 940 unittest 941 { 942 import std.file; 943 enum fn = "temp.ini"; 944 std.file.write(fn, "a=b\n"); 945 scope(exit) remove(fn); 946 updateIniFile(fn, "a", "c"); 947 assert(read(fn) == "a=c\n"); 948 } 949 950 /// Updates an entire INI file. 951 /// Like formatIni, but tries to preserve 952 /// existing field order and comments. 953 void updateIni(S, T)(ref S[] lines, auto ref T value) 954 { 955 T oldValue = parseIni!T(lines); 956 S[S[]] oldIni; 957 void oldVisitor(S[] path, S value) 958 { 959 if (value) 960 oldIni[path.idup] = value; 961 } 962 visitWithPath!(oldVisitor, S)(null, oldValue); 963 964 void visitor(S[] path, S value) 965 { 966 if (oldIni.get(path, null) != value) 967 updateIni(lines, path.join('.'), value); 968 } 969 970 visitWithPath!(visitor, S)(null, value); 971 } 972 973 unittest 974 { 975 struct S { int a, b, c; } 976 auto ini = q"< 977 b=2 978 c=3 979 >".strip.splitLines.map!strip.array; 980 updateIni(ini, S(1, 2)); 981 assert(ini == [ 982 "b=2", 983 "a=1", 984 ]); 985 }