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(Throwable) { 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 if(data.length>2 && data[0]==239 && data[1]==187 && data[2]==191) data = data[3 .. $].dup; 322 323 /+ 324 File f = new File(_file, FileMode.In); 325 data = f.readString(f.size()); 326 delete f; 327 +/ 328 } 329 // catch(Throwable o) 330 catch(Throwable) { 331 debug(INI) 332 writeln("INI no file to parse"); 333 // File f = new File(_file, "w"); 334 // write(f," "); 335 // f.close(); 336 337 return; 338 } 339 if(!data.length) { 340 debug(INI) 341 writeln("INI nothing to parse"); 342 return; 343 } 344 345 346 char getc() { 347 //also increment -i- past end so ungetc works properly 348 if(++i >= data.length) 349 return 0; 350 return data[i]; 351 } 352 353 354 void ungetc() { 355 assert(i > 0); 356 i--; 357 } 358 359 360 void reset() { 361 lineStartIndex = i + 1; 362 } 363 364 365 void eol() { 366 IniLine iline = new IniLine; 367 iline.data = data[lineStartIndex .. i]; 368 debug(INI) writeln("INI line: ", iline.data); 369 isec.lines ~= iline; 370 } 371 372 373 char ch, ch2; 374 int i2; 375 isec = new IniSection(this, ""); 376 for(;;) { 377 ch = getc(); 378 switch(ch) { 379 case '\r': 380 eol(); 381 ch2 = getc(); 382 if(ch2 != '\n') 383 ungetc(); 384 reset(); 385 break; 386 387 case '\n': 388 eol(); 389 reset(); 390 break; 391 392 case 0: //eof 393 ini_eof: 394 if(lineStartIndex < i) { 395 eol(); 396 //reset(); 397 } 398 isecs ~= isec; 399 if(!isecs[0].lines) 400 isecs = isecs[1 .. isecs.length]; 401 debug(INI) 402 writeln("INI done parsing"); 403 return; 404 405 case ' ': 406 case '\t': 407 case '\v': 408 case '\f': 409 break; 410 411 case ';': //comments 412 case '#': 413 done_comment: 414 for(;;) { 415 ch2 = getc(); 416 switch(ch2) { 417 case '\r': 418 eol(); 419 ch2 = getc(); 420 if(ch2 != '\n') 421 ungetc(); 422 reset(); 423 break done_comment; 424 425 case '\n': 426 eol(); 427 reset(); 428 break done_comment; 429 430 case 0: //eof 431 goto ini_eof; 432 433 default: 434 break; 435 } 436 } 437 break; 438 439 default: 440 if(ch == secStart) { // '[' 441 i2 = i + 1; 442 done_sec: 443 for(;;) { 444 ch2 = getc(); 445 switch(ch2) { 446 case '\r': 447 eol(); 448 ch2 = getc(); 449 if(ch2 != '\n') 450 ungetc(); 451 reset(); 452 break done_sec; 453 454 case '\n': 455 eol(); 456 reset(); 457 break done_sec; 458 459 case 0: //eof 460 goto ini_eof; 461 462 default: 463 if(ch2 == secEnd) { // ']' 464 isecs ~= isec; 465 isec = new IniSection(this, data[i2 .. i]); 466 debug(INI) writeln("INI section: ", isec._name); 467 for(;;) { 468 ch2 = getc(); 469 switch(ch2) { 470 case ' ': 471 case '\t': 472 case '\v': 473 case '\f': 474 //ignore whitespace 475 break; 476 477 case '\r': 478 ch2 = getc(); 479 if(ch2 != '\n') 480 ungetc(); 481 break done_sec; 482 483 case '\n': 484 break done_sec; 485 486 default: 487 //just treat junk after the ] as the next line 488 ungetc(); 489 break done_sec; 490 } 491 } 492 break done_sec; 493 } 494 } 495 } 496 reset(); 497 break; 498 } else { //must be beginning of key name 499 i2 = i; 500 done_default: 501 for(;;) { 502 ch2 = getc(); 503 switch(ch2) { 504 case '\r': 505 eol(); 506 ch2 = getc(); 507 if(ch2 != '\n') 508 ungetc(); 509 reset(); 510 break done_default; 511 512 case '\n': 513 eol(); 514 reset(); 515 break done_default; 516 517 case 0: //eof 518 goto ini_eof; 519 520 case ' ': 521 case '\t': 522 case '\v': 523 case '\f': 524 break; 525 526 case '=': 527 IniKey ikey; 528 529 530 void addKey() { 531 ikey.data = data[lineStartIndex .. i]; 532 ikey._value = data[i2 .. i]; 533 isec.lines ~= ikey; 534 debug(INI) writeln("INI key: [", ikey._name, "] = [", ikey._value, "]"); 535 } 536 537 538 ikey = new IniKey(data[i2 .. i]); 539 i2 = i + 1; //after = 540 for(;;) { //get key value 541 ch2 = getc(); 542 switch(ch2) { 543 case '\r': 544 addKey(); 545 ch2 = getc(); 546 if(ch2 != '\n') 547 ungetc(); 548 reset(); 549 break done_default; 550 551 case '\n': 552 addKey(); 553 reset(); 554 break done_default; 555 556 case 0: //eof 557 addKey(); 558 reset(); 559 goto ini_eof; 560 561 default: 562 break; 563 } 564 } 565 break done_default; 566 567 default: 568 break; 569 } 570 } 571 } 572 } 573 } 574 } 575 576 577 void firstOpen(string file) { 578 //null terminated just to make it easier for the implementation 579 //_file = toStringz(file)[0 .. file.length]; 580 // JP Modified 581 _file = file; 582 parse(); 583 } 584 585 586 public: 587 // Added by Jesse Phillips 588 /// Upon the next save use this file. 589 string saveTo; 590 // Use different section name delimiters; not recommended. 591 this(string file, char secStart, char secEnd) { 592 this.secStart = secStart; 593 this.secEnd = secEnd; 594 595 firstOpen(file); 596 } 597 598 599 /// Construct a new INI _file. 600 this(string file) { 601 firstOpen(file); 602 } 603 604 605 ~this() { 606 debug(PRINT_DTORS) 607 writeln("~Ini ", _file); 608 609 // The reason this is commented is explained above. 610 /+ 611 if(_modified) 612 save(); 613 +/ 614 } 615 616 617 /// Comparison function for section and key names. Override to change behavior. 618 bool match(string s1, string s2) { 619 return !std..string.icmp(s1, s2); 620 } 621 622 623 //reuse same object for another file 624 /// Open an INI _file. 625 void open(string file) { 626 if(_modified) 627 save(); 628 _modified = false; 629 isecs = null; 630 631 firstOpen(file); 632 } 633 634 635 /// Reload INI file; any unsaved changes are lost. 636 void rehash() { 637 _modified = false; 638 isecs = null; 639 parse(); 640 } 641 642 643 /// Release memory without saving changes; contents become empty. 644 @safe nothrow 645 void dump() { 646 _modified = false; 647 isecs = null; 648 } 649 650 651 /// Property: get whether or not the INI file was _modified since it was loaded or saved. 652 @property @safe nothrow 653 bool modified() { 654 return _modified; 655 } 656 657 658 /// Params: 659 /// f = an opened-for-write stream; save() uses BufferedFile by default. Override save() to change stream. 660 protected final void saveToStream(File f) { 661 _modified = false; 662 663 // Если массив секций пустой, то выйти 664 if(!isecs.length) return; 665 666 IniKey ikey; 667 IniSection isec; 668 uint i = 0, j; 669 670 if(isecs[0]._name.length) 671 goto write_name; 672 else //first section doesn't have a name; just keys at start of file 673 goto after_name; 674 675 for(; i != isecs.length; i++) { 676 write_name: 677 // JP Modified added dup 678 f.writeln(secStart, isecs[i]._name, secEnd); 679 after_name: 680 isec = isecs[i]; 681 for(j = 0; j != isec.lines.length; j++) { 682 if(isec.lines[j].data is null) { 683 ikey = cast(IniKey)isec.lines[j]; 684 if(ikey) 685 ikey.data = ikey._name ~ "=" ~ ikey._value; 686 } 687 f.writeln(isec.lines[j].data); 688 } 689 } 690 } 691 692 /// Write contents to disk, even if no changes were made. It is common to do if(modified)save(); 693 void save() { 694 if(saveTo) { 695 _file = saveTo; 696 saveTo = null; 697 } 698 File f = File(_file, "w"); 699 // f.create(_file); 700 try { 701 saveToStream(f); 702 f.flush(); 703 } 704 finally { 705 f.close(); 706 } 707 } 708 709 /// Write contents to disk with filename 710 // Added by Jesse Phillips 711 void save(string filename) { 712 _file = filename; 713 save(); 714 } 715 716 717 /// Finds a _section; returns null if one named name does not exist. 718 IniSection section(string name) { 719 foreach(IniSection isec; isecs) { 720 if(match(isec._name, name)) 721 return isec; 722 } 723 return null; //didn't find it 724 } 725 726 727 /// Shortcut for section(sectionName). 728 IniSection opIndex(string sectionName) { 729 return section(sectionName); 730 } 731 732 733 /// The section is created if one named name does not exist. 734 /// Returns: Section named name. 735 IniSection addSection(string name) { 736 IniSection isec = section(name); 737 if(!isec) { 738 isec = new IniSection(this, name); 739 _modified = true; 740 isecs ~= isec; 741 } 742 return isec; 743 } 744 745 746 /// foreach section. 747 int opApply(int delegate(ref IniSection) dg) { 748 int result = 0; 749 foreach(IniSection isec; isecs) { 750 result = dg(isec); 751 if(result) 752 break; 753 } 754 return result; 755 } 756 757 758 /// Property: get all _sections. 759 @property @safe nothrow 760 IniSection[] sections() { 761 return isecs; 762 } 763 764 765 /// _Remove section named sectionName. 766 void remove(string sectionName) { 767 uint i; 768 for(i = 0; i != isecs.length; i++) { 769 if(match(sectionName, isecs[i]._name)) { 770 if(i == isecs.length - 1) 771 isecs = isecs[0 .. i]; 772 else if(i == 0) 773 isecs = isecs[1 .. isecs.length]; 774 else 775 isecs = isecs[0 .. i] ~ isecs[i + 1 .. isecs.length]; 776 _modified = true; 777 return; 778 } 779 } 780 } 781 } 782 783 784 unittest { 785 string inifile = "unittest.ini"; 786 // Jesse Phillips 787 // Remove file when done. 788 scope(exit) 789 std.file.remove(inifile); 790 Ini ini; 791 792 ini = new Ini(inifile); 793 with(ini.addSection("foo")) { 794 value("asdf", "jkl"); 795 value("bar", "wee!"); 796 value("hi", "hello"); 797 } 798 ini.addSection("BAR"); 799 with(ini.addSection("fOO")) { 800 value("yes", "no"); 801 } 802 with(ini.addSection("Hello")) { 803 value("world", "true"); 804 } 805 with(ini.addSection("test")) { 806 value("1", "2"); 807 value("3", "4"); 808 } 809 ini["test"]["value"] = "true"; 810 assert(ini["Foo"]["yes"] == "no"); 811 ini.save(); 812 delete ini; 813 814 ini = new Ini(inifile); 815 assert(ini["FOO"]["Bar"] == "wee!"); // 816 assert(ini["Foo"]["yes"] == "no"); 817 assert(ini["hello"]["world"] == "true"); 818 assert(ini["FOO"]["Bar"] == "wee!"); 819 assert(ini["55"] is null); 820 assert(ini["hello"]["Yes"] is null); 821 822 ini.open(inifile); 823 ini["bar"].remove("notta"); 824 ini["foo"].remove("bar"); 825 ini.remove("bar"); 826 assert(ini["bar"] is null); 827 assert(ini["foo"] !is null); 828 assert(ini["foo"]["bar"] is null); 829 ini.remove("foo"); 830 assert(ini["foo"] is null); 831 ini.save(); 832 delete ini; 833 }