1 /** 2 * Structured INI 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.sini; 15 16 import std.algorithm; 17 import std.conv; 18 import std.exception; 19 import std.range; 20 import std.string; 21 import std.traits; 22 23 import ae.utils.exception; 24 25 alias std..string.indexOf indexOf; 26 27 /// Represents the user-defined behavior for handling a node in a 28 /// structured INI file's hierarchy. 29 struct IniHandler(S) 30 { 31 /// User callback for parsing a value at this node. 32 void delegate(S name, S value) leafHandler; 33 34 /// User callback for obtaining a child node from this node. 35 IniHandler delegate(S name) nodeHandler; 36 } 37 38 struct IniLine(S) 39 { 40 enum Type 41 { 42 empty, 43 section, 44 value 45 } 46 47 Type type; 48 S name; // section or value 49 S value; 50 } 51 52 IniLine!S lexIniLine(S)(S line) 53 if (isSomeString!S) 54 { 55 IniLine!S result; 56 57 line = line.chomp().stripLeft(); 58 if (line.empty) 59 return result; 60 if (line[0] == '#' || line[0] == ';') 61 return result; 62 63 if (line[0] == '[') 64 { 65 line = line.stripRight(); 66 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 67 result.type = result.Type.section; 68 result.name = line[1..$-1]; 69 } 70 else 71 { 72 auto pos = line.indexOf('='); 73 enforce(pos > 0, "Malformed value line (no '=')"); 74 result.type = result.Type.value; 75 result.name = line[0..pos].strip; 76 result.value = line[pos+1..$].strip; 77 } 78 return result; 79 } 80 81 /// Parse a structured INI from a range of lines, through the given handler. 82 void parseIni(R, H)(R r, H rootHandler) 83 if (isInputRange!R && isSomeString!(ElementType!R)) 84 { 85 auto currentHandler = rootHandler; 86 87 size_t lineNumber; 88 while (!r.empty) 89 { 90 lineNumber++; 91 mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)})); 92 93 scope(success) r.popFront(); 94 auto line = lexIniLine(r.front); 95 final switch (line.type) 96 { 97 case line.Type.empty: 98 break; 99 case line.Type.section: 100 currentHandler = rootHandler; 101 foreach (segment; line.name.split(".")) 102 currentHandler = currentHandler.nodeHandler 103 .enforce("This group may not have any nodes.") 104 (segment); 105 break; 106 case line.Type.value: 107 { 108 auto handler = currentHandler; 109 auto segments = line.name.split("."); 110 enforce(segments.length, "Malformed value line (empty name)"); 111 foreach (segment; segments[0..$-1]) 112 handler = handler.nodeHandler 113 .enforce("This group may not have any nodes.") 114 (segment); 115 handler.leafHandler 116 .enforce("This group may not have any values.") 117 (segments[$-1], line.value); 118 break; 119 } 120 } 121 } 122 } 123 124 /// Helper which creates an INI handler out of delegates. 125 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null) 126 { 127 return IniHandler!S(leafHandler, nodeHandler); 128 } 129 130 unittest 131 { 132 int count; 133 134 parseIni 135 ( 136 q"< 137 s.n1=v1 138 [s] 139 n2=v2 140 >".splitLines(), 141 iniHandler 142 ( 143 null, 144 (in char[] name) 145 { 146 assert(name == "s"); 147 return iniHandler 148 ( 149 (in char[] name, in char[] value) 150 { 151 assert(name .length==2 && name [0] == 'n' 152 && value.length==2 && value[0] == 'v' 153 && name[1] == value[1]); 154 count++; 155 } 156 ); 157 } 158 ) 159 ); 160 161 assert(count==2); 162 } 163 164 /// Alternative API for IniHandler, where each leaf is a node 165 struct IniTraversingHandler(S) 166 { 167 /// User callback for parsing a value at this node. 168 void delegate(S value) leafHandler; 169 170 /// User callback for obtaining a child node from this node. 171 IniTraversingHandler delegate(S name) nodeHandler; 172 173 private IniHandler!S conv() 174 { 175 // Don't reference "this" from a lambda, 176 // as it can be a temporary on the stack 177 IniTraversingHandler thisCopy = this; 178 return IniHandler!S 179 ( 180 (S name, S value) 181 { 182 thisCopy 183 .nodeHandler 184 .enforce("This group may not have any nodes.") 185 (name) 186 .leafHandler 187 .enforce("This group may not have a value.") 188 (value); 189 }, 190 (S name) 191 { 192 return thisCopy 193 .nodeHandler 194 .enforce("This group may not have any nodes.") 195 (name) 196 .conv(); 197 } 198 ); 199 } 200 } 201 202 enum isNestingType(T) = isAssociativeArray!T || is(T == struct); 203 204 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v) 205 { 206 static if (!is(U == Unqual!U)) 207 return makeIniHandler!S(*cast(Unqual!U*)&v); 208 else 209 static if (is(typeof((ref U v){alias V=typeof(v[S.init]); v[S.init] = V.init; *(S.init in v) = V.init;}))) 210 return IniTraversingHandler!S 211 ( 212 null, 213 (S name) 214 { 215 auto pField = name in v; 216 if (!pField) 217 { 218 v[name] = typeof(v[name]).init; 219 pField = name in v; 220 } 221 else 222 static if (!isNestingType!U) 223 throw new Exception("Duplicate value: " ~ to!string(name)); 224 return makeIniHandler!S(*pField); 225 } 226 ); 227 else 228 static if (is(U == struct)) 229 return IniTraversingHandler!S 230 ( 231 null, 232 delegate IniTraversingHandler!S (S name) 233 { 234 bool found; 235 foreach (i, ref field; v.tupleof) 236 { 237 enum fieldName = to!S(v.tupleof[i].stringof[2..$]); 238 if (name == fieldName) 239 { 240 static if (is(typeof(makeIniHandler!S(v.tupleof[i])))) 241 return makeIniHandler!S(v.tupleof[i]); 242 else 243 throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof); 244 } 245 } 246 static if (is(ReturnType!(v.parseSection))) 247 return v.parseSection(name); 248 else 249 throw new Exception("Unknown field " ~ to!string(name)); 250 } 251 ); 252 else 253 static if (isAssociativeArray!U) 254 return IniTraversingHandler!S 255 ( 256 null, 257 (S name) 258 { 259 alias K = typeof(v.keys[0]); 260 auto key = to!K(name); 261 auto pField = key in v; 262 if (!pField) 263 { 264 v[key] = typeof(v[key]).init; 265 pField = key in v; 266 } 267 else 268 static if (!isNestingType!U) 269 throw new Exception("Duplicate value: " ~ to!string(name)); 270 return makeIniHandler!S(*pField); 271 } 272 ); 273 else 274 static if (is(typeof(to!U(string.init)))) 275 return IniTraversingHandler!S 276 ( 277 (S value) 278 { 279 v = to!U(value); 280 } 281 ); 282 else 283 static if (is(U V : V*)) 284 { 285 static if (is(typeof(v = new V))) 286 if (!v) 287 v = new V; 288 return makeIniHandler!S(*v); 289 } 290 else 291 static assert(false, "Can't parse " ~ U.stringof); 292 } 293 294 /// Parse structured INI lines from a range of strings, into a user-defined struct. 295 T parseIni(T, R)(R r) 296 if (isInputRange!R && isSomeString!(ElementType!R)) 297 { 298 T result; 299 r.parseIniInto(result); 300 return result; 301 } 302 303 /// ditto 304 void parseIniInto(R, T)(R r, ref T result) 305 if (isInputRange!R && isSomeString!(ElementType!R)) 306 { 307 parseIni(r, makeIniHandler!(ElementType!R)(result).conv()); 308 } 309 310 unittest 311 { 312 static struct File 313 { 314 struct S 315 { 316 string n1, n2; 317 int[string] a; 318 } 319 S s; 320 } 321 322 auto f = parseIni!File 323 ( 324 q"< 325 s.n1=v1 326 s.a.foo=1 327 [s] 328 n2=v2 329 a.bar=2 330 >".dup.splitLines() 331 ); 332 333 assert(f.s.n1=="v1"); 334 assert(f.s.n2=="v2"); 335 assert(f.s.a==["foo":1, "bar":2]); 336 } 337 338 unittest 339 { 340 static struct Custom 341 { 342 struct Section 343 { 344 string name; 345 string[string] values; 346 } 347 Section[] sections; 348 349 auto parseSection(wstring name) 350 { 351 sections.length++; 352 auto p = §ions[$-1]; 353 p.name = to!string(name); 354 return makeIniHandler!wstring(p.values); 355 } 356 } 357 358 auto c = parseIni!Custom 359 ( 360 q"< 361 [one] 362 a=a 363 [two] 364 b=b 365 >"w.splitLines() 366 ); 367 368 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 369 } 370 371 version(unittest) static import ae.utils.aa; 372 373 unittest 374 { 375 import ae.utils.aa; 376 377 auto o = parseIni!(OrderedMap!(string, string)) 378 ( 379 q"< 380 b=b 381 a=a 382 >".splitLines() 383 ); 384 385 assert(o["a"]=="a" && o["b"] == "b"); 386 } 387 388 unittest 389 { 390 import ae.utils.aa; 391 392 static struct S { string x; } 393 394 auto o = parseIni!(OrderedMap!(string, S)) 395 ( 396 q"< 397 b.x=b 398 [a] 399 x=a 400 >".splitLines() 401 ); 402 403 assert(o["a"].x == "a" && o["b"].x == "b"); 404 } 405 406 unittest 407 { 408 static struct S { string x, y; } 409 410 auto r = parseIni!(S[string]) 411 ( 412 q"< 413 a.x=x 414 [a] 415 y=y 416 >".splitLines() 417 ); 418 419 assert(r["a"].x == "x" && r["a"].y == "y"); 420 } 421 422 unittest 423 { 424 static struct S { string x, y; } 425 static struct T { S* s; } 426 427 { 428 T t; 429 parseIniInto(["s.x=v"], t); 430 assert(t.s.x == "v"); 431 } 432 433 { 434 S s = {"x"}; T t = {&s}; 435 parseIniInto(["s.y=v"], t); 436 assert(s.x == "x"); 437 assert(s.y == "v"); 438 } 439 } 440 441 // *************************************************************************** 442 443 deprecated alias StructuredIniHandler = IniHandler; 444 deprecated alias parseStructuredIni = parseIni; 445 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler; 446 deprecated alias makeStructuredIniHandler = makeIniHandler; 447 448 // *************************************************************************** 449 450 /// Convenience function to load a struct from an INI file. 451 /// Returns .init if the file does not exist. 452 S loadIni(S)(string fileName) 453 { 454 S s; 455 456 import std.file; 457 if (fileName.exists) 458 s = fileName 459 .readText() 460 .splitLines() 461 .parseIni!S(); 462 463 return s; 464 } 465 466 /// As above, though loads several INI files 467 /// (duplicate values appearing in later INI files 468 /// override any values from earlier files). 469 S loadInis(S)(in char[][] fileNames) 470 { 471 S s; 472 473 import std.file; 474 s = fileNames 475 .map!(fileName => 476 fileName.exists ? 477 fileName 478 .readText() 479 .splitLines() 480 : 481 null 482 ) 483 .joiner(["[]"]) 484 .parseIni!S(); 485 486 return s; 487 } 488 489 // *************************************************************************** 490 491 /// Simple convenience formatter for writing INI files. 492 struct IniWriter(O) 493 { 494 O writer; 495 496 void startSection(string name) 497 { 498 writer.put('[', name, "]\n"); 499 } 500 501 void writeValue(string name, string value) 502 { 503 writer.put(name, '=', value, '\n'); 504 } 505 } 506 507 /// Insert a blank line before each section 508 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); } 509 510 // *************************************************************************** 511 512 /** 513 Adds or updates a value in an INI file. 514 515 If the value is already in the INI file, then it is updated 516 in-place; otherwise, a new one is added to the matching section. 517 518 Whitespace and comments on other lines are preserved. 519 520 Params: 521 lines = INI file lines (as in parseIni) 522 name = fully-qualified name of the value to update 523 (use `.` to specify section path) 524 value = new value to write 525 */ 526 527 void updateIni(S)(ref S[] lines, S name, S value) 528 { 529 size_t valueLine = size_t.max; 530 S valueLineSection; 531 532 S currentSection = null; 533 auto pathPrefix() { return chain(currentSection, repeat(typeof(name[0])('.'), currentSection is null ? 0 : 1)); } 534 535 size_t bestSectionEnd; 536 S bestSection; 537 bool inBestSection = true; 538 539 foreach (i, line; lines) 540 { 541 auto lex = lexIniLine(line); 542 final switch (lex.type) 543 { 544 case lex.Type.empty: 545 break; 546 case lex.Type.value: 547 if (equal(chain(pathPrefix, lex.name), name)) 548 { 549 valueLine = i; 550 valueLineSection = currentSection; 551 } 552 break; 553 case lex.type.section: 554 if (inBestSection) 555 bestSectionEnd = i; 556 inBestSection = false; 557 558 currentSection = lex.name; 559 if (name.startsWith(pathPrefix) && currentSection.length > bestSection.length) 560 { 561 bestSection = currentSection; 562 inBestSection = true; 563 } 564 break; 565 } 566 } 567 568 if (inBestSection) 569 bestSectionEnd = lines.length; 570 571 S genLine(S section) { return name[section.length ? section.length + 1 : 0 .. $] ~ '=' ~ value; } 572 573 if (valueLine != size_t.max) 574 lines[valueLine] = genLine(valueLineSection); 575 else 576 lines = lines[0..bestSectionEnd] ~ genLine(bestSection) ~ lines[bestSectionEnd..$]; 577 } 578 579 unittest 580 { 581 auto ini = q"< 582 a=1 583 a=2 584 >".splitLines(); 585 updateIni(ini, "a", "3"); 586 struct S { int a; } 587 assert(parseIni!S(ini).a == 3); 588 } 589 590 unittest 591 { 592 auto ini = q"< 593 a=1 594 [s] 595 a=2 596 [t] 597 a=3 598 >".strip.splitLines.map!strip.array; 599 updateIni(ini, "a", "4"); 600 updateIni(ini, "s.a", "5"); 601 updateIni(ini, "t.a", "6"); 602 assert(equal(ini, q"< 603 a=4 604 [s] 605 a=5 606 [t] 607 a=6 608 >".strip.splitLines.map!strip), text(ini)); 609 } 610 611 unittest 612 { 613 auto ini = q"< 614 [s] 615 [t] 616 >".strip.splitLines.map!strip.array; 617 updateIni(ini, "a", "1"); 618 updateIni(ini, "s.a", "2"); 619 updateIni(ini, "t.a", "3"); 620 assert(equal(ini, q"< 621 a=1 622 [s] 623 a=2 624 [t] 625 a=3 626 >".strip.splitLines.map!strip)); 627 } 628 629 void updateIniFile(S)(string fileName, S name, S value) 630 { 631 import std.file, std.stdio, std.utf; 632 auto lines = fileName.exists ? fileName.readText.splitLines : null; 633 updateIni(lines, name, value); 634 lines.map!(l => chain(l.byCodeUnit, only(typeof(S.init[0])('\n')))).joiner.toFile(fileName); 635 } 636 637 unittest 638 { 639 import std.file; 640 enum fn = "temp.ini"; 641 std.file.write(fn, "a=b\n"); 642 scope(exit) remove(fn); 643 updateIniFile(fn, "a", "c"); 644 assert(read(fn) == "a=c\n"); 645 }