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 }