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 = &sections[$-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 }