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