1 /**
2  * INI parsing functionality.
3  *
4  * Examples:
5  * ---
6  * auto ini = Ini.ParseString("test = bar")
7  * writeln("Value of test is ", ini["test"]);
8  * ---
9  */
10 module dini.parser;
11 
12 import std.algorithm : min, max, countUntil;
13 import std.array     : split, replaceInPlace, join;
14 import std.file      : readText;
15 import std.stdio     : File;
16 import std..string    : strip, splitLines;
17 import std.traits    : isSomeString;
18 import std.range     : ElementType;
19 import std.conv      : to;
20 import dini.reader   : UniversalINIReader, INIException, INIToken;
21 
22 
23 /**
24  * Represents ini section
25  *
26  * Example:
27  * ---
28  * Ini ini = Ini.Parse("path/to/your.conf");
29  * string value = ini.getKey("a");
30  * ---
31  */
32 struct IniSection
33 {
34     /// Section name
35     protected string         _name = "root";
36     
37     /// Parent
38     /// Null if none
39     protected IniSection*    _parent;
40     
41     /// Childs
42     protected IniSection[]   _sections;
43     
44     /// Keys
45     protected string[string] _keys;
46     
47     
48     
49     /**
50      * Creates new IniSection instance
51      *
52      * Params:
53      *  name = Section name
54      */
55     public this(string name)
56     {
57         _name = name;
58         _parent = null;
59     }
60     
61     
62     /**
63      * Creates new IniSection instance
64      *
65      * Params:
66      *  name = Section name
67      *  parent = Section parent
68      */
69     public this(string name, IniSection* parent)
70     {
71         _name = name;
72         _parent = parent;
73     }
74     
75     /**
76      * Sets section key
77      *
78      * Params:
79      *  name = Key name
80      *  value = Value to set
81      */
82     public void setKey(string name, string value)
83     {
84         _keys[name] = value;
85     }
86     
87     /**
88      * Checks if specified key exists
89      *
90      * Params:
91      *  name = Key name
92      *
93      * Returns:
94      *  True if exists, false otherwise 
95      */
96     public bool hasKey(string name) @safe nothrow @nogc
97     {
98         return (name in _keys) !is null;
99     }
100     
101     /**
102      * Gets key value
103      *
104      * Params:
105      *  name = Key name
106      *
107      * Returns:
108      *  Key value
109      *
110      * Throws:
111      *  IniException if key does not exists
112      */
113     public string getKey(string name)
114     {
115         if(!hasKey(name)) {
116             throw new IniException("Key '"~name~"' does not exists");
117         }
118         
119         return _keys[name];
120     }
121     
122     
123     /// ditto
124     alias getKey opCall;
125     
126     /**
127      * Gets key value or defaultValue if key does not exist
128      *
129      * Params:
130      *  name = Key name
131      *  defaultValue = Default value
132      *
133      * Returns:
134      *  Key value or defaultValue
135      *
136      */
137     public string getKey(string name, string defaultValue) @safe nothrow
138     {
139         return hasKey(name) ? _keys[name] : defaultValue;
140     }
141     
142     /**
143      * Removes key
144      *
145      * Params:
146      *  name = Key name
147      */
148     public void removeKey(string name)
149     {
150         _keys.remove(name);
151     }
152     
153     /**
154      * Adds section
155      *
156      * Params:
157      *  section = Section to add
158      */
159     public void addSection(ref IniSection section)
160     {
161         _sections ~= section;
162     }
163     
164     /**
165      * Checks if specified section exists
166      *
167      * Params:
168      *  name = Section name
169      *
170      * Returns:
171      *  True if exists, false otherwise 
172      */
173     public bool hasSection(string name)
174     {
175         foreach(ref section; _sections)
176         {
177             if(section.name() == name)
178                 return true;
179         }
180         
181         return false;
182     }
183     
184     /**
185      * Returns reference to section
186      *
187      * Params:
188      *  Section name
189      *
190      * Returns:
191      *  Section with specified name
192      */
193     public ref IniSection getSection(string name)
194     {
195         foreach(ref section; _sections)
196         {
197             if(section.name() == name)
198                 return section;
199         }
200         
201         throw new IniException("Section '"~name~"' does not exists");
202     }
203     
204     
205     /// ditto
206     public alias getSection opIndex;
207     
208     /**
209      * Removes section
210      *
211      * Params:
212      *  name = Section name
213      */
214     public void removeSection(string name)
215     {
216         IniSection[] childs;
217         
218         foreach(section; _sections)
219         {
220             if(section.name != name)
221                 childs ~= section;
222         }
223         
224         _sections = childs;
225     }
226     
227     /**
228      * Section name
229      *
230      * Returns:
231      *  Section name
232      */
233     public string name() @property
234     {
235         return _name;
236     }
237     
238     /**
239      * Array of keys
240      *
241      * Returns:
242      *  Associative array of keys
243      */
244     public string[string] keys() @property
245     {
246         return _keys;
247     }
248     
249     /**
250      * Array of sections
251      *
252      * Returns:
253      *  Array of sections
254      */
255     public IniSection[] sections() @property
256     {
257         return _sections;
258     }
259     
260     /**
261      * Root section
262      */
263     public IniSection root() @property
264     {
265         IniSection s = this;
266         
267         while(s.getParent() != null)
268             s = *(s.getParent());
269         
270         return s;
271     }
272     
273     /**
274      * Section parent
275      *
276      * Returns:
277      *  Pointer to parent, or null if parent does not exists
278      */
279     public IniSection* getParent()
280     {
281         return _parent;
282     }
283     
284     /**
285      * Checks if current section has parent
286      *
287      * Returns:
288      *  True if section has parent, false otherwise
289      */
290     public bool hasParent()
291     {
292         return _parent != null;
293     }
294     
295     /**
296      * Moves current section to another one
297      *
298      * Params:
299      *  New parent
300      */
301     public void setParent(ref IniSection parent)
302     {
303         _parent.removeSection(this.name);
304         _parent = &parent;
305         parent.addSection(this);
306     }
307     
308     
309     /**
310      * Parses filename
311      *
312      * Params:
313      *  filename = Configuration filename
314      *  doLookups = Should variable lookups be resolved after parsing? 
315      */
316     public void parse(string filename, bool doLookups = true)
317     {
318         parseString(readText(filename), doLookups);
319     }
320 
321     public void parse(File* file, bool doLookups = true)
322     {
323         string data = file.byLine().join().to!string;
324         parseString(data, doLookups);
325     }
326 
327     public void parseWith(Reader)(string filename, bool doLookups = true)
328     {
329         parseStringWith!Reader(readText(filename), doLookups);
330     }
331 
332     public void parseWith(Reader)(File* file, bool doLookups = true)
333     {
334         string data = file.byLine().join().to!string;
335         parseStringWith!Reader(data, doLookups);
336     }
337 
338     public void parseString(string data, bool doLookups = true)
339     {
340         parseStringWith!UniversalINIReader(data, doLookups);
341     }
342 
343     public void parseStringWith(Reader)(string data, bool doLookups = true)
344     {
345         IniSection* section = &this;
346 
347         auto reader = Reader(data);
348         alias KeyType = reader.KeyType;
349         while (reader.next()) switch (reader.type) with (INIToken) {
350             case SECTION:
351                 section = &this;
352                 string name = reader.value.get!string;
353                 auto parts = name.split(":");
354 
355                 // [section : parent]
356                 if (parts.length > 1)
357                     name = parts[0].strip;
358 
359                 IniSection child = IniSection(name, section);
360 
361                 if (parts.length > 1) {
362                     string parent = parts[1].strip;
363                     child.inherit(section.getSectionEx(parent));
364                 }
365                 section.addSection(child);
366                 section = &section.getSection(name);
367                 break;
368 
369             case KEY:
370                 section.setKey(reader.value.get!KeyType.name, reader.value.get!KeyType.value);
371                 break;
372 
373             default:
374                 break;
375         }
376 
377         if(doLookups == true)
378             parseLookups();
379     }
380     
381     /**
382      * Parses lookups
383      */
384     public void parseLookups()
385     {
386         foreach (name, ref value; _keys)
387         {
388             ptrdiff_t start = -1;
389             char[] buf;
390             
391             foreach (i, c; value) {
392                 if (c == '%') {
393                     if (start != -1) {
394                         IniSection sect;
395                         string newValue;
396                         char[][] parts;
397                         
398                         if (buf[0] == '.') {
399                             parts = buf[1..$].split(".");
400                             sect = this.root;
401                         }
402                         else {
403                             parts = buf.split(".");
404                             sect = this;
405                         }
406                         
407                         newValue = sect.getSectionEx(parts[0..$-1].join(".").idup).getKey(parts[$-1].idup);
408                         
409                         value.replaceInPlace(start, i+1, newValue);
410                         start = -1;
411                         buf = [];
412                     }
413                     else {
414                         start = i;
415                     }
416                 }
417                 else if (start != -1) {
418                     buf ~= c;
419                 }
420             }
421         }
422         
423         foreach(child; _sections) {
424             child.parseLookups();
425         }
426     }
427     
428     /**
429      * Returns section by name in inheriting(names connected by dot)
430      *
431      * Params:
432      *  name = Section name
433      *
434      * Returns:
435      *  Section
436      */
437     public IniSection getSectionEx(string name)
438     {
439         IniSection* root = &this;
440         auto parts = name.split(".");
441         
442         foreach(part; parts) {
443             root = (&root.getSection(part));
444         }
445         
446         return *root;
447     }
448     
449     /**
450      * Inherits keys from section
451      *
452      * Params:
453      *  Section to inherit
454      */
455     public void inherit(IniSection sect)
456     {
457         this._keys = sect.keys().dup;
458     }
459 
460 
461     public void save(string filename)
462     {
463         import std.file;
464 
465         if (exists(filename))
466             remove(filename);
467 
468         File file = File(filename, "w");
469 
470         foreach (section; _sections) {
471             file.writeln("[" ~ section.name() ~ "]");
472 
473             string[string] propertiesInSection = section.keys();
474             foreach (key; propertiesInSection.keys) {
475                 file.writeln(key ~ " = " ~ propertiesInSection[key]);
476             }
477 
478             file.writeln();
479         }
480 
481         file.close();
482     }
483 
484 
485     /**
486      * Parses Ini file
487      *
488      * Params:
489      *  filename = Path to ini file
490      *
491      * Returns:
492      *  IniSection root
493      */
494     static Ini Parse(string filename, bool parseLookups = true)
495     {
496         Ini i;
497         i.parse(filename, parseLookups);
498         return i;
499     }
500 
501 
502     /**
503      * Parses Ini file with specified reader
504      *
505      * Params:
506      *  filename = Path to ini file
507      *
508      * Returns:
509      *  IniSection root
510      */
511     static Ini ParseWith(Reader)(string filename, bool parseLookups = true)
512     {
513         Ini i;
514         i.parseWith!Reader(filename, parseLookups);
515         return i;
516     }
517 
518     static Ini ParseString(string data, bool parseLookups = true)
519     {
520         Ini i;
521         i.parseString(data, parseLookups);
522         return i;
523     }
524 
525     static Ini ParseStringWith(Reader)(string data, bool parseLookups = true)
526     {
527         Ini i;
528         i.parseStringWith!Reader(data, parseLookups);
529         return i;
530     }
531 }
532 
533 // Compat
534 alias INIException IniException;
535 
536 /// ditto
537 alias IniSection Ini;
538 
539 
540 ///
541 Struct siphon(Struct)(Ini ini)
542 {
543 	import std.traits;
544 	Struct ans;
545 	if(ini.hasSection(Struct.stringof))
546 		foreach(ti, Name; FieldNameTuple!(Struct))
547 		{
548 			alias ToType = typeof(ans.tupleof[ti]);
549 			if(ini[Struct.stringof].hasKey(Name))
550 				ans.tupleof[ti] = to!ToType(ini[Struct.stringof].getKey(Name));
551 		}
552 	return ans;
553 }
554 
555 unittest {
556 	struct Section {
557 		int var;
558 	}
559 
560 	auto ini = Ini.ParseString("[Section]\nvar=3");
561 	auto m = ini.siphon!Section;
562 	assert(m.var == 3);
563 }
564 
565 
566 unittest {
567     auto data = q"(
568 key1 = value
569 
570 # comment
571 
572 test = bar ; comment
573 
574 [section 1]
575 key1 = new key
576 num = 151
577 empty
578 
579 
580 [ various   ]
581 "quoted key"= VALUE 123
582 
583 quote_multiline = """
584   this is value
585 """
586 
587 escape_sequences = "yay\nboo"
588 escaped_newlines = abcd \
589 efg
590 )";
591 
592     auto ini = Ini.ParseString(data);
593     assert(ini.getKey("key1") == "value");
594     assert(ini.getKey("test") == "bar ; comment");
595 
596     assert(ini.hasSection("section 1"));
597     with (ini["section 1"]) {
598         assert(getKey("key1") == "new key");
599         assert(getKey("num") == "151");
600         assert(getKey("empty") == "");
601     }
602 
603     assert(ini.hasSection("various"));
604     with (ini["various"]) {
605         assert(getKey("quoted key") == "VALUE 123");
606         assert(getKey("quote_multiline") == "\n  this is value\n");
607         assert(getKey("escape_sequences") == "yay\nboo");
608         assert(getKey("escaped_newlines") == "abcd efg");
609     }
610 }
611 
612 unittest {
613     auto data = q"EOF
614 key1 = value
615 
616 # comment
617 
618 test = bar ; comment
619 
620 [section 1]
621 key1 = new key
622 num = 151
623 empty
624 
625 EOF";
626 
627     auto ini = Ini.ParseString(data);
628     assert(ini.getKey("key1") == "value");
629     assert(ini.getKey("test") == "bar ; comment");
630     assert(ini.hasSection("section 1"));
631     assert(ini["section 1"]("key1") == "new key");
632     assert(ini["section 1"]("num") == "151");
633     assert(ini["section 1"]("empty") == "");
634 }
635 
636 unittest {
637 	auto data = q"EOF
638 [def]
639 name1=value1
640 name2=value2
641 
642 [foo : def]
643 name1=Name1 from foo. Lookup for def.name2: %name2%
644 EOF";
645 
646     // Parse file
647     auto ini = Ini.ParseString(data, true);
648 
649     assert(ini["foo"].getKey("name1")
650 	  == "Name1 from foo. Lookup for def.name2: value2");
651 }
652 
653 unittest {
654 	auto data = q"EOF
655 [section]
656 name=%value%
657 EOF";
658 
659 	// Create ini struct instance
660 	Ini ini;
661 	Ini iniSec = IniSection("section");
662 	ini.addSection(iniSec);
663 
664 	// Set key value
665 	ini["section"].setKey("value", "verify");
666 
667 	// Now, you can use value in ini file
668 	ini.parseString(data);
669 
670 	assert(ini["section"].getKey("name") == "verify");
671 }
672 
673 
674 unittest {
675     import dini.reader;
676 
677     alias MyReader = INIReader!(
678         UniversalINIFormat,
679         UniversalINIReader.CurrentFlags & ~INIFlags.ProcessEscapes,
680         UniversalINIReader.CurrentBoxer
681     );
682     auto ini = Ini.ParseStringWith!MyReader(`path=C:\Path`);
683     assert(ini("path") == `C:\Path`);
684 }