1 /** 2 * Class representation of ini-like file. 3 * Authors: 4 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 5 * Copyright: 6 * Roman Chistokhodov, 2015-2017 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.file; 14 15 private { 16 import std.exception; 17 import std.algorithm.iteration : map, filter; 18 import std.algorithm.searching : canFind; 19 import inilike.common; 20 } 21 22 public import inilike.range; 23 public import inilike.exception; 24 import inilike.read; 25 26 private @trusted string makeComment(string line) pure nothrow 27 { 28 if (line.length && line[$-1] == '\n') { 29 line = line[0..$-1]; 30 } 31 if (!line.isComment && line.length) { 32 line = '#' ~ line; 33 } 34 line = line.replace("\n", " "); 35 return line; 36 } 37 38 private @trusted IniLikeLine makeCommentLine(string line) pure nothrow 39 { 40 return IniLikeLine.fromComment(makeComment(line)); 41 } 42 43 /** 44 * Container used internally by $(D IniLikeFile) and $(D IniLikeGroup). 45 * Technically this is a list with optional value access by key. 46 */ 47 struct ListMap(K,V, size_t chunkSize = 32) 48 { 49 /// 50 @disable this(this); 51 52 /** 53 * Insert key-value pair to the front of list. 54 * Returns: Inserted node. 55 */ 56 Node* insertFront(K key, V value) { 57 Node* newNode = givePlace(key, value); 58 putToFront(newNode); 59 return newNode; 60 } 61 62 /** 63 * Insert key-value pair to the back of list. 64 * Returns: Inserted node. 65 */ 66 Node* insertBack(K key, V value) { 67 Node* newNode = givePlace(key, value); 68 putToBack(newNode); 69 return newNode; 70 } 71 72 /** 73 * Insert key-value pair before some node in the list. 74 * Returns: Inserted node. 75 */ 76 Node* insertBefore(Node* node, K key, V value) { 77 Node* newNode = givePlace(key, value); 78 putBefore(node, newNode); 79 return newNode; 80 } 81 82 /** 83 * Insert key-value pair after some node in the list. 84 * Returns: Inserted node. 85 */ 86 Node* insertAfter(Node* node, K key, V value) { 87 Node* newNode = givePlace(key, value); 88 putAfter(node, newNode); 89 return newNode; 90 } 91 92 /** 93 * Add value at the start of list. 94 * Returns: Inserted node. 95 */ 96 Node* prepend(V value) { 97 Node* newNode = givePlace(value); 98 putToFront(newNode); 99 return newNode; 100 } 101 102 /** 103 * Add value at the end of list. 104 * Returns: Inserted node. 105 */ 106 Node* append(V value) { 107 Node* newNode = givePlace(value); 108 putToBack(newNode); 109 return newNode; 110 } 111 112 /** 113 * Add value before some node in the list. 114 * Returns: Inserted node. 115 */ 116 Node* addBefore(Node* node, V value) { 117 Node* newNode = givePlace(value); 118 putBefore(node, newNode); 119 return newNode; 120 } 121 122 /** 123 * Add value after some node in the list. 124 * Returns: Inserted node. 125 */ 126 Node* addAfter(Node* node, V value) { 127 Node* newNode = givePlace(value); 128 putAfter(node, newNode); 129 return newNode; 130 } 131 132 /** 133 * Move node to the front of list. 134 */ 135 void moveToFront(Node* toMove) 136 { 137 if (_head is toMove) { 138 return; 139 } 140 141 pullOut(toMove); 142 putToFront(toMove); 143 } 144 145 /** 146 * Move node to the back of list. 147 */ 148 void moveToBack(Node* toMove) 149 { 150 if (_tail is toMove) { 151 return; 152 } 153 154 pullOut(toMove); 155 putToBack(toMove); 156 } 157 158 /** 159 * Move node to the location before other node. 160 */ 161 void moveBefore(Node* other, Node* toMove) { 162 if (other is toMove) { 163 return; 164 } 165 166 pullOut(toMove); 167 putBefore(other, toMove); 168 } 169 170 /** 171 * Move node to the location after other node. 172 */ 173 void moveAfter(Node* other, Node* toMove) { 174 if (other is toMove) { 175 return; 176 } 177 178 pullOut(toMove); 179 putAfter(other, toMove); 180 } 181 182 /** 183 * Remove node from list. It also becomes unaccessible via key lookup. 184 */ 185 void remove(Node* toRemove) 186 { 187 pullOut(toRemove); 188 189 if (toRemove.hasKey()) { 190 _dict.remove(toRemove.key); 191 } 192 193 if (_lastEmpty) { 194 _lastEmpty.next = toRemove; 195 } 196 toRemove.prev = _lastEmpty; 197 _lastEmpty = toRemove; 198 } 199 200 /** 201 * Remove value by key. 202 * Returns: true if node with such key was found and removed. False otherwise. 203 */ 204 bool remove(K key) { 205 Node** toRemove = key in _dict; 206 if (toRemove) { 207 remove(*toRemove); 208 return true; 209 } 210 return false; 211 } 212 213 /** 214 * Remove the first node. 215 */ 216 void removeFront() { 217 remove(_head); 218 } 219 220 /** 221 * Remove the last node. 222 */ 223 void removeBack() { 224 remove(_tail); 225 } 226 227 /** 228 * Get list node by key. 229 * Returns: Found Node or null if container does not have node associated with key. 230 */ 231 inout(Node)* getNode(K key) inout { 232 auto toReturn = key in _dict; 233 if (toReturn) { 234 return *toReturn; 235 } 236 return null; 237 } 238 239 private static struct ByNode(NodeType) 240 { 241 private: 242 NodeType* _begin; 243 NodeType* _end; 244 245 public: 246 bool empty() const { 247 return _begin is null || _end is null || _begin.prev is _end || _end.next is _begin; 248 } 249 250 auto front() { 251 return _begin; 252 } 253 254 auto back() { 255 return _end; 256 } 257 258 void popFront() { 259 _begin = _begin.next; 260 } 261 262 void popBack() { 263 _end = _end.prev; 264 } 265 266 @property auto save() { 267 return this; 268 } 269 } 270 271 /** 272 * Iterate over list nodes. 273 * See_Also: $(D byEntry) 274 */ 275 auto byNode() 276 { 277 return ByNode!Node(_head, _tail); 278 } 279 280 ///ditto 281 auto byNode() const 282 { 283 return ByNode!(const(Node))(_head, _tail); 284 } 285 286 /** 287 * Iterate over nodes mapped to Entry elements (useful for testing). 288 */ 289 auto byEntry() const { 290 return byNode().map!(node => node.toEntry()); 291 } 292 293 /** 294 * Represenation of list node. 295 */ 296 static struct Node { 297 private: 298 K _key; 299 V _value; 300 bool _hasKey; 301 Node* _prev; 302 Node* _next; 303 304 @trusted this(K key, V value) pure nothrow { 305 _key = key; 306 _value = value; 307 _hasKey = true; 308 } 309 310 @trusted this(V value) pure nothrow { 311 _value = value; 312 _hasKey = false; 313 } 314 315 @trusted void prev(Node* newPrev) pure nothrow { 316 _prev = newPrev; 317 } 318 319 @trusted void next(Node* newNext) pure nothrow { 320 _next = newNext; 321 } 322 323 public: 324 /** 325 * Get stored value. 326 */ 327 @trusted inout(V) value() inout pure nothrow { 328 return _value; 329 } 330 331 /** 332 * Set stored value. 333 */ 334 @trusted void value(V newValue) pure nothrow { 335 _value = newValue; 336 } 337 338 /** 339 * Tell whether this node is a key-value node. 340 */ 341 @trusted bool hasKey() const pure nothrow { 342 return _hasKey; 343 } 344 345 /** 346 * Key in key-value node. 347 */ 348 @trusted auto key() const pure nothrow { 349 return _key; 350 } 351 352 /** 353 * Access previous node in the list. 354 */ 355 @trusted inout(Node)* prev() inout pure nothrow { 356 return _prev; 357 } 358 359 /** 360 * Access next node in the list. 361 */ 362 @trusted inout(Node)* next() inout pure nothrow { 363 return _next; 364 } 365 366 /// 367 auto toEntry() const { 368 static if (is(V == class)) { 369 alias Rebindable!(const(V)) T; 370 if (hasKey()) { 371 return Entry!T(_key, rebindable(_value)); 372 } else { 373 return Entry!T(rebindable(_value)); 374 } 375 376 } else { 377 alias V T; 378 379 if (hasKey()) { 380 return Entry!T(_key, _value); 381 } else { 382 return Entry!T(_value); 383 } 384 } 385 } 386 } 387 388 /// Mapping of Node to structure. 389 static struct Entry(T = V) 390 { 391 private: 392 K _key; 393 T _value; 394 bool _hasKey; 395 396 public: 397 /// 398 this(T value) { 399 _value = value; 400 _hasKey = false; 401 } 402 403 /// 404 this(K key, T value) { 405 _key = key; 406 _value = value; 407 _hasKey = true; 408 } 409 410 /// 411 auto value() inout { 412 return _value; 413 } 414 415 /// 416 auto key() const { 417 return _key; 418 } 419 420 /// 421 bool hasKey() const { 422 return _hasKey; 423 } 424 } 425 426 private: 427 void putToFront(Node* toPut) 428 in { 429 assert(toPut !is null); 430 } 431 do { 432 if (_head) { 433 _head.prev = toPut; 434 toPut.next = _head; 435 _head = toPut; 436 } else { 437 _head = toPut; 438 _tail = toPut; 439 } 440 } 441 442 void putToBack(Node* toPut) 443 in { 444 assert(toPut !is null); 445 } 446 do { 447 if (_tail) { 448 _tail.next = toPut; 449 toPut.prev = _tail; 450 _tail = toPut; 451 } else { 452 _tail = toPut; 453 _head = toPut; 454 } 455 } 456 457 void putBefore(Node* node, Node* toPut) 458 in { 459 assert(toPut !is null); 460 assert(node !is null); 461 } 462 do { 463 toPut.prev = node.prev; 464 if (toPut.prev) { 465 toPut.prev.next = toPut; 466 } 467 toPut.next = node; 468 node.prev = toPut; 469 470 if (node is _head) { 471 _head = toPut; 472 } 473 } 474 475 void putAfter(Node* node, Node* toPut) 476 in { 477 assert(toPut !is null); 478 assert(node !is null); 479 } 480 do { 481 toPut.next = node.next; 482 if (toPut.next) { 483 toPut.next.prev = toPut; 484 } 485 toPut.prev = node; 486 node.next = toPut; 487 488 if (node is _tail) { 489 _tail = toPut; 490 } 491 } 492 493 void pullOut(Node* node) 494 in { 495 assert(node !is null); 496 } 497 do { 498 if (node.next) { 499 node.next.prev = node.prev; 500 } 501 if (node.prev) { 502 node.prev.next = node.next; 503 } 504 505 if (node is _head) { 506 _head = node.next; 507 } 508 if (node is _tail) { 509 _tail = node.prev; 510 } 511 512 node.next = null; 513 node.prev = null; 514 } 515 516 Node* givePlace(K key, V value) { 517 auto newNode = Node(key, value); 518 return givePlace(newNode); 519 } 520 521 Node* givePlace(V value) { 522 auto newNode = Node(value); 523 return givePlace(newNode); 524 } 525 526 Node* givePlace(ref Node node) { 527 Node* toReturn; 528 if (_lastEmpty is null) { 529 if (_storageSize < _storage.length) { 530 toReturn = &_storage[_storageSize]; 531 } else { 532 size_t storageIndex = (_storageSize - chunkSize) / chunkSize; 533 if (storageIndex >= _additonalStorages.length) { 534 _additonalStorages ~= (Node[chunkSize]).init; 535 } 536 537 size_t index = (_storageSize - chunkSize) % chunkSize; 538 toReturn = &_additonalStorages[storageIndex][index]; 539 } 540 541 _storageSize++; 542 } else { 543 toReturn = _lastEmpty; 544 _lastEmpty = _lastEmpty.prev; 545 if (_lastEmpty) { 546 _lastEmpty.next = null; 547 } 548 toReturn.next = null; 549 toReturn.prev = null; 550 } 551 552 toReturn._hasKey = node._hasKey; 553 toReturn._key = node._key; 554 toReturn._value = node._value; 555 556 if (toReturn.hasKey()) { 557 _dict[toReturn.key] = toReturn; 558 } 559 return toReturn; 560 } 561 562 Node[chunkSize] _storage; 563 Node[chunkSize][] _additonalStorages; 564 size_t _storageSize; 565 566 Node* _tail; 567 Node* _head; 568 Node* _lastEmpty; 569 Node*[K] _dict; 570 } 571 572 unittest 573 { 574 import std.range : isBidirectionalRange; 575 ListMap!(string, string) listMap; 576 static assert(isBidirectionalRange!(typeof(listMap.byNode()))); 577 } 578 579 unittest 580 { 581 import std.algorithm : equal; 582 import std.range : ElementType; 583 584 alias ListMap!(string, string, 2) TestListMap; 585 586 TestListMap listMap; 587 alias typeof(listMap).Node Node; 588 alias ElementType!(typeof(listMap.byEntry())) Entry; 589 590 assert(listMap.byEntry().empty); 591 assert(listMap.getNode("Nonexistent") is null); 592 593 listMap.insertFront("Start", "Fast"); 594 assert(listMap.getNode("Start") !is null); 595 assert(listMap.getNode("Start").key() == "Start"); 596 assert(listMap.getNode("Start").value() == "Fast"); 597 assert(listMap.getNode("Start").hasKey()); 598 assert(listMap.byEntry().equal([Entry("Start", "Fast")])); 599 assert(listMap.remove("Start")); 600 assert(listMap.byEntry().empty); 601 assert(listMap.getNode("Start") is null); 602 603 listMap.insertBack("Finish", "Bad"); 604 assert(listMap.byEntry().equal([Entry("Finish", "Bad")])); 605 assert(listMap.getNode("Finish").value() == "Bad"); 606 607 listMap.insertFront("Begin", "Good"); 608 assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad")])); 609 assert(listMap.getNode("Begin").value() == "Good"); 610 611 listMap.insertFront("Start", "Slow"); 612 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")])); 613 614 listMap.insertAfter(listMap.getNode("Begin"), "Middle", "Person"); 615 assert(listMap.getNode("Middle").value() == "Person"); 616 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Middle", "Person"), Entry("Finish", "Bad")])); 617 618 listMap.insertBefore(listMap.getNode("Middle"), "Mean", "Man"); 619 assert(listMap.getNode("Mean").value() == "Man"); 620 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Mean", "Man"), Entry("Middle", "Person"), Entry("Finish", "Bad")])); 621 622 assert(listMap.remove("Mean")); 623 assert(listMap.remove("Middle")); 624 625 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")])); 626 627 listMap.insertFront("New", "Era"); 628 assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad")])); 629 630 listMap.insertBack("Old", "Epoch"); 631 assert(listMap.byEntry().equal([Entry("New", "Era"), Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch")])); 632 633 listMap.moveToBack(listMap.getNode("New")); 634 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")])); 635 636 listMap.moveToFront(listMap.getNode("Begin")); 637 assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("Old", "Epoch"), Entry("New", "Era")])); 638 639 listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Start")); 640 assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era")])); 641 642 listMap.moveBefore(listMap.getNode("Finish"), listMap.getNode("Old")); 643 assert(listMap.byEntry().equal([Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("Start", "Slow"), Entry("New", "Era")])); 644 645 listMap.moveBefore(listMap.getNode("Begin"), listMap.getNode("Start")); 646 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("Finish", "Bad"), Entry("New", "Era")])); 647 648 listMap.moveAfter(listMap.getNode("New"), listMap.getNode("Finish")); 649 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Begin", "Good"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")])); 650 651 listMap.getNode("Begin").value = "Evil"; 652 assert(listMap.getNode("Begin").value() == "Evil"); 653 654 listMap.remove("Begin"); 655 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Old", "Epoch"), Entry("New", "Era"), Entry("Finish", "Bad")])); 656 listMap.remove("Old"); 657 listMap.remove("New"); 658 assert(!listMap.remove("Begin")); 659 660 Node* shebang = listMap.prepend("Shebang"); 661 Node* endOfStory = listMap.append("End of story"); 662 663 assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Finish", "Bad"), Entry("End of story")])); 664 665 Node* mid = listMap.addAfter(listMap.getNode("Start"), "Mid"); 666 Node* average = listMap.addBefore(listMap.getNode("Finish"), "Average"); 667 assert(listMap.byEntry().equal([Entry("Shebang"), Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")])); 668 669 listMap.remove(shebang); 670 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad"), Entry("End of story")])); 671 672 listMap.remove(endOfStory); 673 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")])); 674 675 listMap.moveToFront(listMap.getNode("Start")); 676 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")])); 677 listMap.moveToBack(listMap.getNode("Finish")); 678 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")])); 679 680 listMap.moveBefore(listMap.getNode("Start"), listMap.getNode("Start")); 681 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")])); 682 listMap.moveAfter(listMap.getNode("Finish"), listMap.getNode("Finish")); 683 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Average"), Entry("Finish", "Bad")])); 684 685 listMap.insertAfter(mid, "Center", "Universe"); 686 listMap.insertBefore(average, "Focus", "Cosmos"); 687 assert(listMap.byEntry().equal([Entry("Start", "Slow"), Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")])); 688 689 listMap.removeFront(); 690 assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average"), Entry("Finish", "Bad")])); 691 listMap.removeBack(); 692 693 assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")])); 694 695 assert(listMap.byEntry().retro.equal([Entry("Average"), Entry("Focus", "Cosmos"), Entry("Center", "Universe"), Entry("Mid")])); 696 697 auto byEntry = listMap.byEntry(); 698 Entry entry = byEntry.front; 699 assert(entry.value == "Mid"); 700 assert(!entry.hasKey()); 701 702 byEntry.popFront(); 703 assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")])); 704 byEntry.popBack(); 705 assert(byEntry.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")])); 706 707 entry = byEntry.back; 708 assert(entry.key == "Focus"); 709 assert(entry.value == "Cosmos"); 710 assert(entry.hasKey()); 711 712 auto saved = byEntry.save; 713 714 byEntry.popFront(); 715 assert(byEntry.equal([Entry("Focus", "Cosmos")])); 716 byEntry.popBack(); 717 assert(byEntry.empty); 718 719 assert(saved.equal([Entry("Center", "Universe"), Entry("Focus", "Cosmos")])); 720 saved.popBack(); 721 assert(saved.equal([Entry("Center", "Universe")])); 722 saved.popFront(); 723 assert(saved.empty); 724 725 static void checkConst(ref const TestListMap listMap) 726 { 727 assert(listMap.byEntry().equal([Entry("Mid"), Entry("Center", "Universe"), Entry("Focus", "Cosmos"), Entry("Average")])); 728 } 729 checkConst(listMap); 730 731 static class Class 732 { 733 this(string name) { 734 _name = name; 735 } 736 737 string name() const { 738 return _name; 739 } 740 private: 741 string _name; 742 } 743 744 alias ListMap!(string, Class) TestClassListMap; 745 TestClassListMap classListMap; 746 classListMap.insertFront("name", new Class("Name")); 747 classListMap.append(new Class("Value")); 748 auto byClass = classListMap.byEntry(); 749 assert(byClass.front.value.name == "Name"); 750 assert(byClass.front.key == "name"); 751 assert(byClass.back.value.name == "Value"); 752 } 753 754 /** 755 * Line in group. 756 */ 757 struct IniLikeLine 758 { 759 /** 760 * Type of line. 761 */ 762 enum Type 763 { 764 None = 0, /// deleted or invalid line 765 Comment = 1, /// a comment or empty line 766 KeyValue = 2 /// key-value pair 767 } 768 769 /** 770 * Contruct from comment. 771 */ 772 @nogc @safe static IniLikeLine fromComment(string comment) nothrow pure { 773 return IniLikeLine(comment, null, Type.Comment); 774 } 775 776 /** 777 * Construct from key and value. Value must be provided as it's written in a file, i.e in the escaped form. 778 */ 779 @nogc @safe static IniLikeLine fromKeyValue(string key, string value) nothrow pure { 780 return IniLikeLine(key, value, Type.KeyValue); 781 } 782 783 /** 784 * Get comment. 785 * Returns: Comment or empty string if type is not Type.Comment. 786 */ 787 @nogc @safe string comment() const nothrow pure { 788 return _type == Type.Comment ? _first : null; 789 } 790 791 /** 792 * Get key. 793 * Returns: Key or empty string if type is not Type.KeyValue 794 */ 795 @nogc @safe string key() const nothrow pure { 796 return _type == Type.KeyValue ? _first : null; 797 } 798 799 /** 800 * Get value. 801 * Returns: Value in the escaped form or empty string if type is not Type.KeyValue 802 */ 803 @nogc @safe string value() const nothrow pure { 804 return _type == Type.KeyValue ? _second : null; 805 } 806 807 /** 808 * Get type of line. 809 */ 810 @nogc @safe Type type() const nothrow pure { 811 return _type; 812 } 813 private: 814 string _first; 815 string _second; 816 Type _type = Type.None; 817 } 818 819 820 /** 821 * This class represents the group (section) of key-value entries in the ini-like file. 822 * Instances of this class can be created only in the context of $(D IniLikeFile) or its derivatives. 823 * Values are stored in the escaped form, but the interface allows to set and get values in both escaped and unescaped forms. 824 * Note: Keys are case-sensitive. 825 */ 826 class IniLikeGroup 827 { 828 private: 829 alias ListMap!(string, IniLikeLine) LineListMap; 830 831 public: 832 /// 833 enum InvalidKeyPolicy : ubyte { 834 ///Throw error on invalid key 835 throwError, 836 ///Skip invalid key 837 skip, 838 ///Save entry with invalid key. 839 save 840 } 841 842 /** 843 * Create $(D IniLikeGroup) instance with given name. 844 */ 845 protected @nogc @safe this(string groupName) nothrow { 846 _name = groupName; 847 } 848 849 private @safe final string setKeyValueImpl(string key, string value) 850 in { 851 assert(!value.needEscaping); 852 } 853 do { 854 import std.stdio; 855 auto node = _listMap.getNode(key); 856 if (node) { 857 node.value = IniLikeLine.fromKeyValue(key, value); 858 } else { 859 _listMap.insertBack(key, IniLikeLine.fromKeyValue(key, value)); 860 } 861 return value; 862 } 863 864 /** 865 * Check if the group contains a value associated with the key. 866 */ 867 @nogc @safe final bool contains(string key) const nothrow pure { 868 return _listMap.getNode(key) !is null; 869 } 870 871 /** 872 * Get value by key in escaped form. 873 * Returns: The escaped value associated with the key, or empty string if group does not contain such item. 874 * See_Also: $(D setEscapedValue), $(D unescapedValue) 875 */ 876 @nogc @safe final string escapedValue(string key) const nothrow pure { 877 auto node = _listMap.getNode(key); 878 if (node) { 879 return node.value.value; 880 } else { 881 return null; 882 } 883 } 884 885 /** 886 * Perform locale matching lookup as described in $(LINK2 https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html, Localized values for keys). 887 * Params: 888 * key = Non-localized key. 889 * locale = Locale in intereset. 890 * nonLocaleFallback = Allow fallback to non-localized version. 891 * Returns: 892 * The escaped localized value associated with key and locale, 893 * or the value associated with non-localized key if group does not contain localized value and nonLocaleFallback is true. 894 * See_Also: $(D setEscapedValue), $(D unescapedValue) 895 */ 896 @safe final string escapedValue(string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure { 897 //Any ideas how to get rid of this boilerplate and make less allocations? 898 const t = parseLocaleName(locale); 899 auto lang = t.lang; 900 auto country = t.country; 901 auto modifier = t.modifier; 902 903 if (lang.length) { 904 string pick; 905 if (country.length && modifier.length) { 906 pick = escapedValue(localizedKey(key, locale)); 907 if (pick !is null) { 908 return pick; 909 } 910 } 911 if (country.length) { 912 pick = escapedValue(localizedKey(key, lang, country)); 913 if (pick !is null) { 914 return pick; 915 } 916 } 917 if (modifier.length) { 918 pick = escapedValue(localizedKey(key, lang, string.init, modifier)); 919 if (pick !is null) { 920 return pick; 921 } 922 } 923 pick = escapedValue(localizedKey(key, lang, string.init)); 924 if (pick !is null) { 925 return pick; 926 } 927 } 928 929 if (nonLocaleFallback) { 930 return escapedValue(key); 931 } else { 932 return null; 933 } 934 } 935 936 /// 937 unittest 938 { 939 auto lilf = new IniLikeFile; 940 lilf.addGenericGroup("Entry"); 941 auto group = lilf.group("Entry"); 942 assert(group.groupName == "Entry"); 943 group.setEscapedValue("Name", "Programmer"); 944 group.setEscapedValue("Name[ru_RU]", "Разработчик"); 945 group.setEscapedValue("Name[ru@jargon]", "Кодер"); 946 group.setEscapedValue("Name[ru]", "Программист"); 947 group.setEscapedValue("Name[de_DE@dialect]", "Programmierer"); //just example 948 group.setEscapedValue("Name[fr_FR]", "Programmeur"); 949 group.setEscapedValue("GenericName", "Program"); 950 group.setEscapedValue("GenericName[ru]", "Программа"); 951 assert(group.escapedValue("Name") == "Programmer"); 952 assert(group.escapedValue("Name", "ru@jargon") == "Кодер"); 953 assert(group.escapedValue("Name", "ru_RU@jargon") == "Разработчик"); 954 assert(group.escapedValue("Name", "ru") == "Программист"); 955 assert(group.escapedValue("Name", "ru_RU.UTF-8") == "Разработчик"); 956 assert(group.escapedValue("Name", "nonexistent locale") == "Programmer"); 957 assert(group.escapedValue("Name", "de_DE@dialect") == "Programmierer"); 958 assert(group.escapedValue("Name", "fr_FR.UTF-8") == "Programmeur"); 959 assert(group.escapedValue("GenericName", "ru_RU") == "Программа"); 960 assert(group.escapedValue("GenericName", "fr_FR") == "Program"); 961 assert(group.escapedValue("GenericName", "fr_FR", No.nonLocaleFallback) is null); 962 } 963 964 private @trusted final bool validateKeyValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy) 965 { 966 validateValue(key, value); 967 968 try { 969 validateKey(key, value); 970 return true; 971 } catch(IniLikeEntryException e) { 972 final switch(invalidKeyPolicy) { 973 case InvalidKeyPolicy.throwError: 974 throw e; 975 case InvalidKeyPolicy.save: 976 validateKeyImpl(key, value, _name); 977 return true; 978 case InvalidKeyPolicy.skip: 979 validateKeyImpl(key, value, _name); 980 return false; 981 } 982 } 983 } 984 985 /** 986 * Set value associated with key. 987 * Params: 988 * key = Key to associate value with. 989 * value = Value to set. Must be in escaped form. 990 * invalidKeyPolicy = Policy about invalid keys. 991 * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid or value needs to be escaped. 992 * See_Also: $(D escapedValue), $(D setUnescapedValue) 993 */ 994 @safe final string setEscapedValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) 995 { 996 if (validateKeyValue(key, value, invalidKeyPolicy)) { 997 return setKeyValueImpl(key, value); 998 } 999 return null; 1000 } 1001 1002 /** 1003 * Set value associated with key and locale. 1004 * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid or value needs to be escaped. 1005 * See_Also: $(D escapedValue), $(D setUnescapedValue) 1006 */ 1007 @safe final string setEscapedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) { 1008 return setEscapedValue(localizedKey(key, locale), value, invalidKeyPolicy); 1009 } 1010 1011 /** 1012 * Get value by key. 1013 * Params: 1014 * key = Key of value. 1015 * locale = Optional locale to use in localized lookup if not empty. 1016 * nonLocaleFallback = Allow fallback to non-localized version. 1017 * Returns: The unescaped value associated with key or null if not found. 1018 * See_Also: $(D escapedValue), $(D setUnescapedValue) 1019 */ 1020 @safe final string unescapedValue(string key, string locale = null, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) const nothrow pure { 1021 if (locale.length) { 1022 return escapedValue(key, locale, nonLocaleFallback).unescapeValue(); 1023 } else { 1024 return escapedValue(key).unescapeValue(); 1025 } 1026 } 1027 1028 /** 1029 * Set value by key. The value is considered to be in the unescaped form. 1030 * Throws: $(D inilike.exception.IniLikeEntryException) if key or value is not valid. 1031 * See_Also: $(D unescapedValue), $(D setEscapedValue) 1032 */ 1033 @safe final string setUnescapedValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) { 1034 value = value.escapeValue(); 1035 return setEscapedValue(key, value, invalidKeyPolicy); 1036 } 1037 1038 ///ditto, localized version 1039 @safe final string setUnescapedValue(string key, string locale, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) { 1040 value = value.escapeValue(); 1041 return setEscapedValue(key, locale, value, invalidKeyPolicy); 1042 } 1043 1044 /** 1045 * Removes entry by key. Do nothing if no value associated with key found. 1046 * Returns: true if entry was removed, false otherwise. 1047 */ 1048 @safe final bool removeEntry(string key) nothrow pure { 1049 return _listMap.remove(key); 1050 } 1051 1052 ///ditto, but remove entry by localized key. 1053 @safe final bool removeEntry(string key, string locale) nothrow pure { 1054 return removeEntry(localizedKey(key, locale)); 1055 } 1056 1057 ///ditto, but remove entry by node. 1058 @safe final void removeEntry(LineNode node) nothrow pure { 1059 _listMap.remove(node.node); 1060 } 1061 1062 private @nogc @safe static auto staticByKeyValue(Range)(Range nodes) nothrow { 1063 return nodes.map!(node => node.value).filter!(v => v.type == IniLikeLine.Type.KeyValue).map!(v => keyValueTuple(v.key, v.value)); 1064 } 1065 1066 /** 1067 * Iterate by Key-Value pairs. Values are left in escaped form. 1068 * Returns: Range of Tuple!(string, "key", string, "value"). 1069 * See_Also: $(D escapedValue), $(D byIniLine) 1070 */ 1071 @nogc @safe final auto byKeyValue() const nothrow { 1072 return staticByKeyValue(_listMap.byNode); 1073 } 1074 1075 /** 1076 * Empty range of the same type as $(D byKeyValue). Can be used in derived classes if it's needed to have an empty range. 1077 * Returns: Empty range of Tuple!(string, "key", string, "value"). 1078 */ 1079 @nogc @safe static auto emptyByKeyValue() nothrow { 1080 const ListMap!(string, IniLikeLine) listMap; 1081 return staticByKeyValue(listMap.byNode); 1082 } 1083 1084 /// 1085 unittest 1086 { 1087 assert(emptyByKeyValue().empty); 1088 auto group = new IniLikeGroup("Group name"); 1089 static assert(is(typeof(emptyByKeyValue()) == typeof(group.byKeyValue()) )); 1090 } 1091 1092 /** 1093 * Get name of this group. 1094 * Returns: The name of this group. 1095 */ 1096 @nogc @safe final string groupName() const nothrow pure { 1097 return _name; 1098 } 1099 1100 /** 1101 * Returns: Range of $(D IniLikeLine)s included in this group. 1102 * See_Also: $(D byNode), $(D byKeyValue) 1103 */ 1104 @trusted final auto byIniLine() const { 1105 return _listMap.byNode.map!(node => node.value); 1106 } 1107 1108 /** 1109 * Wrapper for internal ListMap node. 1110 */ 1111 static struct LineNode 1112 { 1113 private: 1114 LineListMap.Node* node; 1115 string groupName; 1116 public: 1117 /** 1118 * Get key of node. 1119 */ 1120 @nogc @trusted string key() const pure nothrow { 1121 if (node) { 1122 return node.key; 1123 } else { 1124 return null; 1125 } 1126 } 1127 1128 /** 1129 * Get $(D IniLikeLine) pointed by node. 1130 */ 1131 @nogc @trusted IniLikeLine line() const pure nothrow { 1132 if (node) { 1133 return node.value; 1134 } else { 1135 return IniLikeLine.init; 1136 } 1137 } 1138 1139 /** 1140 * Set value for line. If underline line is comment, than newValue is set as comment. 1141 * Prerequisites: Node must be non-null. 1142 */ 1143 @trusted void setEscapedValue(string newValue) pure { 1144 auto type = node.value.type; 1145 if (type == IniLikeLine.Type.KeyValue) { 1146 node.value = IniLikeLine.fromKeyValue(node.value.key, newValue); 1147 } else if (type == IniLikeLine.Type.Comment) { 1148 node.value = makeCommentLine(newValue); 1149 } 1150 } 1151 1152 /** 1153 * Check if underlined node is null. 1154 */ 1155 @nogc @safe bool isNull() const pure nothrow { 1156 return node is null; 1157 } 1158 } 1159 1160 private @trusted auto lineNode(LineListMap.Node* node) pure nothrow { 1161 return LineNode(node, groupName()); 1162 } 1163 1164 /** 1165 * Iterate over nodes of internal list. 1166 * See_Also: $(D getNode), $(D byIniLine) 1167 */ 1168 @trusted auto byNode() { 1169 return _listMap.byNode().map!(node => lineNode(node)); 1170 } 1171 1172 /** 1173 * Get internal list node for key. 1174 * See_Also: $(D byNode) 1175 */ 1176 @trusted final auto getNode(string key) { 1177 return lineNode(_listMap.getNode(key)); 1178 } 1179 1180 /** 1181 * Add key-value entry without diret association of the value with the key. Can be used to add duplicates. 1182 */ 1183 final auto appendValue(string key, string value, InvalidKeyPolicy invalidKeyPolicy = InvalidKeyPolicy.throwError) { 1184 if (validateKeyValue(key, value, invalidKeyPolicy)) { 1185 return lineNode(_listMap.append(IniLikeLine.fromKeyValue(key, value))); 1186 } else { 1187 return lineNode(null); 1188 } 1189 } 1190 1191 /** 1192 * Add comment line into the group. 1193 * Returns: Added $(D LineNode). 1194 * See_Also: $(D byIniLine), $(D prependComment), $(D addCommentBefore), $(D addCommentAfter) 1195 */ 1196 @safe final auto appendComment(string comment) nothrow pure { 1197 return lineNode(_listMap.append(makeCommentLine(comment))); 1198 } 1199 1200 /** 1201 * Add comment line at the start of group (after group header, before any key-value pairs). 1202 * Returns: Added $(D LineNode). 1203 * See_Also: $(D byIniLine), $(D appendComment), $(D addCommentBefore), $(D addCommentAfter) 1204 */ 1205 @safe final auto prependComment(string comment) nothrow pure { 1206 return lineNode(_listMap.prepend(makeCommentLine(comment))); 1207 } 1208 1209 /** 1210 * Add comment before some node. 1211 * Returns: Added $(D LineNode). 1212 * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentAfter) 1213 */ 1214 @trusted final auto addCommentBefore(LineNode node, string comment) nothrow pure 1215 in { 1216 assert(!node.isNull()); 1217 } 1218 do { 1219 return _listMap.addBefore(node.node, makeCommentLine(comment)); 1220 } 1221 1222 /** 1223 * Add comment after some node. 1224 * Returns: Added $(D LineNode). 1225 * See_Also: $(D byIniLine), $(D appendComment), $(D prependComment), $(D getNode), $(D addCommentBefore) 1226 */ 1227 @trusted final auto addCommentAfter(LineNode node, string comment) nothrow pure 1228 in { 1229 assert(!node.isNull()); 1230 } 1231 do { 1232 return _listMap.addAfter(node.node, makeCommentLine(comment)); 1233 } 1234 1235 /** 1236 * Move line to the start of group. 1237 * Prerequisites: $(D toMove) is not null and belongs to this group. 1238 * See_Also: $(D getNode) 1239 */ 1240 @trusted final void moveLineToFront(LineNode toMove) nothrow pure { 1241 _listMap.moveToFront(toMove.node); 1242 } 1243 1244 /** 1245 * Move line to the end of group. 1246 * Prerequisites: $(D toMove) is not null and belongs to this group. 1247 * See_Also: $(D getNode) 1248 */ 1249 @trusted final void moveLineToBack(LineNode toMove) nothrow pure { 1250 _listMap.moveToBack(toMove.node); 1251 } 1252 1253 /** 1254 * Move line before other line in the group. 1255 * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group. 1256 * See_Also: $(D getNode) 1257 */ 1258 @trusted final void moveLineBefore(LineNode other, LineNode toMove) nothrow pure { 1259 _listMap.moveBefore(other.node, toMove.node); 1260 } 1261 1262 /** 1263 * Move line after other line in the group. 1264 * Prerequisites: $(D toMove) and $(D other) are not null and belong to this group. 1265 * See_Also: $(D getNode) 1266 */ 1267 @trusted final void moveLineAfter(LineNode other, LineNode toMove) nothrow pure { 1268 _listMap.moveAfter(other.node, toMove.node); 1269 } 1270 1271 private: 1272 @trusted static void validateKeyImpl(string key, string value, string groupName) 1273 { 1274 if (key.empty || key.strip.empty) { 1275 throw new IniLikeEntryException("key must not be empty", groupName, key, value); 1276 } 1277 if (key.isComment()) { 1278 throw new IniLikeEntryException("key must not start with #", groupName, key, value); 1279 } 1280 if (key.canFind('=')) { 1281 throw new IniLikeEntryException("key must not have '=' character in it", groupName, key, value); 1282 } 1283 if (key.needEscaping()) { 1284 throw new IniLikeEntryException("key must not contain new line characters", groupName, key, value); 1285 } 1286 } 1287 1288 protected: 1289 /** 1290 * Validate key before setting value to key for this group and throw exception if not valid. 1291 * Can be reimplemented in derived classes. 1292 * 1293 * Default implementation checks if key is not empty string, does not look like comment and does not contain new line or carriage return characters. 1294 * Params: 1295 * key = key to validate. 1296 * value = value that is being set to key. 1297 * Throws: $(D inilike.exception.IniLikeEntryException) if either key is invalid. 1298 * See_Also: $(D validateValue) 1299 * Note: 1300 * Implementer should ensure that their implementation still validates key for format consistency (i.e. no new line characters, etc.). 1301 * If not sure, just call super.validateKey(key, value) in your implementation. 1302 */ 1303 @trusted void validateKey(string key, string value) const { 1304 validateKeyImpl(key, value, _name); 1305 } 1306 1307 /// 1308 unittest 1309 { 1310 auto ilf = new IniLikeFile(); 1311 ilf.addGenericGroup("Group"); 1312 1313 auto entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("", "Value1")); 1314 assert(entryException !is null); 1315 assert(entryException.groupName == "Group"); 1316 assert(entryException.key == ""); 1317 assert(entryException.value == "Value1"); 1318 1319 entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue(" ", "Value2")); 1320 assert(entryException !is null); 1321 assert(entryException.key == " "); 1322 assert(entryException.value == "Value2"); 1323 1324 entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("New\nLine", "Value3")); 1325 assert(entryException !is null); 1326 assert(entryException.key == "New\nLine"); 1327 assert(entryException.value == "Value3"); 1328 1329 entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("# Comment", "Value4")); 1330 assert(entryException !is null); 1331 assert(entryException.key == "# Comment"); 1332 assert(entryException.value == "Value4"); 1333 1334 entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("Everyone=Is", "Equal")); 1335 assert(entryException !is null); 1336 assert(entryException.key == "Everyone=Is"); 1337 assert(entryException.value == "Equal"); 1338 } 1339 1340 /** 1341 * Validate value for key before setting value to key for this group and throw exception if not valid. 1342 * Can be reimplemented in derived classes. 1343 * The key is provided because you may want to implement specific checks depending on the key name. 1344 * 1345 * Default implementation checks if value is escaped. It does not check the key in any way. 1346 * Params: 1347 * key = key the value is being set to. 1348 * value = value to validate. Considered to be escaped. 1349 * Throws: $(D inilike.exception.IniLikeEntryException) if value is invalid. 1350 * See_Also: $(D validateKey) 1351 */ 1352 @trusted void validateValue(string key, string value) const { 1353 if (value.needEscaping()) { 1354 throw new IniLikeEntryException("The value needs to be escaped", _name, key, value); 1355 } 1356 } 1357 1358 /// 1359 unittest 1360 { 1361 auto ilf = new IniLikeFile(); 1362 ilf.addGenericGroup("Group"); 1363 1364 auto entryException = collectException!IniLikeEntryException(ilf.group("Group").setEscapedValue("Key", "New\nline")); 1365 assert(entryException !is null); 1366 assert(entryException.key == "Key"); 1367 assert(entryException.value == "New\nline"); 1368 } 1369 private: 1370 LineListMap _listMap; 1371 string _name; 1372 } 1373 1374 /** 1375 * Ini-like file. 1376 */ 1377 class IniLikeFile 1378 { 1379 private: 1380 alias ListMap!(string, IniLikeGroup, 8) GroupListMap; 1381 public: 1382 ///Behavior on duplicate key in the group. 1383 enum DuplicateKeyPolicy : ubyte 1384 { 1385 ///Throw error on entry with duplicate key. 1386 throwError, 1387 ///Skip duplicate without error. 1388 skip, 1389 ///Preserve all duplicates in the list. The first found value remains accessible by key. 1390 preserve 1391 } 1392 1393 ///Behavior on group with duplicate name in the file. 1394 enum DuplicateGroupPolicy : ubyte 1395 { 1396 ///Throw error on group with duplicate name. 1397 throwError, 1398 ///Skip duplicate without error. 1399 skip, 1400 ///Preserve all duplicates in the list. The first found group remains accessible by key. 1401 preserve 1402 } 1403 1404 ///Behavior of ini-like file reading. 1405 static struct ReadOptions 1406 { 1407 ///Behavior on groups with duplicate names. 1408 DuplicateGroupPolicy duplicateGroupPolicy = DuplicateGroupPolicy.throwError; 1409 ///Behavior on duplicate keys. 1410 DuplicateKeyPolicy duplicateKeyPolicy = DuplicateKeyPolicy.throwError; 1411 1412 ///Behavior on invalid keys. 1413 IniLikeGroup.InvalidKeyPolicy invalidKeyPolicy = IniLikeGroup.InvalidKeyPolicy.throwError; 1414 1415 ///Whether to preserve comments on reading. 1416 Flag!"preserveComments" preserveComments = Yes.preserveComments; 1417 1418 ///Setting parameters in any order, leaving not mentioned ones in default state. 1419 @nogc @safe this(Args...)(Args args) nothrow pure { 1420 foreach(arg; args) { 1421 assign(arg); 1422 } 1423 } 1424 1425 /// 1426 unittest 1427 { 1428 ReadOptions readOptions; 1429 1430 readOptions = ReadOptions(No.preserveComments); 1431 assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.throwError); 1432 assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.throwError); 1433 assert(!readOptions.preserveComments); 1434 1435 readOptions = ReadOptions(DuplicateGroupPolicy.skip, DuplicateKeyPolicy.preserve); 1436 assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.skip); 1437 assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.preserve); 1438 assert(readOptions.preserveComments); 1439 1440 const duplicateGroupPolicy = DuplicateGroupPolicy.preserve; 1441 immutable duplicateKeyPolicy = DuplicateKeyPolicy.skip; 1442 const preserveComments = No.preserveComments; 1443 readOptions = ReadOptions(duplicateGroupPolicy, IniLikeGroup.InvalidKeyPolicy.skip, preserveComments, duplicateKeyPolicy); 1444 assert(readOptions.duplicateGroupPolicy == DuplicateGroupPolicy.preserve); 1445 assert(readOptions.duplicateKeyPolicy == DuplicateKeyPolicy.skip); 1446 assert(readOptions.invalidKeyPolicy == IniLikeGroup.InvalidKeyPolicy.skip); 1447 } 1448 1449 /** 1450 * Assign arg to the struct member of corresponding type. 1451 * Note: 1452 * It's compile-time error to assign parameter of type which is not part of ReadOptions. 1453 */ 1454 @nogc @safe void assign(T)(T arg) nothrow pure { 1455 alias Unqual!(T) ArgType; 1456 static if (is(ArgType == DuplicateKeyPolicy)) { 1457 duplicateKeyPolicy = arg; 1458 } else static if (is(ArgType == DuplicateGroupPolicy)) { 1459 duplicateGroupPolicy = arg; 1460 } else static if (is(ArgType == Flag!"preserveComments")) { 1461 preserveComments = arg; 1462 } else static if (is(ArgType == IniLikeGroup.InvalidKeyPolicy)) { 1463 invalidKeyPolicy = arg; 1464 } else { 1465 static assert(false, "Unknown argument type " ~ typeof(arg).stringof); 1466 } 1467 } 1468 } 1469 1470 /// 1471 unittest 1472 { 1473 string contents = `# The first comment 1474 [First Entry] 1475 # Comment 1476 GenericName=File manager 1477 GenericName[ru]=Файловый менеджер 1478 # Another comment 1479 [Another Group] 1480 Name=Commander 1481 # The last comment`; 1482 1483 alias IniLikeFile.ReadOptions ReadOptions; 1484 alias IniLikeFile.DuplicateKeyPolicy DuplicateKeyPolicy; 1485 alias IniLikeFile.DuplicateGroupPolicy DuplicateGroupPolicy; 1486 1487 IniLikeFile ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(No.preserveComments)); 1488 assert(!ilf.readOptions().preserveComments); 1489 assert(ilf.leadingComments().empty); 1490 assert(equal( 1491 ilf.group("First Entry").byIniLine(), 1492 [IniLikeLine.fromKeyValue("GenericName", "File manager"), IniLikeLine.fromKeyValue("GenericName[ru]", "Файловый менеджер")] 1493 )); 1494 assert(equal( 1495 ilf.group("Another Group").byIniLine(), 1496 [IniLikeLine.fromKeyValue("Name", "Commander")] 1497 )); 1498 1499 contents = `[Group] 1500 Duplicate=First 1501 Key=Value 1502 Duplicate=Second`; 1503 1504 ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.skip)); 1505 assert(equal( 1506 ilf.group("Group").byIniLine(), 1507 [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value")] 1508 )); 1509 1510 ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateKeyPolicy.preserve)); 1511 assert(equal( 1512 ilf.group("Group").byIniLine(), 1513 [IniLikeLine.fromKeyValue("Duplicate", "First"), IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromKeyValue("Duplicate", "Second")] 1514 )); 1515 assert(ilf.group("Group").escapedValue("Duplicate") == "First"); 1516 1517 contents = `[Duplicate] 1518 Key=First 1519 [Group] 1520 [Duplicate] 1521 Key=Second`; 1522 1523 ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.preserve)); 1524 auto byGroup = ilf.byGroup(); 1525 assert(byGroup.front.escapedValue("Key") == "First"); 1526 assert(byGroup.back.escapedValue("Key") == "Second"); 1527 1528 auto byNode = ilf.byNode(); 1529 assert(byNode.front.group.groupName == "Duplicate"); 1530 assert(byNode.front.key == "Duplicate"); 1531 assert(byNode.back.key is null); 1532 1533 contents = `[Duplicate] 1534 Key=First 1535 [Group] 1536 [Duplicate] 1537 Key=Second`; 1538 1539 ilf = new IniLikeFile(iniLikeStringReader(contents), null, ReadOptions(DuplicateGroupPolicy.skip)); 1540 auto byGroup2 = ilf.byGroup(); 1541 assert(byGroup2.front.escapedValue("Key") == "First"); 1542 assert(byGroup2.back.groupName == "Group"); 1543 } 1544 1545 /** 1546 * Behavior of ini-like file saving. 1547 * See_Also: $(D save) 1548 */ 1549 static struct WriteOptions 1550 { 1551 ///Whether to preserve comments (lines that starts with '#') on saving. 1552 Flag!"preserveComments" preserveComments = Yes.preserveComments; 1553 ///Whether to preserve empty lines on saving. 1554 Flag!"preserveEmptyLines" preserveEmptyLines = Yes.preserveEmptyLines; 1555 /** 1556 * Whether to write empty line after each group except for the last. 1557 * New line is not written when it already exists before the next group. 1558 */ 1559 Flag!"lineBetweenGroups" lineBetweenGroups = No.lineBetweenGroups; 1560 1561 /** 1562 * Pretty mode. Save comments, skip existing new lines, add line before the next group. 1563 */ 1564 @nogc @safe static auto pretty() nothrow pure { 1565 return WriteOptions(Yes.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups); 1566 } 1567 1568 /** 1569 * Exact mode. Save all comments and empty lines as is. 1570 */ 1571 @nogc @safe static auto exact() nothrow pure { 1572 return WriteOptions(Yes.preserveComments, Yes.preserveEmptyLines, No.lineBetweenGroups); 1573 } 1574 1575 @nogc @safe this(Args...)(Args args) nothrow pure { 1576 foreach(arg; args) { 1577 assign(arg); 1578 } 1579 } 1580 1581 /** 1582 * Assign arg to the struct member of corresponding type. 1583 * Note: 1584 * It's compile-time error to assign parameter of type which is not part of $(D WriteOptions). 1585 */ 1586 @nogc @safe void assign(T)(T arg) nothrow pure { 1587 alias Unqual!(T) ArgType; 1588 static if (is(ArgType == Flag!"preserveEmptyLines")) { 1589 preserveEmptyLines = arg; 1590 } else static if (is(ArgType == Flag!"lineBetweenGroups")) { 1591 lineBetweenGroups = arg; 1592 } else static if (is(ArgType == Flag!"preserveComments")) { 1593 preserveComments = arg; 1594 } else { 1595 static assert(false, "Unknown argument type " ~ typeof(arg).stringof); 1596 } 1597 } 1598 } 1599 1600 /** 1601 * Wrapper for internal $(D ListMap) node. 1602 */ 1603 static struct GroupNode 1604 { 1605 private: 1606 GroupListMap.Node* node; 1607 public: 1608 /** 1609 * Key the group associated with. 1610 * While every group has groupName, it might be added to the group list without association, therefore will not have key. 1611 */ 1612 @nogc @trusted string key() const pure nothrow { 1613 if (node) { 1614 return node.key(); 1615 } else { 1616 return null; 1617 } 1618 } 1619 1620 /** 1621 * Access underlined group. 1622 */ 1623 @nogc @trusted IniLikeGroup group() pure nothrow { 1624 if (node) { 1625 return node.value(); 1626 } else { 1627 return null; 1628 } 1629 } 1630 1631 /** 1632 * Check if underlined node is null. 1633 */ 1634 @nogc @safe bool isNull() pure nothrow const { 1635 return node is null; 1636 } 1637 } 1638 1639 protected: 1640 /** 1641 * Insert group into $(D IniLikeFile) object and use its name as key. 1642 * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object. 1643 */ 1644 @trusted final auto insertGroup(IniLikeGroup group) 1645 in { 1646 assert(group !is null); 1647 } 1648 do { 1649 return GroupNode(_listMap.insertBack(group.groupName, group)); 1650 } 1651 1652 /** 1653 * Append group to group list without associating group name with it. Can be used to add groups with duplicated names. 1654 * Prerequisites: group must be non-null. It also should not be held by some other $(D IniLikeFile) object. 1655 */ 1656 @trusted final auto putGroup(IniLikeGroup group) 1657 in { 1658 assert(group !is null); 1659 } 1660 do { 1661 return GroupNode(_listMap.append(group)); 1662 } 1663 1664 /** 1665 * Add comment before groups. 1666 * This function is called only in constructor and can be reimplemented in derived classes. 1667 * Params: 1668 * comment = Comment line to add. 1669 */ 1670 @trusted void onLeadingComment(string comment) { 1671 if (_readOptions.preserveComments) { 1672 appendLeadingComment(comment); 1673 } 1674 } 1675 1676 /** 1677 * Add comment for group. 1678 * This function is called only in constructor and can be reimplemented in derived classes. 1679 * Params: 1680 * comment = Comment line to add. 1681 * currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded) 1682 * groupName = The name of the currently parsed group. Set even if currentGroup is null. 1683 * See_Also: $(D createGroup), $(D IniLikeGroup.appendComment) 1684 */ 1685 @trusted void onCommentInGroup(string comment, IniLikeGroup currentGroup, string groupName) 1686 { 1687 if (currentGroup && _readOptions.preserveComments) { 1688 currentGroup.appendComment(comment); 1689 } 1690 } 1691 1692 /** 1693 * Add key/value pair for group. 1694 * This function is called only in constructor and can be reimplemented in derived classes. 1695 * Params: 1696 * key = Key to insert or set. 1697 * value = Value to set for key. 1698 * currentGroup = The group returned recently by createGroup during parsing. Can be null (e.g. if discarded) 1699 * groupName = The name of the currently parsed group. Set even if currentGroup is null. 1700 * See_Also: $(D createGroup) 1701 */ 1702 @trusted void onKeyValue(string key, string value, IniLikeGroup currentGroup, string groupName) 1703 { 1704 if (currentGroup) { 1705 if (currentGroup.contains(key)) { 1706 final switch(_readOptions.duplicateKeyPolicy) { 1707 case DuplicateKeyPolicy.throwError: 1708 throw new IniLikeEntryException("key already exists", groupName, key, value); 1709 case DuplicateKeyPolicy.skip: 1710 break; 1711 case DuplicateKeyPolicy.preserve: 1712 currentGroup.appendValue(key, value, _readOptions.invalidKeyPolicy); 1713 break; 1714 } 1715 } else { 1716 currentGroup.setEscapedValue(key, value, _readOptions.invalidKeyPolicy); 1717 } 1718 } 1719 } 1720 1721 /** 1722 * Create $(D IniLikeGroup) by groupName during file parsing. 1723 * 1724 * This function can be reimplemented in derived classes, e.g. to insert additional checks. 1725 * Returned value is later passed to $(D onCommentInGroup) and $(D onKeyValue) methods as currentGroup. 1726 * Reimplemented method also is allowed to return null. 1727 * Default implementation just creates and returns empty $(D IniLikeGroup) with name set to groupName. 1728 * If group already exists and $(D DuplicateGroupPolicy) is skip, then null is returned. 1729 * Throws: 1730 * $(D inilike.exception.IniLikeGroupException) if group with such name already exists. 1731 * $(D inilike.exception.IniLikeException) if groupName is empty. 1732 * See_Also: 1733 * $(D onKeyValue), $(D onCommentInGroup) 1734 */ 1735 @trusted IniLikeGroup onGroup(string groupName) { 1736 if (group(groupName) !is null) { 1737 final switch(_readOptions.duplicateGroupPolicy) { 1738 case DuplicateGroupPolicy.throwError: 1739 throw new IniLikeGroupException("group with such name already exists", groupName); 1740 case DuplicateGroupPolicy.skip: 1741 return null; 1742 case DuplicateGroupPolicy.preserve: 1743 auto toPut = createGroupByName(groupName); 1744 if (toPut) { 1745 putGroup(toPut); 1746 } 1747 return toPut; 1748 } 1749 } else { 1750 auto toInsert = createGroupByName(groupName); 1751 if (toInsert) { 1752 insertGroup(toInsert); 1753 } 1754 return toInsert; 1755 } 1756 } 1757 1758 /** 1759 * Reimplement in derive class. 1760 */ 1761 @trusted IniLikeGroup createGroupByName(string groupName) { 1762 return createEmptyGroup(groupName); 1763 } 1764 1765 /** 1766 * Can be used in derived classes to create instance of IniLikeGroup. 1767 * Throws: $(D inilike.exception.IniLikeException) if groupName is empty. 1768 */ 1769 @safe static createEmptyGroup(string groupName) { 1770 if (groupName.length == 0) { 1771 throw new IniLikeException("empty group name"); 1772 } 1773 return new IniLikeGroup(groupName); 1774 } 1775 public: 1776 /** 1777 * Construct empty $(D IniLikeFile), i.e. without any groups or values 1778 */ 1779 @nogc @safe this() nothrow { 1780 1781 } 1782 1783 /** 1784 * Read from file. 1785 * Throws: 1786 * $(B ErrnoException) if file could not be opened. 1787 * $(D inilike.exception.IniLikeReadException) if error occured while reading the file. 1788 */ 1789 @trusted this(string fileName, ReadOptions readOptions = ReadOptions.init) { 1790 this(iniLikeFileReader(fileName), fileName, readOptions); 1791 } 1792 1793 /** 1794 * Read from range of $(D inilike.range.IniLikeReader). 1795 * Note: All exceptions thrown within constructor are turning into $(D IniLikeReadException). 1796 * Throws: 1797 * $(D inilike.exception.IniLikeReadException) if error occured while parsing. 1798 */ 1799 this(IniLikeReader)(IniLikeReader reader, string fileName = null, ReadOptions readOptions = ReadOptions.init) 1800 { 1801 _readOptions = readOptions; 1802 IniLikeGroup currentGroup; 1803 1804 auto onMyLeadingComment = delegate void(string line) { 1805 onLeadingComment(line); 1806 }; 1807 auto onMyGroup = delegate ActionOnGroup(string groupName) { 1808 currentGroup = onGroup(groupName); 1809 return ActionOnGroup.proceed; 1810 }; 1811 auto onMyKeyValue = delegate void(string key, string value, string groupName) { 1812 onKeyValue(key, value, currentGroup, groupName); 1813 }; 1814 auto onMyCommentInGroup = delegate void(string line, string groupName) { 1815 onCommentInGroup(line, currentGroup, groupName); 1816 }; 1817 1818 readIniLike(reader, onMyLeadingComment, onMyGroup, onMyKeyValue, onMyCommentInGroup, fileName); 1819 _fileName = fileName; 1820 } 1821 1822 /** 1823 * Get group by name. 1824 * Returns: $(D IniLikeGroup) instance associated with groupName or null if not found. 1825 * See_Also: $(D byGroup) 1826 */ 1827 @nogc @safe final inout(IniLikeGroup) group(string groupName) nothrow inout pure { 1828 auto pick = _listMap.getNode(groupName); 1829 if (pick) { 1830 return pick.value; 1831 } 1832 return null; 1833 } 1834 1835 /** 1836 * Get $(D GroupNode) by groupName. 1837 */ 1838 @nogc @safe final auto getNode(string groupName) nothrow pure { 1839 return GroupNode(_listMap.getNode(groupName)); 1840 } 1841 1842 /** 1843 * Create new group using groupName. 1844 * Returns: Newly created instance of $(D IniLikeGroup). 1845 * Throws: 1846 * $(D inilike.exception.IniLikeGroupException) if group with such name already exists. 1847 * $(D inilike.exception.IniLikeException) if groupName is empty. 1848 * See_Also: $(D removeGroup), $(D group) 1849 */ 1850 @safe final IniLikeGroup addGenericGroup(string groupName) { 1851 if (group(groupName) !is null) { 1852 throw new IniLikeGroupException("group already exists", groupName); 1853 } 1854 auto toReturn = createEmptyGroup(groupName); 1855 insertGroup(toReturn); 1856 return toReturn; 1857 } 1858 1859 /** 1860 * Remove group by name. Do nothing if group with such name does not exist. 1861 * Returns: true if group was deleted, false otherwise. 1862 * See_Also: $(D addGenericGroup), $(D group) 1863 */ 1864 @safe bool removeGroup(string groupName) nothrow { 1865 return _listMap.remove(groupName); 1866 } 1867 1868 /** 1869 * Range of groups in order how they were defined in file. 1870 * See_Also: $(D group) 1871 */ 1872 @nogc @safe final auto byGroup() inout nothrow { 1873 return _listMap.byNode().map!(node => node.value); 1874 } 1875 1876 /** 1877 * Iterate over $(D GroupNode)s. 1878 */ 1879 @nogc @safe final auto byNode() nothrow { 1880 return _listMap.byNode().map!(node => GroupNode(node)); 1881 } 1882 1883 /** 1884 * Save object to the file using .ini-like format. 1885 * Throws: $(D ErrnoException) if the file could not be opened or an error writing to the file occured. 1886 * See_Also: $(D saveToString), $(D save) 1887 */ 1888 @trusted final void saveToFile(string fileName, const WriteOptions options = WriteOptions.exact) const { 1889 import std.stdio : File; 1890 1891 auto f = File(fileName, "w"); 1892 void dg(in string line) { 1893 f.writeln(line); 1894 } 1895 save(&dg, options); 1896 } 1897 1898 /** 1899 * Save object to string using .ini like format. 1900 * Returns: A string that represents the contents of file. 1901 * Note: The resulting string differs from the contents that would be written to file via $(D saveToFile) 1902 * in the way it does not add new line character at the end of the last line. 1903 * See_Also: $(D saveToFile), $(D save) 1904 */ 1905 @trusted final string saveToString(const WriteOptions options = WriteOptions.exact) const { 1906 auto a = appender!(string[])(); 1907 save(a, options); 1908 return a.data.join("\n"); 1909 } 1910 1911 /// 1912 unittest 1913 { 1914 string contents = 1915 ` 1916 # Leading comment 1917 [First group] 1918 # Comment inside 1919 Key=Value 1920 [Second group] 1921 1922 Key=Value 1923 1924 [Third group] 1925 Key=Value`; 1926 1927 auto ilf = new IniLikeFile(iniLikeStringReader(contents)); 1928 assert(ilf.saveToString(WriteOptions.exact) == contents); 1929 1930 assert(ilf.saveToString(WriteOptions.pretty) == 1931 `# Leading comment 1932 [First group] 1933 # Comment inside 1934 Key=Value 1935 1936 [Second group] 1937 Key=Value 1938 1939 [Third group] 1940 Key=Value`); 1941 1942 assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines)) == 1943 `[First group] 1944 Key=Value 1945 [Second group] 1946 Key=Value 1947 [Third group] 1948 Key=Value`); 1949 1950 assert(ilf.saveToString(WriteOptions(No.preserveComments, No.preserveEmptyLines, Yes.lineBetweenGroups)) == 1951 `[First group] 1952 Key=Value 1953 1954 [Second group] 1955 Key=Value 1956 1957 [Third group] 1958 Key=Value`); 1959 } 1960 1961 /** 1962 * Use Output range or delegate to retrieve strings line by line. 1963 * Those strings can be written to the file or be showed in text area. 1964 * Note: Output strings don't have trailing newline character. 1965 * See_Also: $(D saveToFile), $(D saveToString) 1966 */ 1967 final void save(OutRange)(OutRange sink, const WriteOptions options = WriteOptions.exact) const if (isOutputRange!(OutRange, string)) { 1968 foreach(line; leadingComments()) { 1969 if (options.preserveComments) { 1970 if (line.empty && !options.preserveEmptyLines) { 1971 continue; 1972 } 1973 put(sink, line); 1974 } 1975 } 1976 bool firstGroup = true; 1977 bool lastWasEmpty = false; 1978 1979 foreach(group; byGroup()) { 1980 if (!firstGroup && !lastWasEmpty && options.lineBetweenGroups) { 1981 put(sink, ""); 1982 } 1983 1984 put(sink, "[" ~ group.groupName ~ "]"); 1985 foreach(line; group.byIniLine()) { 1986 lastWasEmpty = false; 1987 if (line.type == IniLikeLine.Type.Comment) { 1988 if (!options.preserveComments) { 1989 continue; 1990 } 1991 if (line.comment.empty) { 1992 if (!options.preserveEmptyLines) { 1993 continue; 1994 } 1995 lastWasEmpty = true; 1996 } 1997 put(sink, line.comment); 1998 } else if (line.type == IniLikeLine.Type.KeyValue) { 1999 put(sink, line.key ~ "=" ~ line.value); 2000 } 2001 } 2002 firstGroup = false; 2003 } 2004 } 2005 2006 /** 2007 * File path where the object was loaded from. 2008 * Returns: File name as was specified on the object creation. 2009 */ 2010 @nogc @safe final string fileName() nothrow const pure { 2011 return _fileName; 2012 } 2013 2014 /** 2015 * Leading comments. 2016 * Returns: Range of leading comments (before any group) 2017 * See_Also: $(D appendLeadingComment), $(D prependLeadingComment), $(D clearLeadingComments) 2018 */ 2019 @nogc @safe final auto leadingComments() const nothrow pure { 2020 return _leadingComments; 2021 } 2022 2023 /// 2024 unittest 2025 { 2026 auto ilf = new IniLikeFile(); 2027 assert(ilf.appendLeadingComment("First") == "#First"); 2028 assert(ilf.appendLeadingComment("#Second") == "#Second"); 2029 assert(ilf.appendLeadingComment("Sneaky\nKey=Value") == "#Sneaky Key=Value"); 2030 assert(ilf.appendLeadingComment("# New Line\n") == "# New Line"); 2031 assert(ilf.appendLeadingComment("") == ""); 2032 assert(ilf.appendLeadingComment("\n") == ""); 2033 assert(ilf.prependLeadingComment("Shebang") == "#Shebang"); 2034 assert(ilf.leadingComments().equal(["#Shebang", "#First", "#Second", "#Sneaky Key=Value", "# New Line", "", ""])); 2035 ilf.clearLeadingComments(); 2036 assert(ilf.leadingComments().empty); 2037 } 2038 2039 /** 2040 * Add leading comment. This will be appended to the list of leadingComments. 2041 * Note: # will be prepended automatically if line is not empty and does not have # at the start. 2042 * The last new line character will be removed if present. Others will be replaced with whitespaces. 2043 * Returns: Line that was added as comment. 2044 * See_Also: $(D leadingComments), $(D prependLeadingComment) 2045 */ 2046 @safe final string appendLeadingComment(string line) nothrow pure { 2047 line = makeComment(line); 2048 _leadingComments ~= line; 2049 return line; 2050 } 2051 2052 /** 2053 * Prepend leading comment (e.g. for setting shebang line). 2054 * Returns: Line that was added as comment. 2055 * See_Also: $(D leadingComments), $(D appendLeadingComment) 2056 */ 2057 @safe final string prependLeadingComment(string line) nothrow pure { 2058 line = makeComment(line); 2059 _leadingComments = line ~ _leadingComments; 2060 return line; 2061 } 2062 2063 /** 2064 * Remove all coments met before groups. 2065 * See_Also: $(D leadingComments) 2066 */ 2067 @nogc final @safe void clearLeadingComments() nothrow { 2068 _leadingComments = null; 2069 } 2070 2071 /** 2072 * Move the group to make it the first. 2073 */ 2074 @trusted final void moveGroupToFront(GroupNode toMove) nothrow pure { 2075 _listMap.moveToFront(toMove.node); 2076 } 2077 2078 /** 2079 * Move the group to make it the last. 2080 */ 2081 @trusted final void moveGroupToBack(GroupNode toMove) nothrow pure { 2082 _listMap.moveToBack(toMove.node); 2083 } 2084 2085 /** 2086 * Move group before other. 2087 */ 2088 @trusted final void moveGroupBefore(GroupNode other, GroupNode toMove) nothrow pure { 2089 _listMap.moveBefore(other.node, toMove.node); 2090 } 2091 2092 /** 2093 * Move group after other. 2094 */ 2095 @trusted final void moveGroupAfter(GroupNode other, GroupNode toMove) nothrow pure { 2096 _listMap.moveAfter(other.node, toMove.node); 2097 } 2098 2099 @safe final ReadOptions readOptions() nothrow const pure { 2100 return _readOptions; 2101 } 2102 2103 /** 2104 * Shortcut to $(D IniLikeGroup.escapedValue) of given group. 2105 * Returns null if the group does not exist. 2106 */ 2107 @nogc @safe final string escapedValue(string groupName, string key) 2108 { 2109 auto g = group(groupName); 2110 if (g) { 2111 return g.escapedValue(key); 2112 } else { 2113 return null; 2114 } 2115 } 2116 2117 /// ditto, localized version 2118 @safe final string escapedValue(string groupName, string key, string locale, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) 2119 { 2120 auto g = group(groupName); 2121 if (g) { 2122 return g.escapedValue(key, locale, nonLocaleFallback); 2123 } else { 2124 return null; 2125 } 2126 } 2127 2128 /** 2129 * Shortcut to $(D IniLikeGroup.unescapedValue) of given group. 2130 * Returns null if the group does not exist. 2131 */ 2132 @safe final string unescapedValue(string groupName, string key, string locale = null, Flag!"nonLocaleFallback" nonLocaleFallback = Yes.nonLocaleFallback) 2133 { 2134 auto g = group(groupName); 2135 if (g) { 2136 return g.unescapedValue(key, locale, nonLocaleFallback); 2137 } else { 2138 return null; 2139 } 2140 } 2141 2142 private: 2143 string _fileName; 2144 GroupListMap _listMap; 2145 string[] _leadingComments; 2146 ReadOptions _readOptions; 2147 } 2148 2149 /// 2150 unittest 2151 { 2152 import std.file; 2153 import std.path; 2154 import std.stdio; 2155 2156 string contents = 2157 `# The first comment 2158 [First Entry] 2159 # Comment 2160 GenericName=File manager 2161 GenericName[ru]=Файловый менеджер 2162 NeedUnescape=yes\\i\tneed 2163 NeedUnescape[ru]=да\\я\tнуждаюсь 2164 # Another comment 2165 [Another Group] 2166 Name=Commander 2167 Comment=Manage files 2168 # The last comment`; 2169 2170 auto ilf = new IniLikeFile(iniLikeStringReader(contents), "contents.ini"); 2171 assert(ilf.fileName() == "contents.ini"); 2172 assert(equal(ilf.leadingComments(), ["# The first comment"])); 2173 assert(ilf.group("First Entry")); 2174 assert(ilf.group("Another Group")); 2175 assert(ilf.getNode("Another Group").group is ilf.group("Another Group")); 2176 assert(ilf.group("NonExistent") is null); 2177 assert(ilf.getNode("NonExistent").isNull()); 2178 assert(ilf.getNode("NonExistent").key() is null); 2179 assert(ilf.getNode("NonExistent").group() is null); 2180 assert(ilf.saveToString(IniLikeFile.WriteOptions.exact) == contents); 2181 2182 version(inilikeFileTest) 2183 { 2184 string tempFile = buildPath(tempDir(), "inilike-unittest-tempfile"); 2185 try { 2186 assertNotThrown!IniLikeReadException(ilf.saveToFile(tempFile)); 2187 auto fileContents = cast(string)std.file.read(tempFile); 2188 assert(equal(fileContents.lineSplitter, contents.lineSplitter), "Contents should be preserved as is"); 2189 2190 IniLikeFile filf; 2191 assertNotThrown!IniLikeReadException(filf = new IniLikeFile(tempFile)); 2192 assert(filf.fileName() == tempFile); 2193 remove(tempFile); 2194 } catch(Exception e) { 2195 //environmental error in unittests 2196 } 2197 } 2198 2199 assert(ilf.escapedValue("NonExistent", "Key") is null); 2200 assert(ilf.escapedValue("NonExistent", "Key", "ru") is null); 2201 assert(ilf.unescapedValue("NonExistent", "Key") is null); 2202 2203 auto firstEntry = ilf.group("First Entry"); 2204 2205 assert(!firstEntry.contains("NonExistent")); 2206 assert(firstEntry.contains("GenericName")); 2207 assert(firstEntry.contains("GenericName[ru]")); 2208 assert(firstEntry.byNode().filter!(node => node.isNull()).empty); 2209 assert(firstEntry.escapedValue("GenericName") == "File manager"); 2210 assert(firstEntry.getNode("GenericName").key == "GenericName"); 2211 assert(firstEntry.getNode("NonExistent").key is null); 2212 assert(firstEntry.getNode("NonExistent").line.type == IniLikeLine.Type.None); 2213 2214 assert(firstEntry.escapedValue("NeedUnescape") == `yes\\i\tneed`); 2215 assert(ilf.escapedValue("First Entry", "NeedUnescape") == `yes\\i\tneed`); 2216 2217 assert(firstEntry.unescapedValue("NeedUnescape") == "yes\\i\tneed"); 2218 assert(ilf.unescapedValue("First Entry", "NeedUnescape") == "yes\\i\tneed"); 2219 2220 assert(firstEntry.escapedValue("NeedUnescape", "ru") == `да\\я\tнуждаюсь`); 2221 assert(ilf.escapedValue("First Entry", "NeedUnescape", "ru") == `да\\я\tнуждаюсь`); 2222 2223 assert(firstEntry.unescapedValue("NeedUnescape", "ru") == "да\\я\tнуждаюсь"); 2224 assert(ilf.unescapedValue("First Entry", "NeedUnescape", "ru") == "да\\я\tнуждаюсь"); 2225 2226 firstEntry.setUnescapedValue("NeedEscape", "i\rneed\nescape"); 2227 assert(firstEntry.escapedValue("NeedEscape") == `i\rneed\nescape`); 2228 firstEntry.setUnescapedValue("NeedEscape", "ru", "мне\rнужно\nэкранирование"); 2229 assert(firstEntry.escapedValue("NeedEscape", "ru") == `мне\rнужно\nэкранирование`); 2230 2231 firstEntry.setEscapedValue("GenericName", "Manager of files"); 2232 assert(firstEntry.escapedValue("GenericName") == "Manager of files"); 2233 firstEntry.setEscapedValue("Authors", "Unknown"); 2234 assert(firstEntry.escapedValue("Authors") == "Unknown"); 2235 firstEntry.getNode("Authors").setEscapedValue("Known"); 2236 assert(firstEntry.escapedValue("Authors") == "Known"); 2237 2238 assert(firstEntry.escapedValue("GenericName", "ru") == "Файловый менеджер"); 2239 firstEntry.setEscapedValue("GenericName", "ru", "Менеджер файлов"); 2240 assert(firstEntry.escapedValue("GenericName", "ru") == "Менеджер файлов"); 2241 firstEntry.setEscapedValue("Authors", "ru", "Неизвестны"); 2242 assert(firstEntry.escapedValue("Authors", "ru") == "Неизвестны"); 2243 2244 firstEntry.removeEntry("GenericName"); 2245 assert(!firstEntry.contains("GenericName")); 2246 firstEntry.removeEntry("GenericName", "ru"); 2247 assert(!firstEntry.contains("GenericName[ru]")); 2248 firstEntry.setEscapedValue("GenericName", "File Manager"); 2249 assert(firstEntry.escapedValue("GenericName") == "File Manager"); 2250 2251 assert(ilf.group("Another Group").escapedValue("Name") == "Commander"); 2252 assert(equal(ilf.group("Another Group").byKeyValue(), [ keyValueTuple("Name", "Commander"), keyValueTuple("Comment", "Manage files") ])); 2253 2254 auto latestCommentNode = ilf.group("Another Group").appendComment("The lastest comment"); 2255 assert(latestCommentNode.line.comment == "#The lastest comment"); 2256 latestCommentNode.setEscapedValue("The latest comment"); 2257 assert(latestCommentNode.line.comment == "#The latest comment"); 2258 assert(ilf.group("Another Group").prependComment("The first comment").line.comment == "#The first comment"); 2259 2260 assert(equal( 2261 ilf.group("Another Group").byIniLine(), 2262 [IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"), IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment")] 2263 )); 2264 2265 auto nameLineNode = ilf.group("Another Group").getNode("Name"); 2266 assert(nameLineNode.line.value == "Commander"); 2267 auto commentLineNode = ilf.group("Another Group").getNode("Comment"); 2268 assert(commentLineNode.line.value == "Manage files"); 2269 2270 ilf.group("Another Group").addCommentAfter(nameLineNode, "Middle comment"); 2271 ilf.group("Another Group").addCommentBefore(commentLineNode, "Average comment"); 2272 2273 assert(equal( 2274 ilf.group("Another Group").byIniLine(), 2275 [ 2276 IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"), 2277 IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"), 2278 IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment"), IniLikeLine.fromComment("#The latest comment") 2279 ] 2280 )); 2281 2282 ilf.group("Another Group").removeEntry(latestCommentNode); 2283 2284 assert(equal( 2285 ilf.group("Another Group").byIniLine(), 2286 [ 2287 IniLikeLine.fromComment("#The first comment"), IniLikeLine.fromKeyValue("Name", "Commander"), 2288 IniLikeLine.fromComment("#Middle comment"), IniLikeLine.fromComment("#Average comment"), 2289 IniLikeLine.fromKeyValue("Comment", "Manage files"), IniLikeLine.fromComment("# The last comment") 2290 ] 2291 )); 2292 2293 assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group"])); 2294 2295 assert(!ilf.removeGroup("NonExistent Group")); 2296 2297 assert(ilf.removeGroup("Another Group")); 2298 assert(!ilf.group("Another Group")); 2299 assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry"])); 2300 2301 ilf.addGenericGroup("Another Group"); 2302 assert(ilf.group("Another Group")); 2303 assert(ilf.group("Another Group").byIniLine().empty); 2304 assert(ilf.group("Another Group").byKeyValue().empty); 2305 2306 assertThrown(ilf.addGenericGroup("Another Group")); 2307 2308 ilf.addGenericGroup("Other Group"); 2309 assert(equal(ilf.byGroup().map!(g => g.groupName), ["First Entry", "Another Group", "Other Group"])); 2310 2311 assertThrown!IniLikeException(ilf.addGenericGroup("")); 2312 2313 import std.range : isForwardRange; 2314 2315 const IniLikeFile cilf = ilf; 2316 static assert(isForwardRange!(typeof(cilf.byGroup()))); 2317 static assert(isForwardRange!(typeof(cilf.group("First Entry").byKeyValue()))); 2318 static assert(isForwardRange!(typeof(cilf.group("First Entry").byIniLine()))); 2319 2320 contents = 2321 `[Group] 2322 GenericName=File manager 2323 [Group] 2324 GenericName=Commander`; 2325 2326 auto shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents), "config.ini")); 2327 assert(shouldThrow !is null, "Duplicate groups should throw"); 2328 assert(shouldThrow.lineNumber == 3); 2329 assert(shouldThrow.lineIndex == 2); 2330 assert(shouldThrow.fileName == "config.ini"); 2331 2332 contents = 2333 `[Group] 2334 Key=Value1 2335 Key=Value2`; 2336 2337 shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents))); 2338 assert(shouldThrow !is null, "Duplicate key should throw"); 2339 assert(shouldThrow.lineNumber == 3); 2340 2341 contents = 2342 `[Group] 2343 Key=Value 2344 =File manager`; 2345 2346 shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents))); 2347 assert(shouldThrow !is null, "Empty key should throw"); 2348 assert(shouldThrow.lineNumber == 3); 2349 2350 contents = 2351 `[Group] 2352 #Comment 2353 Valid=Key 2354 NotKeyNotGroupNotComment`; 2355 2356 shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents))); 2357 assert(shouldThrow !is null, "Invalid entry should throw"); 2358 assert(shouldThrow.lineNumber == 4); 2359 2360 contents = 2361 `#Comment 2362 NotComment 2363 [Group] 2364 Valid=Key`; 2365 shouldThrow = collectException!IniLikeReadException(new IniLikeFile(iniLikeStringReader(contents))); 2366 assert(shouldThrow !is null, "Invalid comment should throw"); 2367 assert(shouldThrow.lineNumber == 2); 2368 2369 2370 contents = `# The leading comment 2371 [One] 2372 # Comment1 2373 Key1=Value1 2374 Key2=Value2 2375 Key3=Value3 2376 [Two] 2377 Key1=Value1 2378 Key2=Value2 2379 Key3=Value3 2380 # Comment2 2381 [Three] 2382 Key1=Value1 2383 Key2=Value2 2384 # Comment3 2385 Key3=Value3`; 2386 2387 ilf = new IniLikeFile(iniLikeStringReader(contents)); 2388 2389 ilf.moveGroupToFront(ilf.getNode("Two")); 2390 assert(ilf.byNode().map!(g => g.key).equal(["Two", "One", "Three"])); 2391 2392 ilf.moveGroupToBack(ilf.getNode("One")); 2393 assert(ilf.byNode().map!(g => g.key).equal(["Two", "Three", "One"])); 2394 2395 ilf.moveGroupBefore(ilf.getNode("Two"), ilf.getNode("Three")); 2396 assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "Two", "One"])); 2397 2398 ilf.moveGroupAfter(ilf.getNode("Three"), ilf.getNode("One")); 2399 assert(ilf.byGroup().map!(g => g.groupName).equal(["Three", "One", "Two"])); 2400 2401 auto groupOne = ilf.group("One"); 2402 groupOne.moveLineToFront(groupOne.getNode("Key3")); 2403 groupOne.moveLineToBack(groupOne.getNode("Key1")); 2404 2405 assert(groupOne.byIniLine().equal([ 2406 IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromComment("# Comment1"), 2407 IniLikeLine.fromKeyValue("Key2", "Value2"), IniLikeLine.fromKeyValue("Key1", "Value1") 2408 ])); 2409 2410 auto groupTwo = ilf.group("Two"); 2411 groupTwo.moveLineBefore(groupTwo.getNode("Key1"), groupTwo.getNode("Key3")); 2412 groupTwo.moveLineAfter(groupTwo.getNode("Key2"), groupTwo.getNode("Key1")); 2413 2414 assert(groupTwo.byIniLine().equal([ 2415 IniLikeLine.fromKeyValue("Key3", "Value3"), IniLikeLine.fromKeyValue("Key2", "Value2"), 2416 IniLikeLine.fromKeyValue("Key1", "Value1"), IniLikeLine.fromComment("# Comment2") 2417 ])); 2418 }