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