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 = (§ion.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 }