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