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