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