1 /** 2 * Reading and writing ini-like files, used in Unix systems. 3 * Authors: 4 * $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov). 5 * Copyright: 6 * Roman Chistokhodov, 2015 7 * License: 8 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 9 * See_Also: 10 * $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification). 11 */ 12 13 module inilike; 14 15 private { 16 import std.algorithm; 17 import std.array; 18 import std.conv; 19 import std.exception; 20 import std.file; 21 import std.path; 22 import std.process; 23 import std.range; 24 import std.stdio; 25 import std.string; 26 import std.traits; 27 import std.typecons; 28 29 static if( __VERSION__ < 2066 ) enum nogc = 1; 30 } 31 32 private alias LocaleTuple = Tuple!(string, "lang", string, "country", string, "encoding", string, "modifier"); 33 private alias KeyValueTuple = Tuple!(string, "key", string, "value"); 34 35 /** Retrieve current locale probing environment variables LC_TYPE, LC_ALL and LANG (in this order) 36 * Returns: locale in posix form or an empty string if could not determine locale. 37 * Note: this function does not cache its results. 38 */ 39 @safe string currentLocale() nothrow 40 { 41 string cache; 42 try { 43 cache = environment.get("LC_CTYPE", environment.get("LC_ALL", environment.get("LANG"))); 44 } 45 catch(Exception e) { 46 47 } 48 return cache; 49 } 50 51 /** 52 * Make locale name based on language, country, encoding and modifier. 53 * Returns: locale name in form lang_COUNTRY.ENCODING@MODIFIER 54 * See_Also: parseLocaleName 55 */ 56 @safe string makeLocaleName(string lang, string country = null, string encoding = null, string modifier = null) pure nothrow 57 { 58 return lang ~ (country.length ? "_"~country : "") ~ (encoding.length ? "."~encoding : "") ~ (modifier.length ? "@"~modifier : ""); 59 } 60 61 /** 62 * Parse locale name into the tuple of 4 values corresponding to language, country, encoding and modifier 63 * Returns: Tuple!(string, "lang", string, "country", string, "encoding", string, "modifier") 64 * See_Also: makeLocaleName 65 */ 66 @nogc @trusted auto parseLocaleName(string locale) pure nothrow 67 { 68 auto modifiderSplit = findSplit(locale, "@"); 69 auto modifier = modifiderSplit[2]; 70 71 auto encodongSplit = findSplit(modifiderSplit[0], "."); 72 auto encoding = encodongSplit[2]; 73 74 auto countrySplit = findSplit(encodongSplit[0], "_"); 75 auto country = countrySplit[2]; 76 77 auto lang = countrySplit[0]; 78 79 return LocaleTuple(lang, country, encoding, modifier); 80 } 81 82 /** 83 * Construct localized key name from key and locale. 84 * Returns: localized key in form key[locale]. Automatically omits locale encoding if present. 85 * Example: 86 ---------- 87 assert(localizedKey("Name", "ru_RU") == "Name[ru_RU]"); 88 ---------- 89 * See_Also: separateFromLocale 90 */ 91 @safe string localizedKey(string key, string locale) pure nothrow 92 { 93 auto t = parseLocaleName(locale); 94 if (!t.encoding.empty) { 95 locale = makeLocaleName(t.lang, t.country, null, t.modifier); 96 } 97 return key ~ "[" ~ locale ~ "]"; 98 } 99 100 /** 101 * ditto, but constructs locale name from arguments. 102 * Example: 103 ---------- 104 assert(localizedKey("Name", "ru", "RU") == "Name[ru_RU]"); 105 ---------- 106 */ 107 @safe string localizedKey(string key, string lang, string country, string modifier = null) pure nothrow 108 { 109 return key ~ "[" ~ makeLocaleName(lang, country, null, modifier) ~ "]"; 110 } 111 112 /** 113 * Separate key name into non-localized key and locale name. 114 * If key is not localized returns original key and empty string. 115 * Returns: tuple of key and locale name. 116 * See_Also: localizedKey 117 */ 118 @nogc @trusted Tuple!(string, string) separateFromLocale(string key) pure nothrow { 119 if (key.endsWith("]")) { 120 auto t = key.findSplit("["); 121 if (t[1].length) { 122 return tuple(t[0], t[2][0..$-1]); 123 } 124 } 125 return tuple(key, string.init); 126 } 127 128 /** 129 * Tells whether the entry value presents true 130 * Example: 131 ----------- 132 assert(isTrue("true")); 133 assert(isTrue("1")); 134 assert(!isTrue("not boolean")); 135 ----------- 136 */ 137 @nogc @safe bool isTrue(string value) pure nothrow { 138 return (value == "true" || value == "1"); 139 } 140 141 /** 142 * Tells whether the entry value presents false 143 * Example: 144 ---------- 145 assert(isFalse("false")); 146 assert(isFalse("0")); 147 assert(!isFalse("not boolean")); 148 ---------- 149 */ 150 @nogc @safe bool isFalse(string value) pure nothrow { 151 return (value == "false" || value == "0"); 152 } 153 154 /** 155 * Check if the entry value can be interpreted as boolean value. 156 * Example: 157 --------- 158 assert(isBoolean("true")); 159 assert(isBoolean("1")); 160 assert(isBoolean("false")); 161 assert(isBoolean("0")); 162 assert(!isBoolean("not boolean")); 163 --------- 164 * See_Also: isTrue, isFalse 165 */ 166 @nogc @safe bool isBoolean(string value) pure nothrow { 167 return isTrue(value) || isFalse(value); 168 } 169 /** 170 * Escapes string by replacing special symbols with escaped sequences. 171 * These symbols are: '\\' (backslash), '\n' (newline), '\r' (carriage return) and '\t' (tab). 172 * Note: 173 * Currently the library stores values as they were loaded from file, i.e. escaped. 174 * To keep things consistent you should take care about escaping the value before inserting. The library will not do it for you. 175 * Returns: Escaped string. 176 * Example: 177 ---- 178 assert("\\next\nline".escapeValue() == `\\next\nline`); // notice how the string on the right is raw. 179 ---- 180 * See_Also: unescapeValue 181 */ 182 @trusted string escapeValue(string value) nothrow pure { 183 return value.replace("\\", `\\`).replace("\n", `\n`).replace("\r", `\r`).replace("\t", `\t`); 184 } 185 186 @trusted string doUnescape(string value, in Tuple!(char, char)[] pairs) nothrow pure { 187 auto toReturn = appender!string(); 188 189 for (size_t i = 0; i < value.length; i++) { 190 if (value[i] == '\\') { 191 if (i < value.length - 1) { 192 char c = value[i+1]; 193 auto t = pairs.find!"a[0] == b[0]"(tuple(c,c)); 194 if (!t.empty) { 195 toReturn.put(t.front[1]); 196 i++; 197 continue; 198 } 199 } 200 } 201 toReturn.put(value[i]); 202 } 203 return toReturn.data; 204 } 205 206 207 /** 208 * Unescapes string. You should unescape values returned by library before displaying until you want keep them as is (e.g., to allow user to edit values in escaped form). 209 * Returns: Unescaped string. 210 * Example: 211 ----- 212 assert(`\\next\nline`.unescapeValue() == "\\next\nline"); // notice how the string on the left is raw. 213 ---- 214 * See_Also: escapeValue 215 */ 216 @trusted string unescapeValue(string value) nothrow pure 217 { 218 static immutable Tuple!(char, char)[] pairs = [ 219 tuple('s', ' '), 220 tuple('n', '\n'), 221 tuple('r', '\r'), 222 tuple('t', '\t'), 223 tuple('\\', '\\') 224 ]; 225 return doUnescape(value, pairs); 226 } 227 228 /** 229 * Represents the line from ini-like file. 230 * Usually you should not use this struct directly, since it's tightly connected with internal $(B IniLikeFile) implementation. 231 */ 232 struct IniLikeLine 233 { 234 enum Type 235 { 236 None = 0, 237 Comment = 1, 238 KeyValue = 2, 239 GroupStart = 4 240 } 241 242 @nogc @safe static IniLikeLine fromComment(string comment) nothrow { 243 return IniLikeLine(comment, null, Type.Comment); 244 } 245 246 @nogc @safe static IniLikeLine fromGroupName(string groupName) nothrow { 247 return IniLikeLine(groupName, null, Type.GroupStart); 248 } 249 250 @nogc @safe static IniLikeLine fromKeyValue(string key, string value) nothrow { 251 return IniLikeLine(key, value, Type.KeyValue); 252 } 253 254 @nogc @safe string comment() const nothrow { 255 return _type == Type.Comment ? _first : null; 256 } 257 258 @nogc @safe string key() const nothrow { 259 return _type == Type.KeyValue ? _first : null; 260 } 261 262 @nogc @safe string value() const nothrow { 263 return _type == Type.KeyValue ? _second : null; 264 } 265 266 @nogc @safe string groupName() const nothrow { 267 return _type == Type.GroupStart ? _first : null; 268 } 269 270 @nogc @safe Type type() const nothrow { 271 return _type; 272 } 273 274 @nogc @safe void makeNone() nothrow { 275 _type = Type.None; 276 } 277 278 private: 279 string _first; 280 string _second; 281 Type _type = Type.None; 282 } 283 284 /** 285 * This class represents the group (section) in the .init like file. 286 * You can create and use instances of this class only in the context of $(B IniLikeFile) or its derivatives. 287 * Note: keys are case-sensitive. 288 */ 289 final class IniLikeGroup 290 { 291 private: 292 @nogc @safe this(string name, const IniLikeFile parent) nothrow { 293 assert(parent, "logic error: no parent for IniLikeGroup"); 294 _name = name; 295 _parent = parent; 296 } 297 298 public: 299 300 /** 301 * Returns: the value associated with the key 302 * Note: it's an error to access nonexistent value 303 * See_Also: value 304 */ 305 @nogc @safe string opIndex(string key) const nothrow { 306 auto i = key in _indices; 307 assert(_values[*i].type == IniLikeLine.Type.KeyValue); 308 assert(_values[*i].key == key); 309 return _values[*i].value; 310 } 311 312 /** 313 * Insert new value or replaces the old one if value associated with key already exists. 314 * Returns: inserted/updated value 315 * Throws: $(B Exception) if key is not valid 316 */ 317 @safe string opIndexAssign(string value, string key) { 318 enforce(_parent.isValidKey(separateFromLocale(key)[0]), "key is invalid"); 319 auto pick = key in _indices; 320 if (pick) { 321 return (_values[*pick] = IniLikeLine.fromKeyValue(key, value)).value; 322 } else { 323 _indices[key] = _values.length; 324 _values ~= IniLikeLine.fromKeyValue(key, value); 325 return value; 326 } 327 } 328 /** 329 * Ditto, localized version. 330 * See_Also: setLocalizedValue, localizedValue 331 */ 332 @safe string opIndexAssign(string value, string key, string locale) { 333 string keyName = localizedKey(key, locale); 334 return this[keyName] = value; 335 } 336 337 /** 338 * Tell if group contains value associated with the key. 339 */ 340 @nogc @safe bool contains(string key) const nothrow { 341 return value(key) !is null; 342 } 343 344 /** 345 * Get value by key. 346 * Returns: the value associated with the key, or defaultValue if group does not contain item with this key. 347 */ 348 @nogc @safe string value(string key, string defaultValue = null) const nothrow { 349 auto pick = key in _indices; 350 if (pick) { 351 if(_values[*pick].type == IniLikeLine.Type.KeyValue) { 352 assert(_values[*pick].key == key); 353 return _values[*pick].value; 354 } 355 } 356 return defaultValue; 357 } 358 359 /** 360 * Perform locale matching lookup as described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys). 361 * Returns: the localized value associated with key and locale, or defaultValue if group does not contain item with this key. 362 * See_Also: currentLocale 363 */ 364 @safe string localizedValue(string key, string locale, string defaultValue = null) const nothrow { 365 //Any ideas how to get rid of this boilerplate and make less allocations? 366 auto t = parseLocaleName(locale); 367 auto lang = t.lang; 368 auto country = t.country; 369 auto modifier = t.modifier; 370 371 if (lang.length) { 372 string pick; 373 if (country.length && modifier.length) { 374 pick = value(localizedKey(key, locale)); 375 if (pick !is null) { 376 return pick; 377 } 378 } 379 if (country.length) { 380 pick = value(localizedKey(key, lang, country)); 381 if (pick !is null) { 382 return pick; 383 } 384 } 385 if (modifier.length) { 386 pick = value(localizedKey(key, lang, null, modifier)); 387 if (pick !is null) { 388 return pick; 389 } 390 } 391 pick = value(localizedKey(key, lang, null)); 392 if (pick !is null) { 393 return pick; 394 } 395 } 396 397 return value(key, defaultValue); 398 } 399 400 /** 401 * Same as localized version of opIndexAssign, but uses function syntax. 402 */ 403 @safe void setLocalizedValue(string key, string locale, string value) { 404 this[key, locale] = value; 405 } 406 407 /** 408 * Removes entry by key. To remove localized values use localizedKey. 409 */ 410 @safe void removeEntry(string key) nothrow { 411 auto pick = key in _indices; 412 if (pick) { 413 _values[*pick].makeNone(); 414 } 415 } 416 417 /** 418 * Iterate by Key-Value pairs. 419 * Returns: range of Tuple!(string, "key", string, "value") 420 */ 421 @nogc @safe auto byKeyValue() const nothrow { 422 return _values.filter!(v => v.type == IniLikeLine.Type.KeyValue).map!(v => KeyValueTuple(v.key, v.value)); 423 } 424 425 /** 426 * Get name of this group. 427 * Returns: the name of this group. 428 */ 429 @nogc @safe string name() const nothrow { 430 return _name; 431 } 432 433 /** 434 * Returns: the range of $(B IniLikeLine)s included in this group. 435 * Note: this does not include Group line itself. 436 */ 437 @system auto byIniLine() const { 438 return _values.filter!(v => v.type != IniLikeLine.Type.None); 439 } 440 441 @trusted void addComment(string comment) nothrow { 442 _values ~= IniLikeLine.fromComment(comment); 443 } 444 445 private: 446 size_t[string] _indices; 447 IniLikeLine[] _values; 448 string _name; 449 const IniLikeFile _parent; 450 } 451 452 /** 453 * Exception thrown on the file read error. 454 */ 455 class IniLikeException : Exception 456 { 457 this(string msg, size_t lineNumber, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe { 458 super(msg, file, line, next); 459 _lineNumber = lineNumber; 460 } 461 462 ///Number of line in the file where the exception occured, starting from 1. Don't be confused with $(B line) property of $(B Throwable). 463 @nogc @safe size_t lineNumber() const nothrow { 464 return _lineNumber; 465 } 466 467 private: 468 size_t _lineNumber; 469 } 470 471 /** 472 * Reads range of strings into the range of IniLikeLines. 473 * See_Also: iniLikeFileReader, iniLikeStringReader 474 */ 475 @trusted auto iniLikeRangeReader(Range)(Range byLine) if(is(ElementType!Range : string)) 476 { 477 return byLine.map!(function(string line) { 478 line = strip(line); 479 if (line.empty || line.startsWith("#")) { 480 return IniLikeLine.fromComment(line); 481 } else if (line.startsWith("[") && line.endsWith("]")) { 482 return IniLikeLine.fromGroupName(line[1..$-1]); 483 } else { 484 auto t = line.findSplit("="); 485 auto key = t[0].stripRight(); 486 auto value = t[2].stripLeft(); 487 488 if (t[1].length) { 489 return IniLikeLine.fromKeyValue(key, value); 490 } else { 491 return IniLikeLine(); 492 } 493 } 494 }); 495 } 496 497 /** 498 * ditto, convenient function for reading from the file. 499 * Throws: $(B ErrnoException) if file could not be opened. 500 */ 501 @trusted auto iniLikeFileReader(string fileName) 502 { 503 return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup)); 504 } 505 506 /** 507 * ditto, convenient function for reading from string. 508 */ 509 @trusted auto iniLikeStringReader(string contents) 510 { 511 return iniLikeRangeReader(contents.splitLines()); 512 } 513 514 /** 515 * Ini-like file. 516 * 517 */ 518 class IniLikeFile 519 { 520 public: 521 ///Flags to manage .ini like file reading 522 enum ReadOptions 523 { 524 noOptions = 0, /// Read all groups and skip comments and empty lines. 525 firstGroupOnly = 1, /// Ignore other groups than the first one. 526 preserveComments = 2, /// Preserve comments and empty lines. Use this when you want to preserve them across writing. 527 ignoreGroupDuplicates = 4, /// Ignore group duplicates. The first found will be used. 528 ignoreInvalidKeys = 8 /// Skip invalid keys during parsing. 529 } 530 531 /** 532 * Construct empty IniLikeFile, i.e. without any groups or values 533 */ 534 @nogc @safe this() nothrow { 535 536 } 537 538 /** 539 * Read from file. 540 * Throws: 541 * $(B ErrnoException) if file could not be opened. 542 * $(B IniLikeException) if error occured while reading the file. 543 */ 544 @safe this(string fileName, ReadOptions options = ReadOptions.noOptions) { 545 this(iniLikeFileReader(fileName), options, fileName); 546 } 547 548 /** 549 * Read from range of $(B IniLikeLine)s. 550 * Throws: 551 * $(B IniLikeException) if error occured while parsing. 552 */ 553 @trusted this(Range)(Range byLine, ReadOptions options = ReadOptions.noOptions, string fileName = null) if(is(ElementType!Range : IniLikeLine)) 554 { 555 size_t lineNumber = 0; 556 IniLikeGroup currentGroup; 557 bool ignoreKeyValues; 558 559 try { 560 foreach(line; byLine) 561 { 562 lineNumber++; 563 final switch(line.type) 564 { 565 case IniLikeLine.Type.Comment: 566 { 567 if (options & ReadOptions.preserveComments) { 568 if (currentGroup is null) { 569 addFirstComment(line.comment); 570 } else { 571 currentGroup.addComment(line.comment); 572 } 573 } 574 } 575 break; 576 case IniLikeLine.Type.GroupStart: 577 { 578 if ((options & ReadOptions.ignoreGroupDuplicates) && group(line.groupName)) { 579 ignoreKeyValues = true; 580 continue; 581 } 582 ignoreKeyValues = false; 583 currentGroup = addGroup(line.groupName); 584 585 if (options & ReadOptions.firstGroupOnly) { 586 break; 587 } 588 } 589 break; 590 case IniLikeLine.Type.KeyValue: 591 { 592 if (ignoreKeyValues || ((options & ReadOptions.ignoreInvalidKeys) && !isValidKey(line.key)) ) { 593 continue; 594 } 595 enforce(currentGroup, "met key-value pair before any group"); 596 currentGroup[line.key] = line.value; 597 } 598 break; 599 case IniLikeLine.Type.None: 600 { 601 throw new Exception("not key-value pair, nor group start nor comment"); 602 } 603 } 604 } 605 606 _fileName = fileName; 607 } 608 catch (Exception e) { 609 throw new IniLikeException(e.msg, lineNumber, e.file, e.line, e.next); 610 } 611 } 612 613 /** 614 * Get group by name. 615 * Returns: IniLikeGroup instance associated with groupName or $(B null) if not found. 616 * See_Also: byGroup 617 */ 618 @nogc @safe inout(IniLikeGroup) group(string groupName) nothrow inout { 619 auto pick = groupName in _groupIndices; 620 if (pick) { 621 return _groups[*pick]; 622 } 623 return null; 624 } 625 626 /** 627 * Create new group using groupName. 628 * Returns: newly created instance of IniLikeGroup. 629 * Throws: Exception if group with such name already exists or groupName is empty. 630 * See_Also: removeGroup, group 631 */ 632 @safe IniLikeGroup addGroup(string groupName) { 633 enforce(groupName.length, "empty group name"); 634 enforce(group(groupName) is null, "group already exists"); 635 636 auto iniLikeGroup = new IniLikeGroup(groupName, this); 637 _groupIndices[groupName] = _groups.length; 638 _groups ~= iniLikeGroup; 639 640 return iniLikeGroup; 641 } 642 643 /** 644 * Remove group by name. 645 * See_Also: addGroup, group 646 */ 647 @safe void removeGroup(string groupName) nothrow { 648 auto pick = groupName in _groupIndices; 649 if (pick) { 650 _groups[*pick] = null; 651 } 652 } 653 654 /** 655 * Range of groups in order how they were defined in file. 656 * See_Also: group 657 */ 658 @nogc @safe final auto byGroup() const nothrow { 659 return _groups.filter!(f => f !is null); 660 } 661 662 ///ditto 663 @nogc @safe final auto byGroup() nothrow { 664 return _groups.filter!(f => f !is null); 665 } 666 667 668 /** 669 * Save object to the file using .ini-like format. 670 * Throws: ErrnoException if the file could not be opened or an error writing to the file occured. 671 * See_Also: saveToString, save 672 */ 673 @trusted void saveToFile(string fileName) const { 674 auto f = File(fileName, "w"); 675 void dg(string line) { 676 f.writeln(line); 677 } 678 save(&dg); 679 } 680 681 /** 682 * Save object to string using .ini like format. 683 * Returns: string representing the contents of file. 684 * See_Also: saveToFile, save 685 */ 686 @safe string saveToString() const { 687 auto a = appender!(string[])(); 688 void dg(string line) { 689 a.put(line); 690 } 691 save(&dg); 692 return a.data.join("\n"); 693 } 694 695 /** 696 * Alias for saving delegate. 697 * See_Also: save 698 */ 699 alias SaveDelegate = void delegate(string); 700 701 /** 702 * Use delegate to retrieve strings line by line. 703 * Those strings can be written to the file or be showed in text area. 704 * Note: returned strings don't have trailing newline character. 705 */ 706 @trusted void save(SaveDelegate sink) const { 707 foreach(line; firstComments()) { 708 sink(line); 709 } 710 711 foreach(group; byGroup()) { 712 sink("[" ~ group.name ~ "]"); 713 foreach(line; group.byIniLine()) { 714 if (line.type == IniLikeLine.Type.Comment) { 715 sink(line.comment); 716 } else if (line.type == IniLikeLine.Type.KeyValue) { 717 sink(line.key ~ "=" ~ line.value); 718 } 719 } 720 } 721 } 722 723 /** 724 * File path where the object was loaded from. 725 * Returns: file name as was specified on the object creation. 726 */ 727 @nogc @safe string fileName() nothrow const { 728 return _fileName; 729 } 730 731 /** 732 * Tell whether the string is valid key. For IniLikeFile the valid key is any non-empty string. 733 * Reimplement this function in the derived class to throw exception from IniLikeGroup when key is invalid. 734 */ 735 @nogc @safe bool isValidKey(string key) pure nothrow const { 736 return key.length != 0; 737 } 738 739 protected: 740 @nogc @trusted final auto firstComments() const nothrow { 741 return _firstComments; 742 } 743 744 @trusted final void addFirstComment(string line) nothrow { 745 _firstComments ~= line; 746 } 747 748 private: 749 string _fileName; 750 size_t[string] _groupIndices; 751 IniLikeGroup[] _groups; 752 string[] _firstComments; 753 } 754 755 unittest 756 { 757 //Test locale-related functions 758 assert(makeLocaleName("ru", "RU") == "ru_RU"); 759 assert(makeLocaleName("ru", "RU", "UTF-8") == "ru_RU.UTF-8"); 760 assert(makeLocaleName("ru", "RU", "UTF-8", "mod") == "ru_RU.UTF-8@mod"); 761 assert(makeLocaleName("ru", null, null, "mod") == "ru@mod"); 762 763 assert(parseLocaleName("ru_RU.UTF-8@mod") == tuple("ru", "RU", "UTF-8", "mod")); 764 assert(parseLocaleName("ru@mod") == tuple("ru", string.init, string.init, "mod")); 765 assert(parseLocaleName("ru_RU") == tuple("ru", "RU", string.init, string.init)); 766 767 assert(localizedKey("Name", "ru_RU") == "Name[ru_RU]"); 768 assert(localizedKey("Name", "ru_RU.UTF-8") == "Name[ru_RU]"); 769 assert(localizedKey("Name", "ru", "RU") == "Name[ru_RU]"); 770 771 assert(separateFromLocale("Name[ru_RU]") == tuple("Name", "ru_RU")); 772 assert(separateFromLocale("Name") == tuple("Name", string.init)); 773 774 //Test locale matching lookup 775 auto lilf = new IniLikeFile; 776 lilf.addGroup("Entry"); 777 auto group = lilf.group("Entry"); 778 assert(group.name == "Entry"); 779 group["Name"] = "Programmer"; 780 group["Name[ru_RU]"] = "Разработчик"; 781 group["Name[ru@jargon]"] = "Кодер"; 782 group["Name[ru]"] = "Программист"; 783 group["GenericName"] = "Program"; 784 group["GenericName[ru]"] = "Программа"; 785 assert(group["Name"] == "Programmer"); 786 assert(group.localizedValue("Name", "ru@jargon") == "Кодер"); 787 assert(group.localizedValue("Name", "ru_RU@jargon") == "Разработчик"); 788 assert(group.localizedValue("Name", "ru") == "Программист"); 789 assert(group.localizedValue("Name", "nonexistent locale") == "Programmer"); 790 assert(group.localizedValue("GenericName", "ru_RU") == "Программа"); 791 792 //Test escaping and unescaping 793 assert("a\\next\nline\top".escapeValue() == `a\\next\nline\top`); 794 assert(`a\\next\nline\top`.unescapeValue() == "a\\next\nline\top"); 795 796 //Test key types functions 797 assert(isTrue("true")); 798 assert(isTrue("1")); 799 assert(!isTrue("not boolean")); 800 801 assert(isFalse("false")); 802 assert(isFalse("0")); 803 assert(!isFalse("not boolean")); 804 805 assert(isBoolean("true")); 806 assert(isBoolean("1")); 807 assert(isBoolean("false")); 808 assert(isBoolean("0")); 809 assert(!isBoolean("not boolean")); 810 811 //Test IniLikeFile 812 string contents = 813 `# The first comment 814 [First Entry] 815 # Comment 816 GenericName=File manager 817 GenericName[ru]=Файловый менеджер 818 # Another comment 819 [Another Group] 820 Name=Commander 821 Comment=Manage files 822 # The last comment`; 823 824 auto ilf = new IniLikeFile(iniLikeStringReader(contents), IniLikeFile.ReadOptions.preserveComments); 825 assert(ilf.group("First Entry")); 826 assert(ilf.group("Another Group")); 827 assert(ilf.saveToString() == contents); 828 829 auto firstEntry = ilf.group("First Entry"); 830 831 assert(firstEntry["GenericName"] == "File manager"); 832 assert(firstEntry.value("GenericName") == "File manager"); 833 firstEntry["GenericName"] = "Manager of files"; 834 assert(firstEntry["GenericName"] == "Manager of files"); 835 firstEntry["Authors"] = "Unknown"; 836 assert(firstEntry["Authors"] == "Unknown"); 837 838 assert(firstEntry.localizedValue("GenericName", "ru") == "Файловый менеджер"); 839 firstEntry.setLocalizedValue("GenericName", "ru", "Менеджер файлов"); 840 assert(firstEntry.localizedValue("GenericName", "ru") == "Менеджер файлов"); 841 firstEntry.setLocalizedValue("Authors", "ru", "Неизвестны"); 842 assert(firstEntry.localizedValue("Authors", "ru") == "Неизвестны"); 843 844 firstEntry.removeEntry("GenericName"); 845 assert(!firstEntry.contains("GenericName")); 846 firstEntry["GenericName"] = "File Manager"; 847 assert(firstEntry["GenericName"] == "File Manager"); 848 849 assert(ilf.group("Another Group")["Name"] == "Commander"); 850 assert(equal(ilf.group("Another Group").byKeyValue(), [ KeyValueTuple("Name", "Commander"), KeyValueTuple("Comment", "Manage files") ])); 851 852 assert(equal(ilf.byGroup().map!(g => g.name), ["First Entry", "Another Group"])); 853 854 ilf.removeGroup("Another Group"); 855 assert(!ilf.group("Another Group")); 856 assert(equal(ilf.byGroup().map!(g => g.name), ["First Entry"])); 857 858 ilf.addGroup("Another Group"); 859 assert(ilf.group("Another Group")); 860 assert(ilf.group("Another Group").byIniLine().empty); 861 assert(ilf.group("Another Group").byKeyValue().empty); 862 863 ilf.addGroup("Other Group"); 864 assert(equal(ilf.byGroup().map!(g => g.name), ["First Entry", "Another Group", "Other Group"])); 865 866 const IniLikeFile cilf = ilf; 867 static assert(is(typeof(cilf.byGroup()))); 868 static assert(is(typeof(cilf.group("First Entry").byKeyValue()))); 869 static assert(is(typeof(cilf.group("First Entry").byIniLine()))); 870 871 contents = 872 `[Group] 873 GenericName=File manager 874 [Group] 875 Name=Commander`; 876 877 assertThrown(new IniLikeFile(iniLikeStringReader(contents))); 878 assertNotThrown(new IniLikeFile(iniLikeStringReader(contents), IniLikeFile.ReadOptions.ignoreGroupDuplicates)); 879 880 contents = 881 `[Group] 882 =File manager`; 883 884 assertThrown(new IniLikeFile(iniLikeStringReader(contents))); 885 assertNotThrown(new IniLikeFile(iniLikeStringReader(contents), IniLikeFile.ReadOptions.ignoreInvalidKeys)); 886 }