1 /**
2  * Class representation of ini-like file.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2015-2016
7  * License:
8  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
9  * See_Also:
10  *  $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
11  */
12 
13 module inilike.file;
14 
15 private {
16     import std.exception;
17     import inilike.common;
18 }
19 
20 public import inilike.range;
21 
22 private @trusted string makeComment(string line) pure nothrow
23 {
24     if (line.length && line[$-1] == '\n') {
25         line = line[0..$-1];
26     }
27     if (!line.isComment && line.length) {
28         line = '#' ~ line;
29     }
30     line = line.replace("\n", " ");
31     return line;
32 }
33 
34 private @trusted IniLikeLine makeCommentLine(string line) pure nothrow
35 {
36     return IniLikeLine.fromComment(makeComment(line));
37 }
38 
39 /**
40  * Container used internally by $(D IniLikeFile) and $(D IniLikeGroup).
41  * Technically this is list with optional value access by key.
42  */
43 struct ListMap(K,V, size_t chunkSize = 32)
44 {
45     ///
46     @disable this(this);
47 
48     /**
49      * Insert key-value pair to the front of list.
50      * Returns: Inserted node.
51      */
52     Node* insertFront(K key, V value) {
53         Node* newNode = givePlace(key, value);
54         putToFront(newNode);
55         return newNode;
56     }
57 
58     /**
59      * Insert key-value pair to the back of list.
60      * Returns: Inserted node.
61      */
62     Node* insertBack(K key, V value) {
63         Node* newNode = givePlace(key, value);
64         putToBack(newNode);
65         return newNode;
66     }
67 
68     /**
69      * Insert key-value pair before some node in the list.
70      * Returns: Inserted node.
71      */
72     Node* insertBefore(Node* node, K key, V value) {
73         Node* newNode = givePlace(key, value);
74         putBefore(node, newNode);
75         return newNode;
76     }
77 
78     /**
79      * Insert key-value pair after some node in the list.
80      * Returns: Inserted node.
81      */
82     Node* insertAfter(Node* node, K key, V value) {
83         Node* newNode = givePlace(key, value);
84         putAfter(node, newNode);
85         return newNode;
86     }
87 
88     /**
89      * Add value at the start of list.
90      * Returns: Inserted node.
91      */
92     Node* prepend(V value) {
93         Node* newNode = givePlace(value);
94         putToFront(newNode);
95         return newNode;
96     }
97 
98     /**
99      * Add value at the end of list.
100      * Returns: Inserted node.
101      */
102     Node* append(V value) {
103         Node* newNode = givePlace(value);
104         putToBack(newNode);
105         return newNode;
106     }
107 
108     /**
109      * Add value before some node in the list.
110      * Returns: Inserted node.
111      */
112     Node* addBefore(Node* node, V value) {
113         Node* newNode = givePlace(value);
114         putBefore(node, newNode);
115         return newNode;
116     }
117 
118     /**
119      * Add value after some node in the list.
120      * Returns: Inserted node.
121      */
122     Node* addAfter(Node* node, V value) {
123         Node* newNode = givePlace(value);
124         putAfter(node, newNode);
125         return newNode;
126     }
127 
128     /**
129      * Move node to the front of list.
130      */
131     void moveToFront(Node* toMove)
132     {
133         if (_head is toMove) {
134             return;
135         }
136 
137         pullOut(toMove);
138         putToFront(toMove);
139     }
140 
141     /**
142      * Move node to the back of list.
143      */
144     void moveToBack(Node* toMove)
145     {
146         if (_tail is toMove) {
147             return;
148         }
149 
150         pullOut(toMove);
151         putToBack(toMove);
152     }
153 
154     /**
155      * Move node to the location before other node.
156      */
157     void moveBefore(Node* other, Node* toMove) {
158         if (other is toMove) {
159             return;
160         }
161 
162         pullOut(toMove);
163         putBefore(other, toMove);
164     }
165 
166     /**
167      * Move node to the location after other node.
168      */
169     void moveAfter(Node* other, Node* toMove) {
170         if (other is toMove) {
171             return;
172         }
173 
174         pullOut(toMove);
175         putAfter(other, toMove);
176     }
177 
178     /**
179      * Remove node from list. It also becomes unaccessible via key lookup.
180      */
181     void remove(Node* toRemove)
182     {
183         pullOut(toRemove);
184 
185         if (toRemove.hasKey()) {
186             _dict.remove(toRemove.key);
187         }
188 
189         if (_lastEmpty) {
190             _lastEmpty.next = toRemove;
191         }
192         toRemove.prev = _lastEmpty;
193         _lastEmpty = toRemove;
194     }
195 
196     /**
197      * Remove value by key.
198      * Returns: true if node with such key was found and removed. False otherwise.
199      */
200     bool remove(K key) {
201         Node** toRemove = key in _dict;
202         if (toRemove) {
203             remove(*toRemove);
204             return true;
205         }
206         return false;
207     }
208 
209     /**
210      * Remove the first node.
211      */
212     void removeFront() {
213         remove(_head);
214     }
215 
216     /**
217      * Remove the last node.
218      */
219     void removeBack() {
220         remove(_tail);
221     }
222 
223     /**
224      * Get list node by key.
225      * Returns: Found Node or null if container does not have node associated with key.
226      */
227     inout(Node)* getNode(K key) inout {
228         auto toReturn = key in _dict;
229         if (toReturn) {
230             return *toReturn;
231         }
232         return null;
233     }
234 
235     private static struct ByNode(NodeType)
236     {
237     private:
238         NodeType* _begin;
239         NodeType* _end;
240 
241     public:
242         bool empty() const {
243             return _begin is null || _end is null || _begin.prev is _end || _end.next is _begin;
244         }
245 
246         auto front() {
247             return _begin;
248         }
249 
250         auto back() {
251             return _end;
252         }
253 
254         void popFront() {
255             _begin = _begin.next;
256         }
257 
258         void popBack() {
259             _end = _end.prev;
260         }
261 
262         @property auto save() {
263             return this;
264         }
265     }
266 
267     /**
268      * Iterate over list nodes.
269      * See_Also: $(D byEntry)
270      */
271     auto byNode()
272     {
273         return ByNode!Node(_head, _tail);
274     }
275 
276     ///ditto
277     auto byNode() const
278     {
279         return ByNode!(const(Node))(_head, _tail);
280     }
281 
282     /**
283      * Iterate over nodes mapped to Entry elements (useful for testing).
284      */
285     auto byEntry() const {
286         import std.algorithm : map;
287         return byNode().map!(node => node.toEntry());
288     }
289 
290     /**
291      * Represenation of list node.
292      */
293     static struct Node {
294     private:
295         K _key;
296         V _value;
297         bool _hasKey;
298         Node* _prev;
299         Node* _next;
300 
301         @trusted this(K key, V value) pure nothrow {
302             _key = key;
303             _value = value;
304             _hasKey = true;
305         }
306 
307         @trusted this(V value) pure nothrow {
308             _value = value;
309             _hasKey = false;
310         }
311 
312         @trusted void prev(Node* newPrev) pure nothrow {
313             _prev = newPrev;
314         }
315 
316         @trusted void next(Node* newNext) pure nothrow {
317             _next = newNext;
318         }
319 
320     public:
321         /**
322          * Get stored value.
323          */
324         @trusted inout(V) value() inout pure nothrow {
325             return _value;
326         }
327 
328         /**
329          * Set stored value.
330          */
331         @trusted void value(V newValue) pure nothrow {
332             _value = newValue;
333         }
334 
335         /**
336          * Tell whether this node is a key-value node.
337          */
338         @trusted bool hasKey() const pure nothrow {
339             return _hasKey;
340         }
341 
342         /**
343          * Key in key-value node.
344          */
345         @trusted auto key() const pure nothrow {
346             return _key;
347         }
348 
349         /**
350          * Access previous node in the list.
351          */
352         @trusted inout(Node)* prev() inout pure nothrow {
353             return _prev;
354         }
355 
356         /**
357          * Access next node in the list.
358          */
359         @trusted inout(Node)* next() inout pure nothrow {
360             return _next;
361         }
362 
363         ///
364         auto toEntry() const {
365             static if (is(V == class)) {
366                 alias Rebindable!(const(V)) T;
367                 if (hasKey()) {
368                     return Entry!T(_key, rebindable(_value));
369                 } else {
370                     return Entry!T(rebindable(_value));
371                 }
372 
373             } else {
374                 alias V T;
375 
376                 if (hasKey()) {
377                     return Entry!T(_key, _value);
378                 } else {
379                     return Entry!T(_value);
380                 }
381             }
382         }
383     }
384 
385     /// Mapping of Node to structure.
386     static struct Entry(T = V)
387     {
388     private:
389         K _key;
390         T _value;
391         bool _hasKey;
392 
393     public:
394         ///
395         this(T value) {
396             _value = value;
397             _hasKey = false;
398         }
399 
400         ///
401         this(K key, T value) {
402             _key = key;
403             _value = value;
404             _hasKey = true;
405         }
406 
407         ///
408         auto value() inout {
409             return _value;
410         }
411 
412         ///
413         auto key() const {
414             return _key;
415         }
416 
417         ///
418         bool hasKey() const {
419             return _hasKey;
420         }
421     }
422 
423 private:
424     void putToFront(Node* toPut)
425     in {
426         assert(toPut !is null);
427     }
428     body {
429         if (_head) {
430             _head.prev = toPut;
431             toPut.next = _head;
432             _head = toPut;
433         } else {
434             _head = toPut;
435             _tail = toPut;
436         }
437     }
438 
439     void putToBack(Node* toPut)
440     in {
441         assert(toPut !is null);
442     }
443     body {
444         if (_tail) {
445             _tail.next = toPut;
446             toPut.prev = _tail;
447             _tail = toPut;
448         } else {
449             _tail = toPut;
450             _head = toPut;
451         }
452     }
453 
454     void putBefore(Node* node, Node* toPut)
455     in {
456         assert(toPut !is null);
457         assert(node !is null);
458     }
459     body {
460         toPut.prev = node.prev;
461         if (toPut.prev) {
462             toPut.prev.next = toPut;
463         }
464         toPut.next = node;
465         node.prev = toPut;
466 
467         if (node is _head) {
468             _head = toPut;
469         }
470     }
471 
472     void putAfter(Node* node, Node* toPut)
473     in {
474         assert(toPut !is null);
475         assert(node !is null);
476     }
477     body {
478         toPut.next = node.next;
479         if (toPut.next) {
480             toPut.next.prev = toPut;
481         }
482         toPut.prev = node;
483         node.next = toPut;
484 
485         if (node is _tail) {
486             _tail = toPut;
487         }
488     }
489 
490     void pullOut(Node* node)
491     in {
492         assert(node !is null);
493     }
494     body {
495         if (node.next) {
496             node.next.prev = node.prev;
497         }
498         if (node.prev) {
499             node.prev.next = node.next;
500         }
501 
502         if (node is _head) {
503             _head = node.next;
504         }
505         if (node is _tail) {
506             _tail = node.prev;
507         }
508 
509         node.next = null;
510         node.prev = null;
511     }
512 
513     Node* givePlace(K key, V value) {
514         auto newNode = Node(key, value);
515         return givePlace(newNode);
516     }
517 
518     Node* givePlace(V value) {
519         auto newNode = Node(value);
520         return givePlace(newNode);
521     }
522 
523     Node* givePlace(ref Node node) {
524         Node* toReturn;
525         if (_lastEmpty is null) {
526             if (_storageSize < _storage.length) {
527                 toReturn = &_storage[_storageSize];
528             } else {
529                 size_t storageIndex = (_storageSize - chunkSize) / chunkSize;
530                 if (storageIndex >= _additonalStorages.length) {
531                     _additonalStorages ~= (Node[chunkSize]).init;
532                 }
533 
534                 size_t index = (_storageSize - chunkSize) % chunkSize;
535                 toReturn = &_additonalStorages[storageIndex][index];
536             }
537 
538             _storageSize++;
539         } else {
540             toReturn = _lastEmpty;
541             _lastEmpty = _lastEmpty.prev;
542             if (_lastEmpty) {
543                 _lastEmpty.next = null;
544             }
545             toReturn.next = null;
546             toReturn.prev = null;
547         }
548 
549         toReturn._hasKey = node._hasKey;
550         toReturn._key = node._key;
551         toReturn._value = node._value;
552 
553         if (toReturn.hasKey()) {
554             _dict[toReturn.key] = toReturn;
555         }
556         return toReturn;
557     }
558 
559     Node[chunkSize] _storage;
560     Node[chunkSize][] _additonalStorages;
561     size_t _storageSize;
562 
563     Node* _tail;
564     Node* _head;
565     Node* _lastEmpty;
566     Node*[K] _dict;
567 }
568 
569 unittest
570 {
571     import std.range : isBidirectionalRange;
572     ListMap!(string, string) listMap;
573     static assert(isBidirectionalRange!(typeof(listMap.byNode())));
574 }
575 
576 unittest
577 {
578     import std.algorithm : equal;
579     import std.range : ElementType;
580 
581     alias ListMap!(string, string, 2) TestListMap;
582 
583     TestListMap listMap;
584     alias typeof(listMap).Node Node;
585     alias ElementType!(typeof(listMap.byEntry())) Entry;
586 
587     assert(listMap.byEntry().empty);
588     assert(listMap.getNode("Nonexistent") is null);
589 
590     listMap.insertFront("Start", "Fast");
591     assert(listMap.getNode("Start") !is null);
592     assert(listMap.getNode("Start").key() == "Start");
593     assert(listMap.getNode("Start").value() == "Fast");
594     assert(listMap.getNode("Start").hasKey());
595     assert(listMap.byEntry().equal([Entry("Start", "Fast")]));
596     assert(listMap.remove("Start"));
597     assert(listMap.byEntry().empty);
598     assert(listMap.getNode("Start") is null);
599 
600     listMap.insertBack("Finish", "Bad");
601     assert(listMap.byEntry().equal([Entry("Finish", "Bad")]));
602     assert(listMap.getNode("Finish").value() == "Bad");
603 
604     listMap.insertFront("Begin", "Good");
605     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad")]));
606     assert(listMap.getNode("Begin").value() == "Good");
607 
608     listMap.insertFront("Start", "Slow");
609     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
610 
611     listMap.insertAfter(listMap.getNode("Begin"), "Middle", "Person");
612     assert(listMap.getNode("Middle").value() == "Person");
613     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Middle", "Person"), Entry("Finish", "Bad")]));
614 
615     listMap.insertBefore(listMap.getNode("Middle"), "Mean", "Man");
616     assert(listMap.getNode("Mean").value() == "Man");
617     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Mean", "Man"), Entry("Middle", "Person"), Entry("Finish", "Bad")]));
618 
619     assert(listMap.remove("Mean"));
620     assert(listMap.remove("Middle"));
621 
622     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
623 
624     listMap.insertFront("New", "Era");
625     assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")]));
626 
627     listMap.insertBack("Old", "Epoch");
628     assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch")]));
629 
630     listMap.moveToBack(listMap.getNode("New"));
631     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")]));
632 
633     listMap.moveToFront(listMap.getNode("Begin"));
634     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")]));
635 
636     listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Start"));
637     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era")]));
638 
639     listMap.moveBefore(listMap.getNode("Finish"), listMap.getNode("Old"));
640     assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("New", "Era")]));
641 
642     listMap.moveBefore(listMap.getNode("Begin"), listMap.getNode("Start"));
643     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("New", "Era")]));
644 
645     listMap.moveAfter(listMap.getNode("New"), listMap.getNode("Finish"));
646     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")]));
647 
648     listMap.getNode("Begin").value = "Evil";
649     assert(listMap.getNode("Begin").value() == "Evil");
650 
651     listMap.remove("Begin");
652     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")]));
653     listMap.remove("Old");
654     listMap.remove("New");
655     assert(!listMap.remove("Begin"));
656 
657     Node* shebang = listMap.prepend("Shebang");
658     Node* endOfStory = listMap.append("End of story");
659 
660     assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("End of story")]));
661 
662     Node* mid = listMap.addAfter(listMap.getNode("Start"), "Mid");
663     Node* average = listMap.addBefore(listMap.getNode("Finish"), "Average");
664     assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")]));
665 
666     listMap.remove(shebang);
667     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")]));
668 
669     listMap.remove(endOfStory);
670     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
671 
672     listMap.moveToFront(listMap.getNode("Start"));
673     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
674     listMap.moveToBack(listMap.getNode("Finish"));
675     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
676 
677     listMap.moveBefore(listMap.getNode("Start"), listMap.getNode("Start"));
678     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
679     listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Finish"));
680     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")]));
681 
682     listMap.insertAfter(mid, "Center", "Universe");
683     listMap.insertBefore(average, "Focus", "Cosmos");
684     assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")]));
685 
686     listMap.removeFront();
687     assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")]));
688     listMap.removeBack();
689 
690     assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
691 
692     assert(listMap.byEntry().retro.equal([Entry("Average"), Entry("Focus", "Cosmos"), Entry("Center", "Universe"), Entry("Mid")]));
693 
694     auto byEntry = listMap.byEntry();
695     Entry entry = byEntry.front;
696     assert(entry.value == "Mid");
697     assert(!entry.hasKey());
698 
699     byEntry.popFront();
700     assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
701     byEntry.popBack();
702     assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")]));
703 
704     entry = byEntry.back;
705     assert(entry.key == "Focus");
706     assert(entry.value == "Cosmos");
707     assert(entry.hasKey());
708 
709     auto saved = byEntry.save;
710 
711     byEntry.popFront();
712     assert(byEntry.equal([Entry("Focus", "Cosmos")]));
713     byEntry.popBack();
714     assert(byEntry.empty);
715 
716     assert(saved.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")]));
717     saved.popBack();
718     assert(saved.equal([Entry("Center", "Universe")]));
719     saved.popFront();
720     assert(saved.empty);
721 
722     static void checkConst(ref const TestListMap listMap)
723     {
724         assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")]));
725     }
726     checkConst(listMap);
727 
728     static class Class
729     {
730         this(string name) {
731             _name = name;
732         }
733 
734         string name() const {
735             return _name;
736         }
737     private:
738         string _name;
739     }
740 
741     alias ListMap!(string, Class) TestClassListMap;
742     TestClassListMap classListMap;
743     classListMap.insertFront("name", new Class("Name"));
744     classListMap.append(new Class("Value"));
745     auto byClass = classListMap.byEntry();
746     assert(byClass.front.value.name == "Name");
747     assert(byClass.front.key == "name");
748     assert(byClass.back.value.name == "Value");
749 }
750 
751 /**
752  * Line in group.
753  */
754 struct IniLikeLine
755 {
756     /**
757      * Type of line.
758      */
759     enum Type
760     {
761         None = 0,   /// deleted or invalid line
762         Comment = 1, /// a comment or empty line
763         KeyValue = 2 /// key-value pair
764     }
765 
766     /**
767      * Contruct from comment.
768      */
769     @nogc @safe static IniLikeLine fromComment(string comment) nothrow pure {
770         return IniLikeLine(comment, null, Type.Comment);
771     }
772 
773     /**
774      * Construct from key and value.
775      */
776     @nogc @safe static IniLikeLine fromKeyValue(string key, string value) nothrow pure {
777         return IniLikeLine(key, value, Type.KeyValue);
778     }
779 
780     /**
781      * Get comment.
782      * Returns: Comment or empty string if type is not Type.Comment.
783      */
784     @nogc @safe string comment() const nothrow pure {
785         return _type == Type.Comment ? _first : null;
786     }
787 
788     /**
789      * Get key.
790      * Returns: Key or empty string if type is not Type.KeyValue
791      */
792     @nogc @safe string key() const nothrow pure {
793         return _type == Type.KeyValue ? _first : null;
794     }
795 
796     /**
797      * Get value.
798      * Returns: Value or empty string if type is not Type.KeyValue
799      */
800     @nogc @safe string value() const nothrow pure {
801         return _type == Type.KeyValue ? _second : null;
802     }
803 
804     /**
805      * Get type of line.
806      */
807     @nogc @safe Type type() const nothrow pure {
808         return _type;
809     }
810 private:
811     string _first;
812     string _second;
813     Type _type = Type.None;
814 }
815 
816 
817 /**
818  * This class represents the group (section) in the ini-like file.
819  * Instances of this class can be created only in the context of $(D IniLikeFile) or its derivatives.
820  * Note: Keys are case-sensitive.
821  */
822 class IniLikeGroup
823 {
824 private:
825     alias ListMap!(string, IniLikeLine) LineListMap;
826 
827 public:
828     ///
829     enum InvalidKeyPolicy : ubyte {
830         ///Throw error on invalid key
831         throwError,
832         ///Skip invalid key
833         skip,
834         ///Save entry with invalid key.
835         save
836     }
837 
838     /**
839      * Create instance on IniLikeGroup and set its name to groupName.
840      */
841     protected @nogc @safe this(string groupName) nothrow {
842         _name = groupName;
843     }
844 
845     /**
846      * Returns: The value associated with the key.
847      * Note: The value is not unescaped automatically.
848      * Prerequisites: Value accessed by key must exist.
849      * See_Also: $(D value), $(D readEntry)
850      */
851     @nogc @safe final string opIndex(string key) const nothrow pure {
852         return _listMap.getNode(key).value.value;
853     }
854 
855     private @safe final string setKeyValueImpl(string key, string value)
856     in {
857         assert(!value.needEscaping);
858     }
859     body {
860         import std.stdio;
861         auto node = _listMap.getNode(key);
862         if (node) {
863             node.value = IniLikeLine.fromKeyValue(key, value);
864         } else {
865             _listMap.insertBack(key, IniLikeLine.fromKeyValue(key, value));
866         }
867         return value;
868     }
869 
870     /**
871      * Insert new value or replaces the old one if value associated with key already exists.
872      * Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
873      * Returns: Inserted/updated value or null string if key was not added.
874      * Throws: $(D IniLikeEntryException) if key or value is not valid or value needs to be escaped.
875      * See_Also: $(D writeEntry)
876      */
877     @safe final string opIndexAssign(string value, string key) {
878         return setValue(key, value);
879     }
880 
881     /**
882      * Assign localized value.
883      * Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
884      * See_Also: $(D setLocalizedValue), $(D localizedValue), $(D writeEntry)
885      */
886     @safe final string opIndexAssign(string value, string key, string locale) {
887         return setLocalizedValue(key, locale, value);
888     }
889 
890     /**
891      * Tell if group contains value associated with the key.
892      */
893     @nogc @safe final bool contains(string key) const nothrow pure {
894         return _listMap.getNode(key) !is null;
895     }
896 
897     /**
898      * Get value by key.
899      * Returns: The value associated with the key, or defaultValue if group does not contain such item.
900      * Note: The value is not unescaped automatically.
901      * See_Also: $(D setValue), $(D localizedValue), $(D readEntry)
902      */
903     @nogc @safe final string value(string key) const nothrow pure {
904         auto node = _listMap.getNode(key);
905         if (node) {
906             return node.value.value;
907         } else {
908             return null;
909         }
910     }
911 
912     private @trusted final bool validateKeyValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy)
913     {
914         validateValue(key, value);
915 
916         try {
917             validateKey(key, value);
918             return true;
919         } catch(IniLikeEntryException e) {
920             final switch(invalidKeyPolicy) {
921                 case InvalidKeyPolicy.throwError:
922                     throw e;
923                 case InvalidKeyPolicy.save:
924                     validateKeyImpl(key, value, _name);
925                     return true;
926                 case InvalidKeyPolicy.skip:
927                     validateKeyImpl(key, value, _name);
928                     return false;
929             }
930         }
931     }
932 
933     /**
934      * Set value associated with key.
935      * Params:
936      *  key = Key to associate value with.
937      *  value = Value to set.
938      *  invalidKeyPolicy = Policyt about invalid keys.
939      * See_Also: $(D value), $(D setLocalizedValue), $(D writeEntry)
940      */
941     @safe final string setValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError)
942     {
943         if (validateKeyValue(key, value, invalidKeyPolicy)) {
944             return setKeyValueImpl(key, value);
945         }
946         return null;
947     }
948 
949     /**
950      * Get value by key. This function automatically unescape the found value before returning.
951      * Returns: The unescaped value associated with key or null if not found.
952      * See_Also: $(D value), $(D writeEntry)
953      */
954     @safe final string readEntry(string key, string locale = null) const nothrow pure {
955         if (locale.length) {
956             return localizedValue(key, locale).unescapeValue();
957         } else {
958             return value(key).unescapeValue();
959         }
960     }
961 
962     /**
963      * Set value by key. This function automatically escape the value (you should not escape value yourself) when writing it.
964      * Throws: $(D IniLikeEntryException) if key or value is not valid.
965      * See_Also: $(D readEntry), $(D setValue)
966      */
967     @safe final string writeEntry(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
968         value = value.escapeValue();
969         return setValue(key, value, invalidKeyPolicy);
970     }
971 
972     ///ditto, localized version
973     @safe final string writeEntry(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
974         value = value.escapeValue();
975         return setLocalizedValue(key, locale, value, invalidKeyPolicy);
976     }
977 
978     /**
979      * Perform locale matching lookup as described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys).
980      * Params:
981      *  key = Non-localized key.
982      *  locale = Locale in intereset.
983      *  nonLocaleFallback = Allow fallback to non-localized version.
984      * Returns:
985      *  The localized value associated with key and locale,
986      *  or the value associated with non-localized key if group does not contain localized value and nonLocaleFallback is true.
987      * Note: The value is not unescaped automatically.
988      * See_Also: $(D setLocalizedValue), $(D value), $(D readEntry)
989      */
990     @safe final string localizedValue(string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure {
991         //Any ideas how to get rid of this boilerplate and make less allocations?
992         const t = parseLocaleName(locale);
993         auto lang = t.lang;
994         auto country = t.country;
995         auto modifier = t.modifier;
996 
997         if (lang.length) {
998             string pick;
999             if (country.length && modifier.length) {
1000                 pick = value(localizedKey(key, locale));
1001                 if (pick !is null) {
1002                     return pick;
1003                 }
1004             }
1005             if (country.length) {
1006                 pick = value(localizedKey(key, lang, country));
1007                 if (pick !is null) {
1008                     return pick;
1009                 }
1010             }
1011             if (modifier.length) {
1012                 pick = value(localizedKey(key, lang, string.init, modifier));
1013                 if (pick !is null) {
1014                     return pick;
1015                 }
1016             }
1017             pick = value(localizedKey(key, lang, string.init));
1018             if (pick !is null) {
1019                 return pick;
1020             }
1021         }
1022 
1023         if (nonLocaleFallback) {
1024             return value(key);
1025         } else {
1026             return null;
1027         }
1028     }
1029 
1030     ///
1031     unittest
1032     {
1033         auto lilf = new IniLikeFile;
1034         lilf.addGenericGroup("Entry");
1035         auto group = lilf.group("Entry");
1036         assert(group.groupName == "Entry");
1037         group["Name"] = "Programmer";
1038         group["Name[ru_RU]"] = "Разработчик";
1039         group["Name[ru@jargon]"] = "Кодер";
1040         group["Name[ru]"] = "Программист";
1041         group["Name[de_DE@dialect]"] = "Programmierer"; //just example
1042         group["Name[fr_FR]"] = "Programmeur";
1043         group["GenericName"] = "Program";
1044         group["GenericName[ru]"] = "Программа";
1045         assert(group["Name"] == "Programmer");
1046         assert(group.localizedValue("Name", "ru@jargon") == "Кодер");
1047         assert(group.localizedValue("Name", "ru_RU@jargon") == "Разработчик");
1048         assert(group.localizedValue("Name", "ru") == "Программист");
1049         assert(group.localizedValue("Name", "ru_RU.UTF-8") == "Разработчик");
1050         assert(group.localizedValue("Name", "nonexistent locale") == "Programmer");
1051         assert(group.localizedValue("Name", "de_DE@dialect") == "Programmierer");
1052         assert(group.localizedValue("Name", "fr_FR.UTF-8") == "Programmeur");
1053         assert(group.localizedValue("GenericName", "ru_RU") == "Программа");
1054         assert(group.localizedValue("GenericName", "fr_FR") == "Program");
1055         assert(group.localizedValue("GenericName", "fr_FR", No.nonLocaleFallback) is null);
1056     }
1057 
1058     /**
1059      * Same as localized version of opIndexAssign, but uses function syntax.
1060      * Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
1061      * Throws: $(D IniLikeEntryException) if key or value is not valid or value needs to be escaped.
1062      * See_Also: $(D localizedValue), $(D setValue), $(D writeEntry)
1063      */
1064     @safe final string setLocalizedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1065         return setValue(localizedKey(key, locale), value, invalidKeyPolicy);
1066     }
1067 
1068     /**
1069      * Removes entry by key. Do nothing if not value associated with key found.
1070      * Returns: true if entry was removed, false otherwise.
1071      */
1072     @safe final bool removeEntry(string key) nothrow pure {
1073         return _listMap.remove(key);
1074     }
1075 
1076     ///ditto, but remove entry by localized key
1077     @safe final bool removeEntry(string key, string locale) nothrow pure {
1078         return removeEntry(localizedKey(key, locale));
1079     }
1080 
1081     ///ditto, but remove entry by node.
1082     @safe final void removeEntry(LineNode node) nothrow pure {
1083         _listMap.remove(node.node);
1084     }
1085 
1086     private @nogc @safe static auto staticByKeyValue(Range)(Range nodes) nothrow {
1087         return nodes.map!(node => node.value).filter!(v => v.type == IniLikeLine.Type.KeyValue).map!(v => keyValueTuple(v.key, v.value));
1088     }
1089 
1090     /**
1091      * Iterate by Key-Value pairs. Values are left in escaped form.
1092      * Returns: Range of Tuple!(string, "key", string, "value").
1093      * See_Also: $(D value), $(D localizedValue), $(D byIniLine)
1094      */
1095     @nogc @safe final auto byKeyValue() const nothrow {
1096         return staticByKeyValue(_listMap.byNode);
1097     }
1098 
1099     /**
1100      * Empty range of the same type as byKeyValue. Can be used in derived classes if it's needed to have empty range.
1101      * Returns: Empty range of Tuple!(string, "key", string, "value").
1102      */
1103     @nogc @safe static auto emptyByKeyValue() nothrow {
1104         const ListMap!(string, IniLikeLine) listMap;
1105         return staticByKeyValue(listMap.byNode);
1106     }
1107 
1108     ///
1109     unittest
1110     {
1111         assert(emptyByKeyValue().empty);
1112         auto group = new IniLikeGroup("Group name");
1113         static assert(is(typeof(emptyByKeyValue()) == typeof(group.byKeyValue()) ));
1114     }
1115 
1116     /**
1117      * Get name of this group.
1118      * Returns: The name of this group.
1119      */
1120     @nogc @safe final string groupName() const nothrow pure {
1121         return _name;
1122     }
1123 
1124     /**
1125      * Returns: Range of $(D IniLikeLine)s included in this group.
1126      * See_Also: $(D byNode), $(D byKeyValue)
1127      */
1128     @trusted final auto byIniLine() const {
1129         return _listMap.byNode.map!(node => node.value);
1130     }
1131 
1132     /**
1133      * Wrapper for internal ListMap node.
1134      */
1135     static struct LineNode
1136     {
1137     private:
1138         LineListMap.Node* node;
1139         string groupName;
1140     public:
1141         /**
1142          * Get key of node.
1143          */
1144         @nogc @trusted string key() const pure nothrow {
1145             if (node) {
1146                 return node.key;
1147             } else {
1148                 return null;
1149             }
1150         }
1151 
1152         /**
1153          * Get $(D IniLikeLine) pointed by node.
1154          */
1155         @nogc @trusted IniLikeLine line() const pure nothrow {
1156             if (node) {
1157                 return node.value;
1158             } else {
1159                 return IniLikeLine.init;
1160             }
1161         }
1162 
1163         /**
1164          * Set value for line. If underline line is comment, than newValue is set as comment.
1165          * Prerequisites: Node must be non-null.
1166          */
1167         @trusted void setValue(string newValue) pure {
1168             auto type = node.value.type;
1169             if (type == IniLikeLine.Type.KeyValue) {
1170                 node.value = IniLikeLine.fromKeyValue(node.value.key, newValue);
1171             } else if (type == IniLikeLine.Type.Comment) {
1172                 node.value = makeCommentLine(newValue);
1173             }
1174         }
1175 
1176         /**
1177          * Check if underlined node is null.
1178          */
1179         @nogc @safe bool isNull() const pure nothrow {
1180             return node is null;
1181         }
1182     }
1183 
1184     private @trusted auto lineNode(LineListMap.Node* node) pure nothrow {
1185         return LineNode(node, groupName());
1186     }
1187 
1188     /**
1189      * Iterate over nodes of internal list.
1190      * See_Also: $(D getNode), $(D byIniLine)
1191      */
1192     @trusted auto byNode() {
1193         import std.algorithm : map;
1194         return _listMap.byNode().map!(node => lineNode(node));
1195     }
1196 
1197     /**
1198      * Get internal list node for key.
1199      * See_Also: $(D byNode)
1200      */
1201     @trusted final auto getNode(string key) {
1202         return lineNode(_listMap.getNode(key));
1203     }
1204 
1205     /**
1206      * Add key-value entry without association of value with key. Can be used to add duplicates.
1207      */
1208     final auto appendValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) {
1209         if (validateKeyValue(key, value, invalidKeyPolicy)) {
1210             return lineNode(_listMap.append(IniLikeLine.fromKeyValue(key, value)));
1211         } else {
1212             return lineNode(null);
1213         }
1214     }
1215 
1216     /**
1217      * Add comment line into the group.
1218      * Returns: Added LineNode.
1219      * See_Also: $(D byIniLine), $(D prependComment), $(D addCommentBefore), $(D addCommentAfter)
1220      */
1221     @safe final auto appendComment(string comment) nothrow pure {
1222         return lineNode(_listMap.append(makeCommentLine(comment)));
1223     }
1224 
1225     /**
1226      * Add comment line at the start of group (after group header, before any key-value pairs).
1227      * Returns: Added LineNode.
1228      * See_Also: $(D byIniLine), $(D appendComment), $(D addCommentBefore), $(D addCommentAfter)
1229      */
1230     @safe final auto prependComment(string comment) nothrow pure {
1231         return lineNode(_listMap.prepend(makeCommentLine(comment)));
1232     }
1233 
1234     /**
1235      * Add comment before some node.
1236      * Returns: Added LineNode.
1237      * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentAfter)
1238      */
1239     @trusted final auto addCommentBefore(LineNode node, string comment) nothrow pure
1240     in {
1241         assert(!node.isNull());
1242     }
1243     body {
1244         return _listMap.addBefore(node.node, makeCommentLine(comment));
1245     }
1246 
1247     /**
1248      * Add comment after some node.
1249      * Returns: Added LineNode.
1250      * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentBefore)
1251      */
1252     @trusted final auto addCommentAfter(LineNode node, string comment) nothrow pure
1253     in {
1254         assert(!node.isNull());
1255     }
1256     body {
1257         return _listMap.addAfter(node.node, makeCommentLine(comment));
1258     }
1259 
1260     /**
1261      * Move line to the start of group.
1262      * Prerequisites: $(D toMove) is not null and belongs to this group.
1263      * See_Also: $(D getNode)
1264      */
1265     @trusted final void moveLineToFront(LineNode toMove) nothrow pure {
1266         _listMap.moveToFront(toMove.node);
1267     }
1268 
1269     /**
1270      * Move line to the end of group.
1271      * Prerequisites: $(D toMove) is not null and belongs to this group.
1272      * See_Also: $(D getNode)
1273      */
1274     @trusted final void moveLineToBack(LineNode toMove) nothrow pure {
1275         _listMap.moveToBack(toMove.node);
1276     }
1277 
1278     /**
1279      * Move line before other line in the group.
1280      * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group.
1281      * See_Also: $(D getNode)
1282      */
1283     @trusted final void moveLineBefore(LineNode other, LineNode toMove) nothrow pure {
1284         _listMap.moveBefore(other.node, toMove.node);
1285     }
1286 
1287     /**
1288      * Move line after other line in the group.
1289      * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group.
1290      * See_Also: $(D getNode)
1291      */
1292     @trusted final void moveLineAfter(LineNode other, LineNode toMove) nothrow pure {
1293         _listMap.moveAfter(other.node, toMove.node);
1294     }
1295 
1296 private:
1297     @trusted static void validateKeyImpl(string key, string value, string groupName)
1298     {
1299         if (key.empty || key.strip.empty) {
1300             throw new IniLikeEntryException("key must not be empty", groupName, key, value);
1301         }
1302         if (key.isComment()) {
1303             throw new IniLikeEntryException("key must not start with #", groupName, key, value);
1304         }
1305         if (key.canFind('=')) {
1306             throw new IniLikeEntryException("key must not have '=' character in it", groupName, key, value);
1307         }
1308         if (key.needEscaping()) {
1309             throw new IniLikeEntryException("key must not contain new line characters", groupName, key, value);
1310         }
1311     }
1312 
1313 protected:
1314     /**
1315      * Validate key before setting value to key for this group and throw exception if not valid.
1316      * Can be reimplemented in derived classes.
1317      *
1318      * Default implementation checks if key is not empty string, does not look like comment and does not contain new line or carriage return characters.
1319      * Params:
1320      *  key = key to validate.
1321      *  value = value that is being set to key.
1322      * Throws: $(D IniLikeEntryException) if either key is invalid.
1323      * See_Also: $(D validateValue)
1324      * Note:
1325      *  Implementer should ensure that their implementation still validates key for format consistency (i.e. no new line characters, etc.).
1326      *  If not sure, just call super.validateKey(key, value) in your implementation.
1327      */
1328     @trusted void validateKey(string key, string value) const {
1329         validateKeyImpl(key, value, _name);
1330     }
1331 
1332     ///
1333     unittest
1334     {
1335         auto ilf = new IniLikeFile();
1336         ilf.addGenericGroup("Group");
1337 
1338         auto entryException = collectException!IniLikeEntryException(ilf.group("Group")[""] = "Value1");
1339         assert(entryException !is null);
1340         assert(entryException.groupName == "Group");
1341         assert(entryException.key == "");
1342         assert(entryException.value == "Value1");
1343 
1344         entryException = collectException!IniLikeEntryException(ilf.group("Group")["    "] = "Value2");
1345         assert(entryException !is null);
1346         assert(entryException.key == "    ");
1347         assert(entryException.value == "Value2");
1348 
1349         entryException = collectException!IniLikeEntryException(ilf.group("Group")["New\nLine"] = "Value3");
1350         assert(entryException !is null);
1351         assert(entryException.key == "New\nLine");
1352         assert(entryException.value == "Value3");
1353 
1354         entryException = collectException!IniLikeEntryException(ilf.group("Group")["# Comment"] = "Value4");
1355         assert(entryException !is null);
1356         assert(entryException.key == "# Comment");
1357         assert(entryException.value == "Value4");
1358 
1359         entryException = collectException!IniLikeEntryException(ilf.group("Group")["Everyone=Is"] = "Equal");
1360         assert(entryException !is null);
1361         assert(entryException.key == "Everyone=Is");
1362         assert(entryException.value == "Equal");
1363     }
1364 
1365     /**
1366      * Validate value for key before setting value to key for this group and throw exception if not valid.
1367      * Can be reimplemented in derived classes.
1368      *
1369      * Default implementation checks if value is escaped.
1370      * Params:
1371      *  key = key the value is being set to.
1372      *  value = value to validate. Considered to be escaped.
1373      * Throws: $(D IniLikeEntryException) if value is invalid.
1374      * See_Also: $(D validateKey)
1375      */
1376     @trusted void validateValue(string key, string value) const {
1377         if (value.needEscaping()) {
1378             throw new IniLikeEntryException("The value needs to be escaped", _name, key, value);
1379         }
1380     }
1381 
1382     ///
1383     unittest
1384     {
1385         auto ilf = new IniLikeFile();
1386         ilf.addGenericGroup("Group");
1387 
1388         auto entryException = collectException!IniLikeEntryException(ilf.group("Group")["Key"] = "New\nline");
1389         assert(entryException !is null);
1390         assert(entryException.key == "Key");
1391         assert(entryException.value == "New\nline");
1392     }
1393 private:
1394     LineListMap _listMap;
1395     string _name;
1396 }
1397 
1398 ///Base class for ini-like format errors.
1399 class IniLikeException : Exception
1400 {
1401     ///
1402     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
1403         super(msg, file, line, next);
1404     }
1405 }
1406 
1407 /**
1408  * Exception thrown on error with group.
1409  */
1410 class IniLikeGroupException : Exception
1411 {
1412     ///
1413     this(string msg, string groupName, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
1414         super(msg, file, line, next);
1415         _group = groupName;
1416     }
1417 
1418     /**
1419      * Name of group where error occured.
1420      */
1421     @nogc @safe string groupName() const nothrow pure {
1422         return _group;
1423     }
1424 
1425 private:
1426     string _group;
1427 }
1428 
1429 /**
1430  * Exception thrown when trying to set invalid key or value.
1431  */
1432 class IniLikeEntryException : IniLikeGroupException
1433 {
1434     this(string msg, string group, string key, string value, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
1435         super(msg, group, file, line, next);
1436         _key = key;
1437         _value = value;
1438     }
1439 
1440     /**
1441      * The key the value associated with.
1442      */
1443     @nogc @safe string key() const nothrow pure {
1444         return _key;
1445     }
1446 
1447     /**
1448      * The value associated with key.
1449      */
1450     @nogc @safe string value() const nothrow pure {
1451         return _value;
1452     }
1453 
1454 private:
1455     string _key;
1456     string _value;
1457 }
1458 
1459 /**
1460  * Exception thrown on the file read error.
1461  */
1462 class IniLikeReadException : IniLikeException
1463 {
1464     /**
1465      * Create IniLikeReadException with msg, lineNumber and fileName.
1466      */
1467     this(string msg, size_t lineNumber, string fileName = null, IniLikeEntryException entryException = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
1468         super(msg, file, line, next);
1469         _lineNumber = lineNumber;
1470         _fileName = fileName;
1471         _entryException = entryException;
1472     }
1473 
1474     /**
1475      * Number of line in the file where the exception occured, starting from 1.
1476      * 0 means that error is not bound to any existing line, but instead relate to file at whole (e.g. required group or key is missing).
1477      * Don't confuse with $(B line) property of $(B Throwable).
1478      */
1479     @nogc @safe size_t lineNumber() const nothrow pure {
1480         return _lineNumber;
1481     }
1482 
1483     /**
1484      * Number of line in the file where the exception occured, starting from 0.
1485      * Don't confuse with $(B line) property of $(B Throwable).
1486      */
1487     @nogc @safe size_t lineIndex() const nothrow pure {
1488         return _lineNumber ? _lineNumber - 1 : 0;
1489     }
1490 
1491     /**
1492      * Name of ini-like file where error occured.
1493      * Can be empty if fileName was not given upon IniLikeFile creating.
1494      * Don't confuse with $(B file) property of $(B Throwable).
1495      */
1496     @nogc @safe string fileName() const nothrow pure {
1497         return _fileName;
1498     }
1499 
1500     /**
1501      * Original IniLikeEntryException which caused this error.
1502      * This will have the same msg.
1503      * Returns: $(D IniLikeEntryException) object or null if the cause of error was something else.
1504      */
1505     @nogc @safe IniLikeEntryException entryException() nothrow pure {
1506         return _entryException;
1507     }
1508 
1509 private:
1510     size_t _lineNumber;
1511     string _fileName;
1512     IniLikeEntryException _entryException;
1513 }
1514 
1515 /**
1516  * Ini-like file.
1517  *
1518  */
1519 class IniLikeFile
1520 {
1521 private:
1522     alias ListMap!(string, IniLikeGroup, 8) GroupListMap;
1523 public:
1524     ///Behavior on duplicate key in the group.
1525     enum DuplicateKeyPolicy : ubyte
1526     {
1527         ///Throw error on entry with duplicate key.
1528         throwError,
1529         ///Skip duplicate without error.
1530         skip,
1531         ///Preserve all duplicates in the list. The first found value remains accessible by key.
1532         preserve
1533     }
1534 
1535     ///Behavior on group with duplicate name in the file.
1536     enum DuplicateGroupPolicy : ubyte
1537     {
1538         ///Throw error on group with duplicate name.
1539         throwError,
1540         ///Skip duplicate without error.
1541         skip,
1542         ///Preserve all duplicates in the list. The first found group remains accessible by key.
1543         preserve
1544     }
1545 
1546     ///Behavior of ini-like file reading.
1547     static struct ReadOptions
1548     {
1549         ///Behavior on groups with duplicate names.
1550         DuplicateGroupPolicy duplicateGroupPolicy = DuplicateGroupPolicy.throwError;
1551         ///Behavior on duplicate keys.
1552         DuplicateKeyPolicy duplicateKeyPolicy = DuplicateKeyPolicy.throwError;
1553 
1554         ///Behavior on invalid keys.
1555         IniLikeGroup.InvalidKeyPolicy invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.throwError;
1556 
1557         ///Whether to preserve comments on reading.
1558         Flag!"preserveComments" preserveComments = Yes.preserveComments;
1559 
1560         ///Setting parameters in any order, leaving not mentioned ones in default state.
1561         @nogc @safe this(Args...)(Args args) nothrow pure {
1562             foreach(arg; args) {
1563                 assign(arg);
1564             }
1565         }
1566 
1567         ///
1568         unittest
1569         {
1570             ReadOptions readOptions;
1571 
1572             readOptions = ReadOptions(No.preserveComments);
1573             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.throwError);
1574             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.throwError);
1575             assert(!readOptions.preserveComments);
1576 
1577             readOptions = ReadOptions(DuplicateGroupPolicy.skip, DuplicateKeyPolicy.preserve);
1578             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.skip);
1579             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.preserve);
1580             assert(readOptions.preserveComments);
1581 
1582             const duplicateGroupPolicy = DuplicateGroupPolicy.preserve;
1583             immutable duplicateKeyPolicy = DuplicateKeyPolicy.skip;
1584             const preserveComments = No.preserveComments;
1585             readOptions = ReadOptions(duplicateGroupPolicy, IniLikeGroup.InvalidKeyPolicy.skip, preserveComments, duplicateKeyPolicy);
1586             assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.preserve);
1587             assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.skip);
1588             assert(readOptions.invalidKeyPolicy == IniLikeGroup.InvalidKeyPolicy.skip);
1589         }
1590 
1591         /**
1592          * Assign arg to the struct member of corresponding type.
1593          * Note:
1594          *  It's compile-time error to assign parameter of type which is not part of ReadOptions.
1595          */
1596         @nogc @safe void assign(T)(T arg) nothrow pure {
1597             alias Unqual!(T) ArgType;
1598             static if (is(ArgType == DuplicateKeyPolicy)) {
1599                 duplicateKeyPolicy = arg;
1600             } else static if (is(ArgType == DuplicateGroupPolicy)) {
1601                 duplicateGroupPolicy = arg;
1602             } else static if (is(ArgType == Flag!"preserveComments")) {
1603                 preserveComments = arg;
1604             } else static if (is(ArgType == IniLikeGroup.InvalidKeyPolicy)) {
1605                 invalidKeyPolicy = arg;
1606             } else {
1607                 static assert(false, "Unknown argument type " ~ typeof(arg).stringof);
1608             }
1609         }
1610     }
1611 
1612     ///
1613     unittest
1614     {
1615         string contents = `# The first comment
1616 [First Entry]
1617 # Comment
1618 GenericName=File manager
1619 GenericName[ru]=Файловый менеджер
1620 # Another comment
1621 [Another Group]
1622 Name=Commander
1623 # The last comment`;
1624 
1625         alias IniLikeFile.ReadOptions ReadOptions;
1626         alias IniLikeFile.DuplicateKeyPolicy DuplicateKeyPolicy;
1627         alias IniLikeFile.DuplicateGroupPolicy DuplicateGroupPolicy;
1628 
1629         IniLikeFile ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(No.preserveComments));
1630         assert(!ilf.readOptions().preserveComments);
1631         assert(ilf.leadingComments().empty);
1632         assert(equal(
1633             ilf.group("First Entry").byIniLine(),
1634             [IniLikeLine.fromKeyValue("GenericName", "File manager"), IniLikeLine.fromKeyValue("GenericName[ru]", "Файловый менеджер")]
1635         ));
1636         assert(equal(
1637             ilf.group("Another Group").byIniLine(),
1638             [IniLikeLine.fromKeyValue("Name", "Commander")]
1639         ));
1640 
1641         contents = `[Group]
1642 Duplicate=First
1643 Key=Value
1644 Duplicate=Second`;
1645 
1646         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.skip));
1647         assert(equal(
1648             ilf.group("Group").byIniLine(),
1649             [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value")]
1650         ));
1651 
1652         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.preserve));
1653         assert(equal(
1654             ilf.group("Group").byIniLine(),
1655             [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromKeyValue("Duplicate", "Second")]
1656         ));
1657         assert(ilf.group("Group").value("Duplicate") == "First");
1658 
1659         contents = `[Duplicate]
1660 Key=First
1661 [Group]
1662 [Duplicate]
1663 Key=Second`;
1664 
1665         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.preserve));
1666         auto byGroup = ilf.byGroup();
1667         assert(byGroup.front["Key"] == "First");
1668         assert(byGroup.back["Key"] == "Second");
1669 
1670         auto byNode = ilf.byNode();
1671         assert(byNode.front.group.groupName == "Duplicate");
1672         assert(byNode.front.key == "Duplicate");
1673         assert(byNode.back.key is null);
1674 
1675         contents = `[Duplicate]
1676 Key=First
1677 [Group]
1678 [Duplicate]
1679 Key=Second`;
1680 
1681         ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.skip));
1682         auto byGroup2 = ilf.byGroup();
1683         assert(byGroup2.front["Key"] == "First");
1684         assert(byGroup2.back.groupName == "Group");
1685     }
1686 
1687     /**
1688      * Behavior of ini-like file saving.
1689      * See_Also: $(D save)
1690      */
1691     static struct WriteOptions
1692     {
1693         ///Whether to preserve comments (lines that starts with '#') on saving.
1694         Flag!"preserveComments" preserveComments = Yes.preserveComments;
1695         ///Whether to preserve empty lines on saving.
1696         Flag!"preserveEmptyLines" preserveEmptyLines = Yes.preserveEmptyLines;
1697         /**
1698          * Whether to write empty line after each group except for the last.
1699          * New line is not written when it already exists before the next group.
1700          */
1701         Flag!"lineBetweenGroups" lineBetweenGroups = No.lineBetweenGroups;
1702 
1703         /**
1704          * Pretty mode. Save comments, skip existing new lines, add line before the next group.
1705          */
1706         @nogc @safe static auto pretty() nothrow pure {
1707             return WriteOptions(Yes.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups);
1708         }
1709 
1710         /**
1711          * Exact mode. Save all comments and empty lines as is.
1712          */
1713         @nogc @safe static auto exact() nothrow pure {
1714             return WriteOptions(Yes.preserveComments, Yes.preserveEmptyLines, No.lineBetweenGroups);
1715         }
1716 
1717         @nogc @safe this(Args...)(Args args) nothrow pure {
1718             foreach(arg; args) {
1719                 assign(arg);
1720             }
1721         }
1722 
1723         /**
1724          * Assign arg to the struct member of corresponding type.
1725          * Note:
1726          *  It's compile-time error to assign parameter of type which is not part of WriteOptions.
1727          */
1728         @nogc @safe void assign(T)(T arg) nothrow pure {
1729             alias Unqual!(T) ArgType;
1730             static if (is(ArgType == Flag!"preserveEmptyLines")) {
1731                 preserveEmptyLines = arg;
1732             } else static if (is(ArgType == Flag!"lineBetweenGroups")) {
1733                 lineBetweenGroups = arg;
1734             } else static if (is(ArgType == Flag!"preserveComments")) {
1735                 preserveComments = arg;
1736             } else {
1737                 static assert(false, "Unknown argument type " ~ typeof(arg).stringof);
1738             }
1739         }
1740     }
1741 
1742     /**
1743      * Wrapper for internal $(D ListMap) node.
1744      */
1745     static struct GroupNode
1746     {
1747     private:
1748         GroupListMap.Node* node;
1749     public:
1750         /**
1751          * Key the group associated with.
1752          * While every group has groupName, it might be added to the group list without association, therefore will not have key.
1753          */
1754         @nogc @trusted string key() const pure nothrow {
1755             if (node) {
1756                 return node.key();
1757             } else {
1758                 return null;
1759             }
1760         }
1761 
1762         /**
1763          * Access underlined group.
1764          */
1765         @nogc @trusted IniLikeGroup group() pure nothrow {
1766             if (node) {
1767                 return node.value();
1768             } else {
1769                 return null;
1770             }
1771         }
1772 
1773         /**
1774          * Check if underlined node is null.
1775          */
1776         @nogc @safe bool isNull() pure nothrow const {
1777             return node is null;
1778         }
1779     }
1780 
1781 protected:
1782     /**
1783      * Insert group into $(D IniLikeFile) object and use its name as key.
1784      * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object.
1785      */
1786     @trusted final auto insertGroup(IniLikeGroup group)
1787     in {
1788         assert(group !is null);
1789     }
1790     body {
1791         return GroupNode(_listMap.insertBack(group.groupName, group));
1792     }
1793 
1794     /**
1795      * Append group to group list without associating group name with it. Can be used to add groups with duplicated names.
1796      * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object.
1797      */
1798     @trusted final auto putGroup(IniLikeGroup group)
1799     in {
1800         assert(group !is null);
1801     }
1802     body {
1803         return GroupNode(_listMap.append(group));
1804     }
1805 
1806     /**
1807      * Add comment before groups.
1808      * This function is called only in constructor and can be reimplemented in derived classes.
1809      * Params:
1810      *  comment = Comment line to add.
1811      */
1812     @trusted void onLeadingComment(string comment) {
1813         if (_readOptions.preserveComments) {
1814             appendLeadingComment(comment);
1815         }
1816     }
1817 
1818     /**
1819      * Add comment for group.
1820      * This function is called only in constructor and can be reimplemented in derived classes.
1821      * Params:
1822      *  comment = Comment line to add.
1823      *  currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded)
1824      *  groupName = The name of the currently parsed group. Set even if currentGroup is null.
1825      * See_Also: $(D createGroup), $(D IniLikeGroup.appendComment)
1826      */
1827     @trusted void onCommentInGroup(string comment, IniLikeGroup currentGroup, string groupName)
1828     {
1829         if (currentGroup && _readOptions.preserveComments) {
1830             currentGroup.appendComment(comment);
1831         }
1832     }
1833 
1834     /**
1835      * Add key/value pair for group.
1836      * This function is called only in constructor and can be reimplemented in derived classes.
1837      * Params:
1838      *  key = Key to insert or set.
1839      *  value = Value to set for key.
1840      *  currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded)
1841      *  groupName = The name of the currently parsed group. Set even if currentGroup is null.
1842      * See_Also: $(D createGroup)
1843      */
1844     @trusted void onKeyValue(string key, string value, IniLikeGroup currentGroup, string groupName)
1845     {
1846         if (currentGroup) {
1847             if (currentGroup.contains(key)) {
1848                 final switch(_readOptions.duplicateKeyPolicy) {
1849                     case DuplicateKeyPolicy.throwError:
1850                         throw new IniLikeEntryException("key already exists", groupName, key, value);
1851                     case DuplicateKeyPolicy.skip:
1852                         break;
1853                     case DuplicateKeyPolicy.preserve:
1854                         currentGroup.appendValue(key, value, _readOptions.invalidKeyPolicy);
1855                         break;
1856                 }
1857             } else {
1858                 currentGroup.setValue(key, value, _readOptions.invalidKeyPolicy);
1859             }
1860         }
1861     }
1862 
1863     /**
1864      * Create $(D IniLikeGroup) by groupName during file parsing.
1865      *
1866      * This function can be reimplemented in derived classes,
1867      * e.g. to insert additional checks or create specific derived class depending on groupName.
1868      * Returned value is later passed to $(D onCommentInGroup) and $(D onKeyValue) methods as currentGroup.
1869      * Reimplemented method also is allowed to return null.
1870      * Default implementation just returns empty $(D IniLikeGroup) with name set to groupName.
1871      * Throws:
1872      *  $(D IniLikeGroupException) if group with such name already exists.
1873      *  $(D IniLikeException) if groupName is empty.
1874      * See_Also:
1875      *  $(D onKeyValue), $(D onCommentInGroup)
1876      */
1877     @trusted IniLikeGroup onGroup(string groupName) {
1878         if (group(groupName) !is null) {
1879             final switch(_readOptions.duplicateGroupPolicy) {
1880                 case DuplicateGroupPolicy.throwError:
1881                     throw new IniLikeGroupException("group with such name already exists", groupName);
1882                 case DuplicateGroupPolicy.skip:
1883                     return null;
1884                 case DuplicateGroupPolicy.preserve:
1885                     auto toPut = createGroupByName(groupName);
1886                     if (toPut) {
1887                         putGroup(toPut);
1888                     }
1889                     return toPut;
1890             }
1891         } else {
1892             auto toInsert = createGroupByName(groupName);
1893             if (toInsert) {
1894                 insertGroup(toInsert);
1895             }
1896             return toInsert;
1897         }
1898     }
1899 
1900     /**
1901      * Reimplement in derive class.
1902      */
1903     @trusted IniLikeGroup createGroupByName(string groupName) {
1904         return createEmptyGroup(groupName);
1905     }
1906 
1907     /**
1908      * Can be used in derived classes to create instance of IniLikeGroup.
1909      * Throws: $(D IniLikeException) if groupName is empty.
1910      */
1911     @safe static createEmptyGroup(string groupName) {
1912         if (groupName.length == 0) {
1913             throw new IniLikeException("empty group name");
1914         }
1915         return new IniLikeGroup(groupName);
1916     }
1917 public:
1918     /**
1919      * Construct empty $(D IniLikeFile), i.e. without any groups or values
1920      */
1921     @nogc @safe this() nothrow {
1922 
1923     }
1924 
1925     /**
1926      * Read from file.
1927      * Throws:
1928      *  $(B ErrnoException) if file could not be opened.
1929      *  $(D IniLikeReadException) if error occured while reading the file.
1930      */
1931     @trusted this(string fileName, ReadOptions readOptions = ReadOptions.init) {
1932         this(iniLikeFileReader(fileName), fileName, readOptions);
1933     }
1934 
1935     /**
1936      * Read from range of $(D inilike.range.IniLikeReader).
1937      * Note: All exceptions thrown within constructor are turning into $(D IniLikeReadException).
1938      * Throws:
1939      *  $(D IniLikeReadException) if error occured while parsing.
1940      */
1941     this(IniLikeReader)(IniLikeReader reader, string fileName = null, ReadOptions readOptions = ReadOptions.init)
1942     {
1943         _readOptions = readOptions;
1944         size_t lineNumber = 0;
1945         IniLikeGroup currentGroup;
1946 
1947         version(DigitalMars) {
1948             static void foo(size_t ) {}
1949         }
1950 
1951         try {
1952             foreach(line; reader.byLeadingLines)
1953             {
1954                 lineNumber++;
1955                 if (line.isComment || line.strip.empty) {
1956                     onLeadingComment(line);
1957                 } else {
1958                     throw new IniLikeException("Expected comment or empty line before any group");
1959                 }
1960             }
1961 
1962             foreach(g; reader.byGroup)
1963             {
1964                 lineNumber++;
1965                 string groupName = g.groupName;
1966 
1967                 version(DigitalMars) {
1968                     foo(lineNumber); //fix dmd codgen bug with -O
1969                 }
1970 
1971                 currentGroup = onGroup(groupName);
1972 
1973                 foreach(line; g.byEntry)
1974                 {
1975                     lineNumber++;
1976 
1977                     if (line.isComment || line.strip.empty) {
1978                         onCommentInGroup(line, currentGroup, groupName);
1979                     } else {
1980                         const t = parseKeyValue(line);
1981 
1982                         string key = t.key.stripRight;
1983                         string value = t.value.stripLeft;
1984 
1985                         if (key.length == 0 && value.length == 0) {
1986                             throw new IniLikeException("Expected comment, empty line or key value inside group");
1987                         } else {
1988                             onKeyValue(key, value, currentGroup, groupName);
1989                         }
1990                     }
1991                 }
1992             }
1993 
1994             _fileName = fileName;
1995 
1996         }
1997         catch(IniLikeEntryException e) {
1998             throw new IniLikeReadException(e.msg, lineNumber, fileName, e, e.file, e.line, e.next);
1999         }
2000         catch (Exception e) {
2001             throw new IniLikeReadException(e.msg, lineNumber, fileName, null, e.file, e.line, e.next);
2002         }
2003     }
2004 
2005     /**
2006      * Get group by name.
2007      * Returns: $(D IniLikeGroup) instance associated with groupName or null if not found.
2008      * See_Also: $(D byGroup)
2009      */
2010     @nogc @safe final inout(IniLikeGroup) group(string groupName) nothrow inout pure {
2011         auto pick = _listMap.getNode(groupName);
2012         if (pick) {
2013             return pick.value;
2014         }
2015         return null;
2016     }
2017 
2018     /**
2019      * Get $(D GroupNode) by groupName.
2020      */
2021     @nogc @safe final auto getNode(string groupName) nothrow pure {
2022         return GroupNode(_listMap.getNode(groupName));
2023     }
2024 
2025     /**
2026      * Create new group using groupName.
2027      * Returns: Newly created instance of $(D IniLikeGroup).
2028      * Throws:
2029      *  $(D IniLikeGroupException) if group with such name already exists.
2030      *  $(D IniLikeException) if groupName is empty.
2031      * See_Also: $(D removeGroup), $(D group)
2032      */
2033     @safe final IniLikeGroup addGenericGroup(string groupName) {
2034         if (group(groupName) !is null) {
2035             throw new IniLikeGroupException("group already exists", groupName);
2036         }
2037         auto toReturn = createEmptyGroup(groupName);
2038         insertGroup(toReturn);
2039         return toReturn;
2040     }
2041 
2042     /**
2043      * Remove group by name. Do nothing if group with such name does not exist.
2044      * Returns: true if group was deleted, false otherwise.
2045      * See_Also: $(D addGenericGroup), $(D group)
2046      */
2047     @safe bool removeGroup(string groupName) nothrow {
2048         return _listMap.remove(groupName);
2049     }
2050 
2051     /**
2052      * Range of groups in order how they were defined in file.
2053      * See_Also: $(D group)
2054      */
2055     @nogc @safe final auto byGroup() inout nothrow {
2056         return _listMap.byNode().map!(node => node.value);
2057     }
2058 
2059     /**
2060      * Iterate over $(D GroupNode)s.
2061      */
2062     @nogc @safe final auto byNode() nothrow {
2063         return _listMap.byNode().map!(node => GroupNode(node));
2064     }
2065 
2066     /**
2067      * Save object to the file using .ini-like format.
2068      * Throws: $(D ErrnoException) if the file could not be opened or an error writing to the file occured.
2069      * See_Also: $(D saveToString), $(D save)
2070      */
2071     @trusted final void saveToFile(string fileName, const WriteOptions options = WriteOptions.exact) const {
2072         import std.stdio : File;
2073 
2074         auto f = File(fileName, "w");
2075         void dg(in string line) {
2076             f.writeln(line);
2077         }
2078         save(&dg, options);
2079     }
2080 
2081     /**
2082      * Save object to string using .ini like format.
2083      * Returns: A string that represents the contents of file.
2084      * Note: The resulting string differs from the contents that would be written to file via $(D saveToFile)
2085      * in the way it does not add new line character at the end of the last line.
2086      * See_Also: $(D saveToFile), $(D save)
2087      */
2088     @trusted final string saveToString(const WriteOptions options = WriteOptions.exact) const {
2089         auto a = appender!(string[])();
2090         save(a, options);
2091         return a.data.join("\n");
2092     }
2093 
2094     ///
2095     unittest
2096     {
2097         string contents =
2098 `
2099 # Leading comment
2100 [First group]
2101 # Comment inside
2102 Key=Value
2103 [Second group]
2104 
2105 Key=Value
2106 
2107 [Third group]
2108 Key=Value`;
2109 
2110         auto ilf = new IniLikeFile(iniLikeStringReader(contents));
2111         assert(ilf.saveToString(WriteOptions.exact) == contents);
2112 
2113         assert(ilf.saveToString(WriteOptions.pretty) ==
2114 `# Leading comment
2115 [First group]
2116 # Comment inside
2117 Key=Value
2118 
2119 [Second group]
2120 Key=Value
2121 
2122 [Third group]
2123 Key=Value`);
2124 
2125         assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines)) ==
2126 `[First group]
2127 Key=Value
2128 [Second group]
2129 Key=Value
2130 [Third group]
2131 Key=Value`);
2132 
2133         assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups)) ==
2134 `[First group]
2135 Key=Value
2136 
2137 [Second group]
2138 Key=Value
2139 
2140 [Third group]
2141 Key=Value`);
2142     }
2143 
2144     /**
2145      * Use Output range or delegate to retrieve strings line by line.
2146      * Those strings can be written to the file or be showed in text area.
2147      * Note: Output strings don't have trailing newline character.
2148      * See_Also: $(D saveToFile), $(D saveToString)
2149      */
2150     final void save(OutRange)(OutRange sink, const WriteOptions options = WriteOptions.exact) const if (isOutputRange!(OutRange, string)) {
2151         foreach(line; leadingComments()) {
2152             if (options.preserveComments) {
2153                 if (line.empty && !options.preserveEmptyLines) {
2154                     continue;
2155                 }
2156                 put(sink, line);
2157             }
2158         }
2159         bool firstGroup = true;
2160         bool lastWasEmpty = false;
2161 
2162         foreach(group; byGroup()) {
2163             if (!firstGroup && !lastWasEmpty && options.lineBetweenGroups) {
2164                 put(sink, "");
2165             }
2166 
2167             put(sink, "[" ~ group.groupName ~ "]");
2168             foreach(line; group.byIniLine()) {
2169                 lastWasEmpty = false;
2170                 if (line.type == IniLikeLine.Type.Comment) {
2171                     if (!options.preserveComments) {
2172                         continue;
2173                     }
2174                     if (line.comment.empty) {
2175                         if (!options.preserveEmptyLines) {
2176                             continue;
2177                         }
2178                         lastWasEmpty = true;
2179                     }
2180                     put(sink, line.comment);
2181                 } else if (line.type == IniLikeLine.Type.KeyValue) {
2182                     put(sink, line.key ~ "=" ~ line.value);
2183                 }
2184             }
2185             firstGroup = false;
2186         }
2187     }
2188 
2189     /**
2190      * File path where the object was loaded from.
2191      * Returns: File name as was specified on the object creation.
2192      */
2193     @nogc @safe final string fileName() nothrow const pure {
2194         return _fileName;
2195     }
2196 
2197     /**
2198      * Leading comments.
2199      * Returns: Range of leading comments (before any group)
2200      * See_Also: $(D appendLeadingComment), $(D prependLeadingComment), $(D clearLeadingComments)
2201      */
2202     @nogc @safe final auto leadingComments() const nothrow pure {
2203         return _leadingComments;
2204     }
2205 
2206     ///
2207     unittest
2208     {
2209         auto ilf = new IniLikeFile();
2210         assert(ilf.appendLeadingComment("First") == "#First");
2211         assert(ilf.appendLeadingComment("#Second") == "#Second");
2212         assert(ilf.appendLeadingComment("Sneaky\nKey=Value") == "#Sneaky Key=Value");
2213         assert(ilf.appendLeadingComment("# New Line\n") == "# New Line");
2214         assert(ilf.appendLeadingComment("") == "");
2215         assert(ilf.appendLeadingComment("\n") == "");
2216         assert(ilf.prependLeadingComment("Shebang") == "#Shebang");
2217         assert(ilf.leadingComments().equal(["#Shebang", "#First", "#Second", "#Sneaky Key=Value", "# New Line", "", ""]));
2218         ilf.clearLeadingComments();
2219         assert(ilf.leadingComments().empty);
2220     }
2221 
2222     /**
2223      * Add leading comment. This will be appended to the list of leadingComments.
2224      * Note: # will be prepended automatically if line is not empty and does not have # at the start.
2225      *  The last new line character will be removed if present. Others will be replaced with whitespaces.
2226      * Returns: Line that was added as comment.
2227      * See_Also: $(D leadingComments), $(D prependLeadingComment)
2228      */
2229     @safe final string appendLeadingComment(string line) nothrow pure {
2230         line = makeComment(line);
2231         _leadingComments ~= line;
2232         return line;
2233     }
2234 
2235     /**
2236      * Prepend leading comment (e.g. for setting shebang line).
2237      * Returns: Line that was added as comment.
2238      * See_Also: $(D leadingComments), $(D appendLeadingComment)
2239      */
2240     @safe final string prependLeadingComment(string line) nothrow pure {
2241         line = makeComment(line);
2242         _leadingComments = line ~ _leadingComments;
2243         return line;
2244     }
2245 
2246     /**
2247      * Remove all coments met before groups.
2248      * See_Also: $(D leadingComments)
2249      */
2250     @nogc final @safe void clearLeadingComments() nothrow {
2251         _leadingComments = null;
2252     }
2253 
2254     /**
2255      * Move the group to make it the first.
2256      */
2257     @trusted final void moveGroupToFront(GroupNode toMove) nothrow pure {
2258         _listMap.moveToFront(toMove.node);
2259     }
2260 
2261     /**
2262      * Move the group to make it the last.
2263      */
2264     @trusted final void moveGroupToBack(GroupNode toMove) nothrow pure {
2265         _listMap.moveToBack(toMove.node);
2266     }
2267 
2268     /**
2269      * Move group before other.
2270      */
2271     @trusted final void moveGroupBefore(GroupNode other, GroupNode toMove) nothrow pure {
2272         _listMap.moveBefore(other.node, toMove.node);
2273     }
2274 
2275     /**
2276      * Move group after other.
2277      */
2278     @trusted final void moveGroupAfter(GroupNode other, GroupNode toMove) nothrow pure {
2279         _listMap.moveAfter(other.node, toMove.node);
2280     }
2281 
2282     @safe final ReadOptions readOptions() nothrow const pure {
2283         return _readOptions;
2284     }
2285 private:
2286     string _fileName;
2287     GroupListMap _listMap;
2288     string[] _leadingComments;
2289     ReadOptions _readOptions;
2290 }
2291 
2292 ///
2293 unittest
2294 {
2295     import std.file;
2296     import std.path;
2297     import std.stdio;
2298 
2299     string contents =
2300 `# The first comment
2301 [First Entry]
2302 # Comment
2303 GenericName=File manager
2304 GenericName[ru]=Файловый менеджер
2305 NeedUnescape=yes\\i\tneed
2306 NeedUnescape[ru]=да\\я\tнуждаюсь
2307 # Another comment
2308 [Another Group]
2309 Name=Commander
2310 Comment=Manage files
2311 # The last comment`;
2312 
2313     auto ilf = new IniLikeFile(iniLikeStringReader(contents), "contents.ini");
2314     assert(ilf.fileName() == "contents.ini");
2315     assert(equal(ilf.leadingComments(), ["# The first comment"]));
2316     assert(ilf.group("First Entry"));
2317     assert(ilf.group("Another Group"));
2318     assert(ilf.getNode("Another Group").group is ilf.group("Another Group"));
2319     assert(ilf.group("NonExistent") is null);
2320     assert(ilf.getNode("NonExistent").isNull());
2321     assert(ilf.getNode("NonExistent").key() is null);
2322     assert(ilf.getNode("NonExistent").group() is null);
2323     assert(ilf.saveToString(IniLikeFile.WriteOptions.exact) == contents);
2324 
2325     version(inilikeFileTest)
2326     {
2327         string tempFile = buildPath(tempDir(), "inilike-unittest-tempfile");
2328         try {
2329             assertNotThrown!IniLikeReadException(ilf.saveToFile(tempFile));
2330             auto fileContents = cast(string)std.file.read(tempFile);
2331             static if( __VERSION__ < 2067 ) {
2332                 assert(equal(fileContents.splitLines, contents.splitLines), "Contents should be preserved as is");
2333             } else {
2334                 assert(equal(fileContents.lineSplitter, contents.lineSplitter), "Contents should be preserved as is");
2335             }
2336 
2337             IniLikeFile filf;
2338             assertNotThrown!IniLikeReadException(filf = new IniLikeFile(tempFile));
2339             assert(filf.fileName() == tempFile);
2340             remove(tempFile);
2341         } catch(Exception e) {
2342             //environmental error in unittests
2343         }
2344     }
2345 
2346     auto firstEntry = ilf.group("First Entry");
2347 
2348     assert(!firstEntry.contains("NonExistent"));
2349     assert(firstEntry.contains("GenericName"));
2350     assert(firstEntry.contains("GenericName[ru]"));
2351     assert(firstEntry.byNode().filter!(node => node.isNull()).empty);
2352     assert(firstEntry["GenericName"] == "File manager");
2353     assert(firstEntry.value("GenericName") == "File manager");
2354     assert(firstEntry.getNode("GenericName").key == "GenericName");
2355     assert(firstEntry.getNode("NonExistent").key is null);
2356     assert(firstEntry.getNode("NonExistent").line.type == IniLikeLine.Type.None);
2357 
2358     assert(firstEntry.value("NeedUnescape") == `yes\\i\tneed`);
2359     assert(firstEntry.readEntry("NeedUnescape") == "yes\\i\tneed");
2360     assert(firstEntry.localizedValue("NeedUnescape", "ru") == `да\\я\tнуждаюсь`);
2361     assert(firstEntry.readEntry("NeedUnescape", "ru") == "да\\я\tнуждаюсь");
2362 
2363     firstEntry.writeEntry("NeedEscape", "i\rneed\nescape");
2364     assert(firstEntry.value("NeedEscape") == `i\rneed\nescape`);
2365     firstEntry.writeEntry("NeedEscape", "ru", "мне\rнужно\nэкранирование");
2366     assert(firstEntry.localizedValue("NeedEscape", "ru") == `мне\rнужно\nэкранирование`);
2367 
2368     firstEntry["GenericName"] = "Manager of files";
2369     assert(firstEntry["GenericName"] == "Manager of files");
2370     firstEntry["Authors"] = "Unknown";
2371     assert(firstEntry["Authors"] == "Unknown");
2372     firstEntry.getNode("Authors").setValue("Known");
2373     assert(firstEntry["Authors"] == "Known");
2374 
2375     assert(firstEntry.localizedValue("GenericName", "ru") == "Файловый менеджер");
2376     firstEntry["GenericName", "ru"] = "Менеджер файлов";
2377     assert(firstEntry.localizedValue("GenericName", "ru") == "Менеджер файлов");
2378     firstEntry.setLocalizedValue("Authors", "ru", "Неизвестны");
2379     assert(firstEntry.localizedValue("Authors", "ru") == "Неизвестны");
2380 
2381     firstEntry.removeEntry("GenericName");
2382     assert(!firstEntry.contains("GenericName"));
2383     firstEntry.removeEntry("GenericName", "ru");
2384     assert(!firstEntry.contains("GenericName[ru]"));
2385     firstEntry["GenericName"] = "File Manager";
2386     assert(firstEntry["GenericName"] == "File Manager");
2387 
2388     assert(ilf.group("Another Group")["Name"] == "Commander");
2389     assert(equal(ilf.group("Another Group").byKeyValue(), [ keyValueTuple("Name", "Commander"), keyValueTuple("Comment", "Manage files") ]));
2390 
2391     auto latestCommentNode = ilf.group("Another Group").appendComment("The lastest comment");
2392     assert(latestCommentNode.line.comment == "#The lastest comment");
2393     latestCommentNode.setValue("The latest comment");
2394     assert(latestCommentNode.line.comment == "#The latest comment");
2395     assert(ilf.group("Another Group").prependComment("The first comment").line.comment == "#The first comment");
2396 
2397     assert(equal(
2398         ilf.group("Another Group").byIniLine(),
2399         [IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"), IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment")]
2400     ));
2401 
2402     auto nameLineNode = ilf.group("Another Group").getNode("Name");
2403     assert(nameLineNode.line.value == "Commander");
2404     auto commentLineNode = ilf.group("Another Group").getNode("Comment");
2405     assert(commentLineNode.line.value == "Manage files");
2406 
2407     ilf.group("Another Group").addCommentAfter(nameLineNode, "Middle comment");
2408     ilf.group("Another Group").addCommentBefore(commentLineNode, "Average comment");
2409 
2410     assert(equal(
2411         ilf.group("Another Group").byIniLine(),
2412         [
2413             IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"),
2414             IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"),
2415             IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment")
2416         ]
2417     ));
2418 
2419     ilf.group("Another Group").removeEntry(latestCommentNode);
2420 
2421     assert(equal(
2422         ilf.group("Another Group").byIniLine(),
2423         [
2424             IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"),
2425             IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"),
2426             IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment")
2427         ]
2428     ));
2429 
2430     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group"]));
2431 
2432     assert(!ilf.removeGroup("NonExistent Group"));
2433 
2434     assert(ilf.removeGroup("Another Group"));
2435     assert(!ilf.group("Another Group"));
2436     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry"]));
2437 
2438     ilf.addGenericGroup("Another Group");
2439     assert(ilf.group("Another Group"));
2440     assert(ilf.group("Another Group").byIniLine().empty);
2441     assert(ilf.group("Another Group").byKeyValue().empty);
2442 
2443     assertThrown(ilf.addGenericGroup("Another Group"));
2444 
2445     ilf.addGenericGroup("Other Group");
2446     assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group", "Other Group"]));
2447 
2448     assertThrown!IniLikeException(ilf.addGenericGroup(""));
2449 
2450     import std.range : isForwardRange;
2451 
2452     const IniLikeFile cilf = ilf;
2453     static assert(isForwardRange!(typeof(cilf.byGroup())));
2454     static assert(isForwardRange!(typeof(cilf.group("First Entry").byKeyValue())));
2455     static assert(isForwardRange!(typeof(cilf.group("First Entry").byIniLine())));
2456 
2457     contents =
2458 `[Group]
2459 GenericName=File manager
2460 [Group]
2461 GenericName=Commander`;
2462 
2463     auto shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents), "config.ini"));
2464     assert(shouldThrow !is null, "Duplicate groups should throw");
2465     assert(shouldThrow.lineNumber == 3);
2466     assert(shouldThrow.lineIndex == 2);
2467     assert(shouldThrow.fileName == "config.ini");
2468 
2469     contents =
2470 `[Group]
2471 Key=Value1
2472 Key=Value2`;
2473 
2474     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2475     assert(shouldThrow !is null, "Duplicate key should throw");
2476     assert(shouldThrow.lineNumber == 3);
2477 
2478     contents =
2479 `[Group]
2480 Key=Value
2481 =File manager`;
2482 
2483     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2484     assert(shouldThrow !is null, "Empty key should throw");
2485     assert(shouldThrow.lineNumber == 3);
2486 
2487     contents =
2488 `[Group]
2489 #Comment
2490 Valid=Key
2491 NotKeyNotGroupNotComment`;
2492 
2493     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2494     assert(shouldThrow !is null, "Invalid entry should throw");
2495     assert(shouldThrow.lineNumber == 4);
2496 
2497     contents =
2498 `#Comment
2499 NotComment
2500 [Group]
2501 Valid=Key`;
2502     shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents)));
2503     assert(shouldThrow !is null, "Invalid comment should throw");
2504     assert(shouldThrow.lineNumber == 2);
2505 
2506 
2507     contents = `# The leading comment
2508 [One]
2509 # Comment1
2510 Key1=Value1
2511 Key2=Value2
2512 Key3=Value3
2513 [Two]
2514 Key1=Value1
2515 Key2=Value2
2516 Key3=Value3
2517 # Comment2
2518 [Three]
2519 Key1=Value1
2520 Key2=Value2
2521 # Comment3
2522 Key3=Value3`;
2523 
2524     ilf = new IniLikeFile(iniLikeStringReader(contents));
2525 
2526     ilf.moveGroupToFront(ilf.getNode("Two"));
2527     assert(ilf.byNode().map!(g => g.key).equal(["Two", "One", "Three"]));
2528 
2529     ilf.moveGroupToBack(ilf.getNode("One"));
2530     assert(ilf.byNode().map!(g => g.key).equal(["Two", "Three", "One"]));
2531 
2532     ilf.moveGroupBefore(ilf.getNode("Two"), ilf.getNode("Three"));
2533     assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "Two", "One"]));
2534 
2535     ilf.moveGroupAfter(ilf.getNode("Three"), ilf.getNode("One"));
2536     assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "One", "Two"]));
2537 
2538     auto groupOne = ilf.group("One");
2539     groupOne.moveLineToFront(groupOne.getNode("Key3"));
2540     groupOne.moveLineToBack(groupOne.getNode("Key1"));
2541 
2542     assert(groupOne.byIniLine().equal([
2543         IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromComment("# Comment1"),
2544         IniLikeLine.fromKeyValue("Key2", "Value2"), IniLikeLine.fromKeyValue("Key1", "Value1")
2545     ]));
2546 
2547     auto groupTwo = ilf.group("Two");
2548     groupTwo.moveLineBefore(groupTwo.getNode("Key1"), groupTwo.getNode("Key3"));
2549     groupTwo.moveLineAfter(groupTwo.getNode("Key2"), groupTwo.getNode("Key1"));
2550 
2551     assert(groupTwo.byIniLine().equal([
2552         IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromKeyValue("Key2", "Value2"),
2553          IniLikeLine.fromKeyValue("Key1", "Value1"), IniLikeLine.fromComment("# Comment2")
2554     ]));
2555 }