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