1 /* 2 Copyright (C) 2004-2006 Christopher E. Miller 3 http://www.dprogramming.com/ini.php 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20 */ 21 22 /* 23 24 Modified by Jesse Phillips 25 Made to work with D 2.0. 26 Changed all string to string. 27 Added some @safe and nothrow 28 Other changes marked 29 30 Update: 31 The Ini object no longer saves in the destructor because if it is the 32 garbage collector deleting it, some value or section object could have 33 been destructed first, resulting in undefined behavior, such as an 34 access violation. Solution: save() before you exit the program. 35 36 37 Portable module for reading and writing INI files of the format: 38 39 [section] 40 key=value 41 ... 42 43 Leading spaces and tabs are ignored. 44 Comments start with ; and should be on their own line. 45 46 If there are comments, spaces or keys above the first section, a nameless section is created for them. 47 This means there need not be any sections in the file to have keys. 48 49 Differences with Windows' profile (INI) functions: 50 * Windows 9x does not allow tabs in the value. 51 * Some versions do not allow the file to exceed 64 KB. 52 * If not a full file path, it's relative to the Windows directory. 53 * Windows 9x strips trailing spaces from the value. 54 * There might be a restriction on how long sections/keys/values may be. 55 * If there are double quotes around a value, Windows removes them. 56 * All key/value pairs must be in a named section. 57 58 Проведена модификация MGW 18.02.2016 59 Отключена работа с устаревшим модулем std.stream 60 61 */ 62 63 64 /// Portable module for reading and writing _INI files. _ini.d version 0.6 65 module ini; 66 67 import std.stdio; // writeln 68 69 private import std.file, std..string; 70 71 72 // debug = INI; //show file being parsed 73 74 75 // debug(INI) 76 // private import std.cstream; 77 78 private class IniLine { 79 ~this() { 80 debug(PRINT_DTORS) writeln("~IniLine\n"); 81 } 82 83 private: 84 string data; 85 } 86 87 /// Key in an INI file. 88 class IniKey: IniLine { 89 protected: 90 //these are slices in data if unmodified 91 //if modified, data is set to null 92 string _name; 93 string _value; 94 95 public: 96 this(string name) { 97 _name = name; 98 } 99 100 101 ~this() { 102 debug(PRINT_DTORS) writeln("~IniKey ", _name); 103 } 104 105 106 // public: +++ GENA 107 /// Property: get key _name. 108 @property string name() { //-> имя 109 return _name; 110 } 111 112 113 /// Property: get key _value. 114 @property string value() { //-> значение 115 return _value; 116 } 117 } 118 119 120 /// Section of keys in an INI file. 121 class IniSection { 122 protected: 123 Ini _ini; // Ссылка на объект Ini 124 string _name; // Имя секции 125 IniLine[] lines; // Массив строк в секции 126 127 // Создание. ссылка на объект Ini и имя секции 128 this(Ini ini, string name) { 129 _ini = ini; // Ссылка на объект Ini 130 _name = name; // Имя секции 131 } 132 133 ~this() { 134 debug(PRINT_DTORS) writeln("~IniSection ", _name); 135 } 136 137 138 public: 139 /// Property: get section _name. Вернуть имя секции 140 @property @safe nothrow 141 string name() { //-> Вернуть/установить имя секции 142 return _name; 143 } 144 145 146 /// Property: set section _name. Установить имя секции. Взвести флаг модификации 147 @property @safe nothrow 148 void name(string newName) { //-> Вернуть/установить имя секции 149 _ini._modified = true; 150 _name = newName; 151 } 152 153 154 /// foreach key. Перебор ключей в цикле 155 int opApply(int delegate(ref IniKey) dg) { //-> Найти нужный ключ 156 int result = 0; 157 uint i; 158 IniKey ikey; 159 for(i = 0; i != lines.length; i++) { 160 ikey = cast(IniKey)lines[i]; 161 if(ikey) { 162 result = dg(ikey); 163 if(result) 164 break; 165 } 166 } 167 return result; 168 } 169 170 171 /// Property: get all _keys. Дай перечень всех ключей 172 //better to use foreach unless this array is needed 173 @property IniKey[] keys() { //-> Дай перечень всех ключей 174 IniKey[] ikeys = new IniKey[lines.length]; 175 uint i = 0; 176 foreach(IniKey ikey; this) { 177 ikeys[i++] = ikey; 178 } 179 return ikeys[0 .. i]; 180 } 181 182 183 /// Returns: _key matching keyName, or null if not present. Вернуть ключ или null если отсутствует 184 IniKey key(string keyName) { //-> Вернуть ключ или null если отсутствует 185 foreach(IniKey ikey; this) { 186 if(_ini.match(ikey._name, keyName)) 187 return ikey; 188 } 189 return new IniKey(keyName); //didn't find it 190 } 191 192 193 /// Set an existing key's value. 194 @safe nothrow 195 void setValue(IniKey ikey, string newValue) { //-> Установить значение если существует 196 ikey._value = newValue; 197 _ini._modified = true; 198 ikey.data = null; 199 } 200 201 202 /// Find or create key keyName and set its _value to newValue. 203 void setValue(string keyName, string newValue) { //-> Найти или создать и установить значение 204 IniKey ikey = key(keyName); 205 if(!ikey.data) { // MGW Add ref on .data in ikey 206 ikey = new IniKey(keyName); 207 lines ~= ikey; // К списку lines добавить ещё одну строку 208 // _ini._modified = true; //next call does this 209 } 210 value(ikey, newValue); 211 } 212 213 214 /+ 215 /// 216 alias setValue value; 217 +/ 218 219 220 /// Same as setValue(ikey, newValue). 221 @safe nothrow 222 void value(IniKey ikey, string newValue) { 223 return setValue(ikey, newValue); 224 } 225 226 227 /// Same as setValue(keyName, newValue). 228 void value(string keyName, string newValue) { 229 // asm { 230 // int 3; 231 // } 232 return setValue(keyName, newValue); 233 } 234 235 236 /// Returns: value of the existing key keyName, or defaultValue if not present. 237 string getValue(string keyName, string defaultValue = null) { 238 foreach(IniKey ikey; this) { 239 if(_ini.match(ikey._name, keyName)) 240 return ikey.value; 241 } 242 return defaultValue; //didn't find it 243 } 244 245 246 // /// Returns: _value of the existing key keyName, or null if not present. 247 /// Same as getValue(keyName, null). 248 string value(string keyName) { //-> 249 return getValue(keyName, null); 250 } 251 252 253 /// Shortcut for getValue(keyName). 254 string opIndex(string keyName) { //-> Доступ по имени 255 return value(keyName); 256 } 257 258 259 /// Shortcut for setValue(keyName, newValue). 260 void opIndexAssign(string newValue, string keyName) { //-> 261 value(keyName, newValue); 262 } 263 264 265 /// _Remove key keyName. 266 void remove(string keyName) { //-> Удалить ключ 267 uint i; 268 IniKey ikey; 269 for(i = 0; i != lines.length; i++) { 270 ikey = cast(IniKey)lines[i]; 271 if(ikey && _ini.match(ikey._name, keyName)) { 272 if(i == lines.length - 1) 273 lines = lines[0 .. i]; 274 else if(i == 0) 275 lines = lines[1 .. lines.length]; 276 else 277 lines = lines[0 .. i] ~ lines[i + 1 .. lines.length]; 278 _ini._modified = true; 279 return; 280 } 281 } 282 } 283 } 284 285 286 /// An INI file. 287 class Ini { 288 // protected: 289 string _file; // Имя исходного INI файла 290 bool _modified = false; // F - файл не изменялся 291 IniSection[] isecs; 292 char secStart = '[', secEnd = ']'; 293 294 void print() { 295 writeln("================ Class INI ==============="); 296 writeln("_file = ", _file); 297 writeln("_modified = ", _modified); 298 for(int i; i<isecs.length; i++) { 299 writeln(" ", isecs[i]._name); 300 for(int j; j<isecs[i].lines.length; j++) { 301 IniKey ikey = cast(IniKey)isecs[i].lines[j]; 302 writeln(" ", isecs[i].lines[j].data); 303 try { 304 writeln(" [", ikey._name, "] --> [", ikey._value, "] (", ikey.data,")"); 305 } catch { 306 } 307 } 308 } 309 writeln("=========================================="); 310 } 311 312 void parse() { 313 debug(INI) writeln("INI parsing file ", _file); 314 string data; 315 int i = -1; 316 IniSection isec; 317 uint lineStartIndex = 0; 318 319 try { 320 data = cast(string)std.file.read(_file); 321 /+ 322 File f = new File(_file, FileMode.In); 323 data = f.readString(f.size()); 324 delete f; 325 +/ 326 } 327 // catch(Throwable o) 328 catch { 329 debug(INI) 330 writeln("INI no file to parse"); 331 // File f = new File(_file, "w"); 332 // write(f," "); 333 // f.close(); 334 335 return; 336 } 337 if(!data.length) { 338 debug(INI) 339 writeln("INI nothing to parse"); 340 return; 341 } 342 343 344 char getc() { 345 //also increment -i- past end so ungetc works properly 346 if(++i >= data.length) 347 return 0; 348 return data[i]; 349 } 350 351 352 void ungetc() { 353 assert(i > 0); 354 i--; 355 } 356 357 358 void reset() { 359 lineStartIndex = i + 1; 360 } 361 362 363 void eol() { 364 IniLine iline = new IniLine; 365 iline.data = data[lineStartIndex .. i]; 366 debug(INI) writeln("INI line: ", iline.data); 367 isec.lines ~= iline; 368 } 369 370 371 char ch, ch2; 372 int i2; 373 isec = new IniSection(this, ""); 374 for(;;) { 375 ch = getc(); 376 switch(ch) { 377 case '\r': 378 eol(); 379 ch2 = getc(); 380 if(ch2 != '\n') 381 ungetc(); 382 reset(); 383 break; 384 385 case '\n': 386 eol(); 387 reset(); 388 break; 389 390 case 0: //eof 391 ini_eof: 392 if(lineStartIndex < i) { 393 eol(); 394 //reset(); 395 } 396 isecs ~= isec; 397 if(!isecs[0].lines) 398 isecs = isecs[1 .. isecs.length]; 399 debug(INI) 400 writeln("INI done parsing"); 401 return; 402 403 case ' ': 404 case '\t': 405 case '\v': 406 case '\f': 407 break; 408 409 case ';': //comments 410 case '#': 411 done_comment: 412 for(;;) { 413 ch2 = getc(); 414 switch(ch2) { 415 case '\r': 416 eol(); 417 ch2 = getc(); 418 if(ch2 != '\n') 419 ungetc(); 420 reset(); 421 break done_comment; 422 423 case '\n': 424 eol(); 425 reset(); 426 break done_comment; 427 428 case 0: //eof 429 goto ini_eof; 430 431 default: 432 break; 433 } 434 } 435 break; 436 437 default: 438 if(ch == secStart) { // '[' 439 i2 = i + 1; 440 done_sec: 441 for(;;) { 442 ch2 = getc(); 443 switch(ch2) { 444 case '\r': 445 eol(); 446 ch2 = getc(); 447 if(ch2 != '\n') 448 ungetc(); 449 reset(); 450 break done_sec; 451 452 case '\n': 453 eol(); 454 reset(); 455 break done_sec; 456 457 case 0: //eof 458 goto ini_eof; 459 460 default: 461 if(ch2 == secEnd) { // ']' 462 isecs ~= isec; 463 isec = new IniSection(this, data[i2 .. i]); 464 debug(INI) writeln("INI section: ", isec._name); 465 for(;;) { 466 ch2 = getc(); 467 switch(ch2) { 468 case ' ': 469 case '\t': 470 case '\v': 471 case '\f': 472 //ignore whitespace 473 break; 474 475 case '\r': 476 ch2 = getc(); 477 if(ch2 != '\n') 478 ungetc(); 479 break done_sec; 480 481 case '\n': 482 break done_sec; 483 484 default: 485 //just treat junk after the ] as the next line 486 ungetc(); 487 break done_sec; 488 } 489 } 490 break done_sec; 491 } 492 } 493 } 494 reset(); 495 break; 496 } else { //must be beginning of key name 497 i2 = i; 498 done_default: 499 for(;;) { 500 ch2 = getc(); 501 switch(ch2) { 502 case '\r': 503 eol(); 504 ch2 = getc(); 505 if(ch2 != '\n') 506 ungetc(); 507 reset(); 508 break done_default; 509 510 case '\n': 511 eol(); 512 reset(); 513 break done_default; 514 515 case 0: //eof 516 goto ini_eof; 517 518 case ' ': 519 case '\t': 520 case '\v': 521 case '\f': 522 break; 523 524 case '=': 525 IniKey ikey; 526 527 528 void addKey() { 529 ikey.data = data[lineStartIndex .. i]; 530 ikey._value = data[i2 .. i]; 531 isec.lines ~= ikey; 532 debug(INI) writeln("INI key: [", ikey._name, "] = [", ikey._value, "]"); 533 } 534 535 536 ikey = new IniKey(data[i2 .. i]); 537 i2 = i + 1; //after = 538 for(;;) { //get key value 539 ch2 = getc(); 540 switch(ch2) { 541 case '\r': 542 addKey(); 543 ch2 = getc(); 544 if(ch2 != '\n') 545 ungetc(); 546 reset(); 547 break done_default; 548 549 case '\n': 550 addKey(); 551 reset(); 552 break done_default; 553 554 case 0: //eof 555 addKey(); 556 reset(); 557 goto ini_eof; 558 559 default: 560 break; 561 } 562 } 563 break done_default; 564 565 default: 566 break; 567 } 568 } 569 } 570 } 571 } 572 } 573 574 575 void firstOpen(string file) { 576 //null terminated just to make it easier for the implementation 577 //_file = toStringz(file)[0 .. file.length]; 578 // JP Modified 579 _file = file; 580 parse(); 581 } 582 583 584 public: 585 // Added by Jesse Phillips 586 /// Upon the next save use this file. 587 string saveTo; 588 // Use different section name delimiters; not recommended. 589 this(string file, char secStart, char secEnd) { 590 this.secStart = secStart; 591 this.secEnd = secEnd; 592 593 firstOpen(file); 594 } 595 596 597 /// Construct a new INI _file. 598 this(string file) { 599 firstOpen(file); 600 } 601 602 603 ~this() { 604 debug(PRINT_DTORS) 605 writeln("~Ini ", _file); 606 607 // The reason this is commented is explained above. 608 /+ 609 if(_modified) 610 save(); 611 +/ 612 } 613 614 615 /// Comparison function for section and key names. Override to change behavior. 616 bool match(string s1, string s2) { 617 return !std..string.icmp(s1, s2); 618 } 619 620 621 //reuse same object for another file 622 /// Open an INI _file. 623 void open(string file) { 624 if(_modified) 625 save(); 626 _modified = false; 627 isecs = null; 628 629 firstOpen(file); 630 } 631 632 633 /// Reload INI file; any unsaved changes are lost. 634 void rehash() { 635 _modified = false; 636 isecs = null; 637 parse(); 638 } 639 640 641 /// Release memory without saving changes; contents become empty. 642 @safe nothrow 643 void dump() { 644 _modified = false; 645 isecs = null; 646 } 647 648 649 /// Property: get whether or not the INI file was _modified since it was loaded or saved. 650 @property @safe nothrow 651 bool modified() { 652 return _modified; 653 } 654 655 656 /// Params: 657 /// f = an opened-for-write stream; save() uses BufferedFile by default. Override save() to change stream. 658 protected final void saveToStream(File f) { 659 _modified = false; 660 661 // Если массив секций пустой, то выйти 662 if(!isecs.length) return; 663 664 IniKey ikey; 665 IniSection isec; 666 uint i = 0, j; 667 668 if(isecs[0]._name.length) 669 goto write_name; 670 else //first section doesn't have a name; just keys at start of file 671 goto after_name; 672 673 for(; i != isecs.length; i++) { 674 write_name: 675 // JP Modified added dup 676 f.writeln(secStart, isecs[i]._name, secEnd); 677 after_name: 678 isec = isecs[i]; 679 for(j = 0; j != isec.lines.length; j++) { 680 if(isec.lines[j].data is null) { 681 ikey = cast(IniKey)isec.lines[j]; 682 if(ikey) 683 ikey.data = ikey._name ~ "=" ~ ikey._value; 684 } 685 f.writeln(isec.lines[j].data); 686 } 687 } 688 } 689 690 /// Write contents to disk, even if no changes were made. It is common to do if(modified)save(); 691 void save() { 692 if(saveTo) { 693 _file = saveTo; 694 saveTo = null; 695 } 696 File f = File(_file, "w"); 697 // f.create(_file); 698 try { 699 saveToStream(f); 700 f.flush(); 701 } 702 finally { 703 f.close(); 704 } 705 } 706 707 /// Write contents to disk with filename 708 // Added by Jesse Phillips 709 void save(string filename) { 710 _file = filename; 711 save(); 712 } 713 714 715 /// Finds a _section; returns null if one named name does not exist. 716 IniSection section(string name) { 717 foreach(IniSection isec; isecs) { 718 if(match(isec._name, name)) 719 return isec; 720 } 721 return null; //didn't find it 722 } 723 724 725 /// Shortcut for section(sectionName). 726 IniSection opIndex(string sectionName) { 727 return section(sectionName); 728 } 729 730 731 /// The section is created if one named name does not exist. 732 /// Returns: Section named name. 733 IniSection addSection(string name) { 734 IniSection isec = section(name); 735 if(!isec) { 736 isec = new IniSection(this, name); 737 _modified = true; 738 isecs ~= isec; 739 } 740 return isec; 741 } 742 743 744 /// foreach section. 745 int opApply(int delegate(ref IniSection) dg) { 746 int result = 0; 747 foreach(IniSection isec; isecs) { 748 result = dg(isec); 749 if(result) 750 break; 751 } 752 return result; 753 } 754 755 756 /// Property: get all _sections. 757 @property @safe nothrow 758 IniSection[] sections() { 759 return isecs; 760 } 761 762 763 /// _Remove section named sectionName. 764 void remove(string sectionName) { 765 uint i; 766 for(i = 0; i != isecs.length; i++) { 767 if(match(sectionName, isecs[i]._name)) { 768 if(i == isecs.length - 1) 769 isecs = isecs[0 .. i]; 770 else if(i == 0) 771 isecs = isecs[1 .. isecs.length]; 772 else 773 isecs = isecs[0 .. i] ~ isecs[i + 1 .. isecs.length]; 774 _modified = true; 775 return; 776 } 777 } 778 } 779 } 780 781 782 unittest { 783 string inifile = "unittest.ini"; 784 // Jesse Phillips 785 // Remove file when done. 786 scope(exit) 787 std.file.remove(inifile); 788 Ini ini; 789 790 ini = new Ini(inifile); 791 with(ini.addSection("foo")) { 792 value("asdf", "jkl"); 793 value("bar", "wee!"); 794 value("hi", "hello"); 795 } 796 ini.addSection("BAR"); 797 with(ini.addSection("fOO")) { 798 value("yes", "no"); 799 } 800 with(ini.addSection("Hello")) { 801 value("world", "true"); 802 } 803 with(ini.addSection("test")) { 804 value("1", "2"); 805 value("3", "4"); 806 } 807 ini["test"]["value"] = "true"; 808 assert(ini["Foo"]["yes"] == "no"); 809 ini.save(); 810 delete ini; 811 812 ini = new Ini(inifile); 813 assert(ini["FOO"]["Bar"] == "wee!"); // 814 assert(ini["Foo"]["yes"] == "no"); 815 assert(ini["hello"]["world"] == "true"); 816 assert(ini["FOO"]["Bar"] == "wee!"); 817 assert(ini["55"] is null); 818 assert(ini["hello"]["Yes"] is null); 819 820 ini.open(inifile); 821 ini["bar"].remove("notta"); 822 ini["foo"].remove("bar"); 823 ini.remove("bar"); 824 assert(ini["bar"] is null); 825 assert(ini["foo"] !is null); 826 assert(ini["foo"]["bar"] is null); 827 ini.remove("foo"); 828 assert(ini["foo"] is null); 829 ini.save(); 830 delete ini; 831 }