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