1 /**
2  * This file is part of Dini library
3  * 
4  * Copyright: Robert Pasiński
5  * License: Boost License
6  */
7 module dini;
8 
9 import std.stream : BufferedFile;
10 import std.string : strip;
11 import std.traits : isSomeString;
12 import std.array  : split, indexOf, replaceInPlace, join;
13 import std.algorithm : min, max, countUntil;
14 import std.conv   : to;
15 
16 import std.stdio;
17 
18 
19 /**
20  * Represents ini section
21  *
22  * Example:
23  * ---
24  * Ini ini = Ini.Parse("path/to/your.conf");
25  * string value = ini.getKey("a");
26  * ---
27  */
28 struct IniSection
29 {
30     /// Section name
31     protected string         _name = "root";
32     
33     /// Parent
34     /// Null if none
35     protected IniSection*    _parent;
36     
37     /// Childs
38     protected IniSection[]   _sections;
39     
40     /// Keys
41     protected string[string] _keys;
42     
43     
44     
45     /**
46      * Creates new IniSection instance
47      *
48      * Params:
49      *  name = Section name
50      */
51     public this(string name)
52     {
53         _name = name;
54         _parent = null;
55     }
56     
57     
58     /**
59      * Creates new IniSection instance
60      *
61      * Params:
62      *  name = Section name
63      *  parent = Section parent
64      */
65     public this(string name, IniSection* parent)
66     {
67         _name = name;
68         _parent = parent;
69     }
70     
71     /**
72      * Sets section key
73      *
74      * Params:
75      *  name = Key name
76      *  value = Value to set
77      */
78     public void setKey(string name, string value)
79     {
80         _keys[name] = value;
81     }
82     
83     /**
84      * Checks if specified key exists
85      *
86      * Params:
87      *  name = Key name
88      *
89      * Returns:
90      *  True if exists, false otherwise 
91      */
92     public bool hasKey(string name)
93     {
94         return (name in _keys) !is null;
95     }
96     
97     /**
98      * Gets key value
99      *
100      * Params:
101      *  name = Key name
102      *
103      * Returns:
104      *  Key value
105      *
106      * Throws:
107      *  IniException if key does not exists
108      */
109     public string getKey(string name)
110     {
111         if(!hasKey(name)) {
112             throw new IniException("Key '"~name~"' does not exists");
113         }
114         
115         return _keys[name];
116     }
117     
118     
119     /// ditto
120     alias getKey opCall;
121     
122     
123     /**
124      * Removes key
125      *
126      * Params:
127      *  name = Key name
128      */
129     public void removeKey(string name)
130     {
131         _keys.remove(name);
132     }
133     
134     /**
135      * Adds section
136      *
137      * Params:
138      *  section = Section to add
139      */
140     public void addSection(ref IniSection section)
141     {
142         _sections ~= section;
143     }
144     
145     /**
146      * Checks if specified section exists
147      *
148      * Params:
149      *  name = Section name
150      *
151      * Returns:
152      *  True if exists, false otherwise 
153      */
154     public bool hasSection(string name)
155     {
156         foreach(ref section; _sections)
157         {
158             if(section.name() == name)
159                 return true;
160         }
161         
162         return false;
163     }
164     
165     /**
166      * Returns reference to section
167      *
168      * Params:
169      *  Section name
170      *
171      * Returns:
172      *  Section with specified name
173      */
174     public ref IniSection getSection(string name)
175     {
176         foreach(ref section; _sections)
177         {
178             if(section.name() == name)
179                 return section;
180         }
181         
182         throw new IniException("Section '"~name~"' does not exists");
183     }
184     
185     
186     /// ditto
187     public alias getSection opIndex;
188     
189     /**
190      * Removes section
191      *
192      * Params:
193      *  name = Section name
194      */
195     public void removeSection(string name)
196     {
197         IniSection[] childs;
198         
199         foreach(section; _sections)
200         {
201             if(section.name != name)
202                 childs ~= section;
203         }
204         
205         _sections = childs;
206     }
207     
208     /**
209      * Section name
210      *
211      * Returns:
212      *  Section name
213      */
214     public string name() @property
215     {
216         return _name;
217     }
218     
219     /**
220      * Array of keys
221      *
222      * Returns:
223      *  Associative array of keys
224      */
225     public string[string] keys() @property
226     {
227         return _keys;
228     }
229     
230     /**
231      * Array of sections
232      *
233      * Returns:
234      *  Array of sections
235      */
236     public IniSection[] sections() @property
237     {
238         return _sections;
239     }
240     
241     /**
242      * Root section
243      */
244     public IniSection root() @property
245     {
246         IniSection s = this;
247         
248         while(s.getParent() != null)
249             s = *(s.getParent());
250         
251         return s;
252     }
253     
254     /**
255      * Section parent
256      *
257      * Returns:
258      *  Pointer to parent, or null if parent does not exists
259      */
260     public IniSection* getParent()
261     {
262         return _parent;
263     }
264     
265     /**
266      * Checks if current section has parent
267      *
268      * Returns:
269      *  True if section has parent, false otherwise
270      */
271     public bool hasParent()
272     {
273         return _parent != null;
274     }
275     
276     /**
277      * Moves current section to another one
278      *
279      * Params:
280      *  New parent
281      */
282     public void setParent(ref IniSection parent)
283     {
284         _parent.removeSection(this.name);
285         _parent = &parent;
286         parent.addSection(this);
287     }
288     
289     
290     /**
291      * Parses filename
292      *
293      * Params:
294      *  filename = Configuration filename
295      *  doLookups = Should variable lookups be resolved after parsing? 
296      */
297     public void parse(string filename, bool doLookups = true)
298     {
299         BufferedFile file = new BufferedFile(filename);
300         scope(exit) file.close;
301         
302         IniSection* section = &this;
303         
304         foreach(i, char[] line; file)
305         {
306             line = strip(line);
307             
308             // Empty line
309             if(line.length < 1) continue;
310             
311             // Comment line
312             if(line[0] == ';')  continue;
313             
314             // Section header
315             if(line.length >= 3 && line[0] == '[' && line[$-1] == ']')
316             {
317                 section = &this;
318                 char[] name = line[1..$-1];
319                 string parent;
320                 
321                 ptrdiff_t pos = name.countUntil(":");
322                 if(pos > -1)
323                 {
324                     parent = name[pos+1..$].strip().idup;
325                     name = name[0..pos].strip();
326                 }
327                 
328                 if(name.countUntil(".") > -1)
329                 {
330                     auto names = name.split(".");
331                     foreach(part; names)
332                     {
333                         IniSection sect;
334                         
335                         if(section.hasSection(part.idup)) {
336                             sect = section.getSection(part.idup);
337                         } else {
338                             sect = IniSection(part.idup, section);
339                             section.addSection(sect);
340                         }
341                         
342                         section = (&section.getSection(part.idup));
343                     }
344                 }
345                 else
346                 {
347                     IniSection sect;
348                     
349                     if(section.hasSection(name.idup)) {
350                         sect = section.getSection(name.idup);
351                     } else {
352                         sect = IniSection(name.idup, section);
353                         section.addSection(sect);
354                     }
355                     
356                     section = (&this.getSection(name.idup));
357                 }
358                 
359                 if(parent.length > 1)
360                 {
361                     if(parent[0] == '.')
362                         section.inherit(this.getSectionEx(parent[1..$]));
363                     else 
364                         section.inherit(section.getParent().getSectionEx(parent));
365                 }
366                 continue;
367             }
368             
369             // Assignement
370             auto parts = split(line, "=", 2);
371             if(parts.length > 1)
372             {
373                 auto val = parts[1].strip();
374                 if(val.length > 2 && val[0] == '"' && val[$-1] == '"') val = val[1..$-1];
375                 section.setKey(parts[0].strip().idup, val.idup);
376                 continue;
377             }
378             
379             throw new IniException("Syntax error at line "~to!string(i));
380         }
381         
382         if(doLookups == true)
383             parseLookups();
384     }
385     
386     /**
387      * Parses lookups
388      */
389     public void parseLookups()
390     {
391         foreach(name, ref value; _keys)
392         {
393             ptrdiff_t start = -1;
394             char[] buf;
395             
396             foreach(i, c; value)
397             {
398                 if(c == '%')
399                 {
400                     if(start != -1)
401                     {
402                         IniSection sect;
403                         string newValue;
404                         char[][] parts;
405                         
406                         if(buf[0] == '.')
407                         {
408                             parts = buf[1..$].split(".");
409                             sect = this.root;
410                         }
411                         else
412                         {
413                             parts = buf.split(".");
414                             sect = this;
415                         }
416                         
417                         newValue = sect.getSectionEx(parts[0..$-1].join(".").idup)
418                             .getKey(parts[$-1].idup);
419                         
420                         value.replaceInPlace(start, i+1, newValue);
421                         start = -1;
422                         buf = [];
423                     }
424                     else {
425                         start = i;
426                     }
427                 }
428                 else if(start != -1) {
429                     buf ~= c;
430                 }
431             }
432         }
433         
434         foreach(child; _sections)
435         {
436             child.parseLookups();
437         }
438     }
439     
440     /**
441      * Returns section by name in inheriting(names connected by dot)
442      *
443      * Params:
444      *  name = Section name
445      *
446      * Returns:
447      *  Section
448      */
449     public IniSection getSectionEx(string name)
450     {
451         IniSection* root = &this;
452         auto parts = name.split(".");
453         
454         foreach(part; parts)
455         {
456             root = (&root.getSection(part));
457         }
458         
459         return *root;
460     }
461     
462     /**
463      * Inherits keys from section
464      *
465      * Params:
466      *  Section to inherit
467      */
468     public void inherit(IniSection sect)
469     {
470         this._keys = sect.keys().dup;
471     }
472     
473     /**
474      * Splits string by delimeter with limit
475      *
476      * Params:
477      *  txt     =   Text to split
478      *  delim   =   Delimeter
479      *  limit   =   Limit of splits 
480      *
481      * Returns:
482      *  Splitted string
483      */
484     protected T[] split(T, S)(T txt, S delim, int limit)
485     if(isSomeString!(T) && isSomeString!(S))
486     {
487         limit -= 1;
488         T[] parts;
489         ptrdiff_t last, len = delim.length, cnt;
490         
491         for(int i = 0; i <= txt.length; i++)
492         {
493             if(cnt >= limit)
494                 break;
495             
496             if(txt[i .. min(i + len, txt.length)] == delim)
497             {
498                 parts ~= txt[last .. i];
499                 last = min(i + 1, txt.length);
500                 cnt++;
501             }
502         }
503         
504         parts ~= txt[last .. txt.length];       
505         
506         return parts;
507     }
508     
509     /**
510      * Parses Ini file
511      *
512      * Params:
513      *  filename = Path to ini file
514      *
515      * Returns:
516      *  IniSection root
517      */
518     static Ini Parse(string filename)
519     {
520         Ini i;
521         i.parse(filename);
522         return i;
523     }
524 }
525 
526 /// ditto
527 alias IniSection Ini;
528 
529 ///
530 class IniException : Exception
531 {
532     this(string msg)
533     {
534         super(msg);
535     }
536 }