1 /******************************************************************************* 2 3 Load Configuration from Config File 4 5 Copyright: 6 Copyright (c) 2009-2017 dunnhumby Germany GmbH. 7 All rights reserved. 8 9 License: 10 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 11 Alternatively, this file may be distributed under the terms of the Tango 12 3-Clause BSD License (see LICENSE_BSD.txt for details). 13 14 *******************************************************************************/ 15 16 module ocean.util.config.ConfigParser; 17 18 19 import ocean.transition; 20 21 import ocean.core.Array; 22 import ocean.core.ExceptionDefinitions; 23 import ocean.core.Exception; 24 import ocean.core.Enforce; 25 import ocean.meta.types.Arrays /* : ElementTypeOf */; 26 import ocean.io.Stdout; 27 import ocean.io.stream.Format; 28 import ocean.io.stream.TextFile; 29 import ocean.text.convert.Float: toFloat; 30 import ocean.text.convert.Formatter; 31 import ocean.text.convert.Integer_tango: toLong; 32 import ocean.text.convert.Utf; 33 import ocean.text.Util: locate, trim, delimit, lines; 34 35 /****************************************************************************** 36 37 ConfigException 38 39 *******************************************************************************/ 40 41 /// Exception thrown whenever an error happened while parsing / using 42 /// the configuration, such as `getStrict` on a missing field 43 public class ConfigException : Exception 44 { 45 mixin DefaultExceptionCtor; 46 } 47 48 /******************************************************************************* 49 50 Config reads all properties of the application from an INI style of the 51 following format: 52 53 --- 54 // -------------------------- 55 // Config Example 56 // -------------------------- 57 58 ; Database config parameters 59 [DATABASE] 60 table1 = "name_of_table1" 61 table2 = "name_of_table2" 62 63 ; An example of a multi-value parameter 64 fields = "create_time" 65 "update_time" 66 "count" 67 68 ; Logging config parameters 69 [LOGGING] 70 level = info 71 file = "access.log" 72 --- 73 74 The properties defined in the file are read and stored in an internal array, 75 which can then be accessed through get and set methods as follows: 76 77 The parseFile() method only needs to be called once, though may be called 78 multiple times if the config file needs to be re-read from the file on disk. 79 80 TODO: 81 At the moment it's possible to set a key (se `set`), but it's not possible 82 to write back to the file. 83 84 *******************************************************************************/ 85 86 public class ConfigParser 87 { 88 /*************************************************************************** 89 90 Variable Iterator. Iterates over keys or key/value pairs of a category. 91 The values are converted to T, unless T is istring. 92 93 ***************************************************************************/ 94 95 public struct VarIterator ( T = istring ) 96 { 97 ValueNode[istring]* vars; 98 99 100 /*********************************************************************** 101 102 Variable Iterator. Iterates over key/value pairs of a category. 103 104 ***********************************************************************/ 105 106 public int opApply ( scope int delegate ( ref istring key, ref T val ) dg ) 107 { 108 if ( (&this).vars !is null ) 109 { 110 foreach ( key, valnode; *(&this).vars ) 111 { 112 auto val = conv!(T)(valnode.value); 113 114 if ( int result = dg(key, val) ) 115 return result; 116 } 117 } 118 119 return 0; 120 } 121 122 123 /*********************************************************************** 124 125 Variable Iterator. Iterates over keys of a category. 126 127 ***********************************************************************/ 128 129 public int opApply ( scope int delegate ( ref istring x ) dg ) 130 { 131 return (&this).opApply( 132 (ref istring key, ref istring val) 133 { 134 return dg(key); 135 }); 136 } 137 } 138 139 140 /*************************************************************************** 141 142 Immediate context of the current line being parsed 143 144 ***************************************************************************/ 145 146 private struct ParsingContext 147 { 148 /*********************************************************************** 149 150 Current category being parsed 151 152 ***********************************************************************/ 153 154 mstring category; 155 156 157 /*********************************************************************** 158 159 Current key being parsed 160 161 ***********************************************************************/ 162 163 mstring key; 164 165 166 /*********************************************************************** 167 168 Current value being parsed 169 170 ***********************************************************************/ 171 172 mstring value; 173 174 175 /*********************************************************************** 176 177 True if we are at the first multiline value when parsing 178 179 ***********************************************************************/ 180 181 bool multiline_first = true; 182 } 183 184 private ParsingContext context; 185 186 187 /*************************************************************************** 188 189 Structure representing a single value node in the configuration. 190 191 ***************************************************************************/ 192 193 private struct ValueNode 194 { 195 /*********************************************************************** 196 197 The actual value. 198 199 ***********************************************************************/ 200 201 istring value; 202 203 204 /*********************************************************************** 205 206 Flag used to allow a config file to be parsed, even when a different 207 configuration has already been parsed in the past. 208 209 At the start of every new parse, the flags of all value nodes in an 210 already parsed configuration are set to false. If this value node is 211 found during the parse, its flag is set to true. All new value nodes 212 added will also have the flag set to true. At the end of the parse, 213 all value nodes that have the flag set to false are removed. 214 215 **********************************************************************/ 216 217 bool present_in_config; 218 } 219 220 221 /*************************************************************************** 222 223 Config Keys and Properties 224 225 ***************************************************************************/ 226 227 alias istring String; 228 private ValueNode[String][String] properties; 229 230 231 /*************************************************************************** 232 233 Config File Location 234 235 ***************************************************************************/ 236 237 private istring config_file; 238 239 240 /*************************************************************************** 241 242 Constructor 243 244 ***************************************************************************/ 245 246 public this ( ) 247 { } 248 249 250 /*************************************************************************** 251 252 Constructor 253 254 Params: 255 config = path to the configuration file 256 257 ***************************************************************************/ 258 259 public this ( istring config ) 260 { 261 this.parseFile(config); 262 } 263 264 265 /*************************************************************************** 266 267 Returns an iterator over keys or key/value pairs in a category. 268 The values are converted to T, unless T is istring. 269 270 Params: 271 category = category to iterate over 272 273 Returns: 274 an iterator over the keys or key/value pairs in category. 275 276 ***************************************************************************/ 277 278 public VarIterator!(T) iterateCategory ( T = istring ) ( cstring category ) 279 { 280 return VarIterator!(T)(category in this.properties); 281 } 282 283 284 /*************************************************************************** 285 286 Iterator. Iterates over categories of the config file 287 288 ***************************************************************************/ 289 290 public int opApply ( scope int delegate ( ref istring x ) dg ) 291 { 292 int result = 0; 293 294 foreach ( key, val; this.properties ) 295 { 296 result = dg(key); 297 298 if ( result ) break; 299 } 300 301 return result; 302 } 303 304 305 /*************************************************************************** 306 307 Read Config File 308 309 Reads the content of the configuration file and copies to a static 310 array buffer. 311 312 Each property in the ini file belongs to a category. A property always 313 has a key and a value associated with the key. The function parses the 314 following different elements: 315 316 i. Categories 317 [Example Category] 318 319 ii. Comments 320 // comments start with two slashes, 321 ; a semi-colon 322 # or a hash 323 324 iii. Property 325 key = value 326 327 iv. Multi-value property 328 key = value1 329 value2 330 value3 331 332 Params: 333 file_path = string that contains the path to the configuration file 334 clean_old = true if the existing configuration should be overwritten 335 with the result of the current parse, false if the 336 current parse should only add to or update the existing 337 configuration. (defaults to true) 338 339 ***************************************************************************/ 340 341 public void parseFile ( istring file_path = "etc/config.ini", 342 bool clean_old = true ) 343 { 344 this.config_file = file_path; 345 346 auto get_line = new TextFileInput(this.config_file); 347 348 this.parseIter(get_line, clean_old); 349 } 350 351 /// 352 unittest 353 { 354 void main () 355 { 356 scope config = new ConfigParser(); 357 config.parseFile("etc/config.ini"); 358 } 359 } 360 361 362 /*************************************************************************** 363 364 Parse a string 365 366 See parseFile() for details on the parsed syntax. 367 368 Usage Example: 369 370 --- 371 372 Config.parseString( 373 "[section]\n" 374 "key = value1\n" 375 " value2\n" 376 " value3\n" 377 ); 378 379 --- 380 381 Params: 382 T = Type of characters in the string 383 str = string to parse 384 clean_old = true if the existing configuration should be overwritten 385 with the result of the current parse, false if the 386 current parse should only add to or update the existing 387 configuration. (defaults to true) 388 389 ***************************************************************************/ 390 391 public void parseString (T : dchar) ( T[] str, bool clean_old = true ) 392 { 393 static struct Iterator 394 { 395 T[] data; 396 397 int opApply ( scope int delegate ( ref T[] x ) dg ) 398 { 399 int result = 0; 400 401 foreach ( ref line; lines((&this).data) ) 402 { 403 result = dg(line); 404 405 if ( result ) break; 406 } 407 408 return result; 409 } 410 } 411 412 this.parseIter(Iterator(str), clean_old); 413 } 414 415 416 /*************************************************************************** 417 418 Tells whether the config object has no values loaded. 419 420 Returns: 421 true if it doesn't have any values, false otherwise 422 423 ***************************************************************************/ 424 425 public bool isEmpty() 426 { 427 return this.properties.length == 0; 428 } 429 430 431 /*************************************************************************** 432 433 Checks if Key exists in Category 434 435 Params: 436 category = category in which to look for the key 437 key = key to be checked 438 439 Returns: 440 true if the configuration key exists in this category 441 442 ***************************************************************************/ 443 444 public bool exists ( cstring category, cstring key ) 445 { 446 return ((category in this.properties) && 447 (key in this.properties[category])); 448 } 449 450 451 /*************************************************************************** 452 453 Strict method to get the value of a config key. If the requested key 454 cannot be found, an exception is thrown. 455 456 Template can be instantiated with integer, float or string (istring) 457 type. 458 459 Usage Example: 460 461 --- 462 463 Config.parseFile("some-config.ini"); 464 // throws if not found 465 auto str = Config.getStrict!(istring)("some-cat", "some-key"); 466 auto n = Config.getStrict!(int)("some-cat", "some-key"); 467 468 --- 469 470 Params: 471 category = category to get key from 472 key = key whose value is to be got 473 474 Throws: 475 if the specified key does not exist 476 477 Returns: 478 value of a configuration key, or null if none 479 480 ***************************************************************************/ 481 482 public T getStrict ( T ) ( cstring category, cstring key ) 483 { 484 enforce!(ConfigException)( 485 exists(category, key), 486 format("Critical Error: No configuration key '{}:{}' found", 487 category, key) 488 ); 489 try 490 { 491 auto value_node = this.properties[category][key]; 492 493 return conv!(T)(value_node.value); 494 } 495 catch ( IllegalArgumentException ) 496 { 497 throw new ConfigException( 498 format("Critical Error: Configuration key '{}:{}' " 499 ~ "appears not to be of type '{}'", 500 category, key, T.stringof)); 501 } 502 503 assert(0); 504 } 505 506 507 /*************************************************************************** 508 509 Alternative form strict config value getter, returning the retrieved 510 value via a reference. (The advantage being that the template type can 511 then be inferred by the compiler.) 512 513 Template can be instantiated with integer, float or string (istring) 514 type. 515 516 Usage Example: 517 518 --- 519 520 Config.parseFile("some-config.ini"); 521 // throws if not found 522 istring str; 523 int n; 524 525 Config.getStrict(str, "some-cat", "some-key"); 526 Config.getStrict(n, "some-cat", "some-key"); 527 528 --- 529 530 Params: 531 value = output for config value 532 category = category to get key from 533 key = key whose value is to be got 534 535 Throws: 536 if the specified key does not exist 537 538 TODO: perhaps we should discuss removing the other version of 539 getStrict(), above? It seems a little bit confusing having both methods, 540 and I feel this version is more convenient to use. 541 542 ***************************************************************************/ 543 544 public void getStrict ( T ) ( ref T value, cstring category, cstring key ) 545 { 546 value = this.getStrict!(T)(category, key); 547 } 548 549 550 /*************************************************************************** 551 552 Non-strict method to get the value of a config key into the specified 553 output value. If the config key does not exist, the given default value 554 is returned. 555 556 Template can be instantiated with integer, float or string (istring) 557 type. 558 559 Usage Example: 560 561 --- 562 563 Config.parseFile("some-config.ini"); 564 auto str = Config.get("some-cat", "some-key", "my_default_value"); 565 int n = Config.get("some-cat", "some-int", 5); 566 567 --- 568 569 Params: 570 category = category to get key from 571 key = key whose value is to be got 572 default_value = default value to use if missing in the config 573 574 Returns: 575 config value, if existing, otherwise default value 576 577 ***************************************************************************/ 578 579 public SliceIfD1StaticArray!(T) get ( T ) ( cstring category, cstring key, 580 T default_value ) 581 { 582 if ( exists(category, key) ) 583 { 584 return getStrict!(SliceIfD1StaticArray!(T))(category, key); 585 } 586 return default_value; 587 } 588 589 590 /*************************************************************************** 591 592 Alternative form non-strict config value getter, returning the retrieved 593 value via a reference. (For interface consistency with the reference 594 version of getStrict(), above.) 595 596 Template can be instantiated with integer, float or string (istring) 597 type. 598 599 Usage Example: 600 601 --- 602 603 Config.parseFile("some-config.ini"); 604 istring str; 605 int n; 606 607 Config.get(str, "some-cat", "some-key", "default value"); 608 Config.get(n, "some-cat", "some-key", 23); 609 610 --- 611 612 Params: 613 value = output for config value 614 category = category to get key from 615 key = key whose value is to be got 616 default_value = default value to use if missing in the config 617 618 TODO: perhaps we should discuss removing the other version of 619 get(), above? It seems a little bit confusing having both methods, 620 and I feel the reference version is more convenient to use. 621 622 ***************************************************************************/ 623 624 public void get ( T ) ( ref T value, cstring category, 625 cstring key, T default_value ) 626 { 627 value = this.get(category, key, default_value); 628 } 629 630 631 /*************************************************************************** 632 633 Strict method to get a multi-line value. If the requested key cannot be 634 found, an exception is thrown. 635 636 Retrieves the value list of a configuration key with a multi-line value. 637 If the value is a single line, the list has one element. 638 639 Params: 640 category = category to get key from 641 key = key whose value is to be got 642 643 Throws: 644 if the specified key does not exist 645 646 Returns: 647 list of values 648 649 ***************************************************************************/ 650 651 public T[] getListStrict ( T = istring ) ( cstring category, cstring key ) 652 { 653 auto value = this.getStrict!(istring)(category, key); 654 T[] r; 655 foreach ( elem; delimit(value, "\n") ) 656 { 657 r ~= this.conv!(T)(elem); 658 } 659 return r; 660 } 661 662 663 /*************************************************************************** 664 665 Non-strict method to get a multi-line value. The existence or 666 non-existence of the key is returned. If the configuration key cannot be 667 found, the output list remains unchanged. 668 669 If the value is a single line, the output list has one element. 670 671 Params: 672 category = category to get key from 673 key = key whose value is to be got 674 default_value = default list to use if missing in the config 675 676 Returns: 677 the list of values corresponding to the given category + key 678 combination if such a combination exists, the given list of default 679 values otherwise 680 681 ***************************************************************************/ 682 683 public T[] getList ( T = istring ) ( cstring category, cstring key, 684 T[] default_value ) 685 { 686 if ( exists(category, key) ) 687 { 688 return getListStrict!(T)(category, key); 689 } 690 return default_value; 691 } 692 693 694 /*************************************************************************** 695 696 Set Config-Key Property 697 698 Usage Example: 699 700 --- 701 702 Config.parseFile(`etc/config.ini`); 703 704 Config.set(`category`, `key`, `value`); 705 706 --- 707 708 Params: 709 category = category to be set 710 key = key to be set 711 value = value of the property 712 713 ***************************************************************************/ 714 715 public void set ( istring category, istring key, istring value ) 716 { 717 if ( category == "" || key == "" || value == "" ) 718 { 719 return; 720 } 721 722 if ( this.exists(category, key) ) 723 { 724 (this.properties[category][key]).value = value; 725 } 726 else 727 { 728 ValueNode value_node = { value, true }; 729 730 this.properties[category][key] = value_node; 731 } 732 } 733 734 735 /*************************************************************************** 736 737 Remove Config-Key Property 738 739 Usage Example: 740 741 --- 742 743 Config.parseFile(`etc/config.ini`); 744 745 Config.remove(`category`, `key`); 746 747 --- 748 749 Params: 750 category = category from which the property is to be removed 751 key = key of the property to be removed 752 753 ***************************************************************************/ 754 755 public void remove ( istring category, istring key ) 756 { 757 if ( category == "" || key == "" ) 758 { 759 return; 760 } 761 762 if ( this.exists(category, key) ) 763 { 764 (this.properties[category][key]).present_in_config = false; 765 766 this.pruneConfiguration(); 767 } 768 } 769 770 771 /*************************************************************************** 772 773 Prints the current configuration to the given formatted text stream. 774 775 Note that no guarantees can be made about the order of the categories 776 or the order of the key-value pairs within each category. 777 778 Params: 779 output = formatted text stream in which to print the configuration 780 (defaults to Stdout) 781 782 ***************************************************************************/ 783 784 public void print ( FormatOutput output = Stdout ) 785 { 786 foreach ( category, key_value_pairs; this.properties ) 787 { 788 output.formatln("{}", category); 789 790 foreach ( key, value_node; key_value_pairs ) 791 { 792 output.formatln(" {} = {}", key, value_node.value); 793 } 794 } 795 } 796 797 798 /*************************************************************************** 799 800 Actually performs parsing of the lines of a config file or a string. 801 Each line to be parsed is obtained via an iterator. 802 803 Params: 804 I = type of the iterator that will supply lines to be parsed 805 iter = iterator that will supply lines to be parsed 806 clean_old = true if the existing configuration should be overwritten 807 with the result of the current parse, false if the 808 current parse should only add to or update the existing 809 configuration. 810 811 ***************************************************************************/ 812 813 private void parseIter ( I ) ( I iter, bool clean_old ) 814 { 815 this.clearParsingContext(); 816 817 if ( clean_old ) 818 { 819 this.clearAllValueNodeFlags(); 820 } 821 822 foreach ( ref line; iter ) 823 { 824 this.parseLine(line); 825 } 826 827 this.saveFromParsingContext(); 828 829 this.pruneConfiguration(); 830 831 this.clearParsingContext(); 832 } 833 834 835 /*************************************************************************** 836 837 Converts a string to a boolean value. The following string values are 838 accepted: 839 840 false / true, disabled / enabled, off / on, no / yes, 0 / 1 841 842 Params: 843 property = string to extract boolean value from 844 845 Throws: 846 if the string does not match one of the possible boolean strings 847 848 Returns: 849 boolean value interpreted from string 850 851 ***************************************************************************/ 852 853 private static bool toBool ( cstring property ) 854 { 855 static immutable istring[2][] BOOL_IDS = 856 [ 857 ["false", "true"], 858 ["disabled", "enabled"], 859 ["off", "on"], 860 ["no", "yes"], 861 ["0", "1"] 862 ]; 863 864 foreach ( id; BOOL_IDS ) 865 { 866 if ( property == id[0] ) return false; 867 if ( property == id[1] ) return true; 868 } 869 870 throw new IllegalArgumentException( 871 "Config.toBool :: invalid boolean value"); 872 } 873 874 875 /*************************************************************************** 876 877 Converts property to T 878 879 Params: 880 property = value to convert 881 882 Returns: 883 property converted to T 884 885 ***************************************************************************/ 886 887 private static T conv ( T ) ( cstring property ) 888 { 889 static if ( is(T : bool) ) 890 { 891 return toBool(property); 892 } 893 else static if ( is(T : long) ) 894 { 895 auto v = toLong(property); 896 enforce!(IllegalArgumentException)( 897 v >= T.min && v <= T.max, 898 "Value of " ~ cast(istring) property ~ " is out of " ~ T.stringof ~ " bounds"); 899 return cast(T) v; 900 } 901 else static if ( is(T : real) ) 902 { 903 return toFloat(property); 904 } 905 else static if ( is(T U : U[]) && 906 ( is(Unqual!(U) : char) || is(Unqual!(U) : wchar) 907 || is(Unqual!(U) : dchar)) ) 908 { 909 auto r = fromString8!(Unqual!(U))(property, T.init); 910 return cast(T) r.dup; 911 } 912 else 913 { 914 static assert(false, 915 __FILE__ ~ " : get(): type '" ~ T.stringof 916 ~ "' is not supported"); 917 } 918 } 919 920 921 /*************************************************************************** 922 923 Saves the current contents of the context into the configuration. 924 925 ***************************************************************************/ 926 927 private void saveFromParsingContext ( ) 928 { 929 auto ctx = &this.context; 930 931 if ( ctx.category.length == 0 || 932 ctx.key.length == 0 || 933 ctx.value.length == 0 ) 934 { 935 return; 936 } 937 938 if ( this.exists(ctx.category, ctx.key) ) 939 { 940 ValueNode * value_node = &this.properties[ctx.category][ctx.key]; 941 942 if ( value_node.value != ctx.value ) 943 { 944 value_node.value = idup(ctx.value); 945 } 946 947 value_node.present_in_config = true; 948 } 949 else 950 { 951 ValueNode value_node = { ctx.value.dup, true }; 952 953 this.properties[idup(ctx.category)][idup(ctx.key)] = value_node; 954 } 955 956 ctx.value.length = 0; 957 enableStomping(ctx.value); 958 } 959 960 961 /*************************************************************************** 962 963 Clears the 'present_in_config' flags associated with all value nodes in 964 the configuration. 965 966 ***************************************************************************/ 967 968 private void clearAllValueNodeFlags ( ) 969 { 970 foreach ( category, key_value_pairs; this.properties ) 971 { 972 foreach ( key, ref value_node; key_value_pairs ) 973 { 974 value_node.present_in_config = false; 975 } 976 } 977 } 978 979 980 /*************************************************************************** 981 982 Prunes the configuration removing all keys whose value nodes have the 983 'present_in_config' flag set to false. Also removes all categories that 984 have no keys. 985 986 ***************************************************************************/ 987 988 private void pruneConfiguration ( ) 989 { 990 istring[] keys_to_remove; 991 istring[] categories_to_remove; 992 993 // Remove obsolete keys 994 995 foreach ( category, ref key_value_pairs; this.properties ) 996 { 997 foreach ( key, value_node; key_value_pairs ) 998 { 999 if ( ! value_node.present_in_config ) 1000 { 1001 keys_to_remove ~= key; 1002 } 1003 } 1004 1005 foreach ( key; keys_to_remove ) 1006 { 1007 key_value_pairs.remove(key); 1008 } 1009 1010 keys_to_remove.length = 0; 1011 enableStomping(keys_to_remove); 1012 } 1013 1014 // Remove categories that have no keys 1015 1016 foreach ( category, key_value_pairs; this.properties ) 1017 { 1018 if ( key_value_pairs.length == 0 ) 1019 { 1020 categories_to_remove ~= category; 1021 } 1022 } 1023 1024 foreach ( category; categories_to_remove ) 1025 { 1026 this.properties.remove(category); 1027 } 1028 } 1029 1030 1031 /*************************************************************************** 1032 1033 Clears the current parsing context. 1034 1035 ***************************************************************************/ 1036 1037 private void clearParsingContext ( ) 1038 { 1039 auto ctx = &this.context; 1040 1041 ctx.value.length = 0; 1042 enableStomping(ctx.value); 1043 ctx.category.length = 0; 1044 enableStomping(ctx.category); 1045 ctx.key.length = 0; 1046 enableStomping(ctx.key); 1047 ctx.multiline_first = true; 1048 } 1049 1050 1051 /*************************************************************************** 1052 1053 Parse a line 1054 1055 See parseFile() for details on the parsed syntax. This method only makes 1056 sense to do partial parsing of a string. 1057 1058 Usage Example: 1059 1060 --- 1061 1062 Config.parseLine("[section]"); 1063 Config.parseLine("key = value1\n"); 1064 Config.parseLine(" value2\n"); 1065 Config.parseLine(" value3\n"); 1066 1067 --- 1068 1069 Params: 1070 line = line to parse 1071 1072 ***************************************************************************/ 1073 1074 private void parseLine ( cstring line ) 1075 { 1076 auto ctx = &this.context; 1077 1078 line = trim(line); 1079 1080 if ( line.length == 0 ) 1081 { 1082 // Ignore empty lines. 1083 return; 1084 } 1085 1086 bool slash_comment = line.length >= 2 && line[0 .. 2] == "//"; 1087 bool hash_comment = line[0] == '#'; 1088 bool semicolon_comment = line[0] == ';'; 1089 1090 if ( slash_comment || semicolon_comment || hash_comment ) 1091 { 1092 // Ignore comment lines. 1093 return; 1094 } 1095 1096 auto pos = locate(line, '['); // category present in line? 1097 1098 if ( pos == 0 ) 1099 { 1100 this.saveFromParsingContext(); 1101 1102 auto cat = trim(line[pos + 1 .. locate(line, ']')]); 1103 1104 ctx.category.copy(cat); 1105 1106 ctx.key.length = 0; 1107 enableStomping(ctx.key); 1108 } 1109 else 1110 { 1111 pos = locate(line, '='); // check for key value pair 1112 1113 if ( pos < line.length ) 1114 { 1115 this.saveFromParsingContext(); 1116 1117 ctx.key.copy(trim(line[0 .. pos])); 1118 1119 ctx.value.copy(trim(line[pos + 1 .. $])); 1120 1121 ctx.multiline_first = !ctx.value.length; 1122 } 1123 else 1124 { 1125 if ( ! ctx.multiline_first ) 1126 { 1127 ctx.value ~= '\n'; 1128 } 1129 1130 ctx.value ~= line; 1131 1132 ctx.multiline_first = false; 1133 } 1134 } 1135 } 1136 } 1137 1138 /// Usage example 1139 unittest 1140 { 1141 void main () 1142 { 1143 // Read config file from disk 1144 scope config = new ConfigParser("etc/config.ini"); 1145 1146 // Read a single value 1147 istring value = config.getStrict!(istring)("category", "key"); 1148 1149 // Set a single value 1150 config.set("category", "key", "new value"); 1151 1152 // Read a multi-line value 1153 istring[] values = config.getListStrict("category", "key"); 1154 } 1155 } 1156 1157 1158 version ( UnitTest ) 1159 { 1160 import ocean.core.Test; 1161 } 1162 1163 unittest 1164 { 1165 struct ConfigSanity 1166 { 1167 uint num_categories; 1168 1169 cstring[] categories; 1170 1171 cstring[] keys; 1172 } 1173 1174 void parsedConfigSanityCheck ( ConfigParser config, ConfigSanity expected, 1175 istring test_name ) 1176 { 1177 auto t = new NamedTest(test_name); 1178 cstring[] obtained_categories; 1179 cstring[] obtained_keys; 1180 1181 t.test!("==")(config.isEmpty, (expected.num_categories == 0)); 1182 1183 foreach ( category; config ) 1184 { 1185 obtained_categories ~= category; 1186 1187 foreach ( key; config.iterateCategory(category) ) 1188 { 1189 obtained_keys ~= key; 1190 } 1191 } 1192 1193 t.test!("==")(obtained_categories.length, expected.num_categories); 1194 1195 t.test!("==")(sort(obtained_categories), sort(expected.categories)); 1196 1197 t.test!("==")(sort(obtained_keys), sort(expected.keys)); 1198 } 1199 1200 // Wrapper function that just calls the 'parsedConfigSanityCheck' function, 1201 // but appends the line number to the test name. This is useful when 1202 // slightly different variations of the same basic type of test need to be 1203 // performed. 1204 void parsedConfigSanityCheckN ( ConfigParser config, ConfigSanity expected, 1205 cstring test_name, 1206 typeof(__LINE__) line_num = __LINE__ ) 1207 { 1208 parsedConfigSanityCheck(config, expected, 1209 format("{} (line: {})", test_name, line_num)); 1210 } 1211 1212 scope Config = new ConfigParser(); 1213 1214 /*************************************************************************** 1215 1216 Section 1: unit-tests to confirm correct parsing of config files 1217 1218 ***************************************************************************/ 1219 1220 auto str1 = 1221 ` 1222 [Section1] 1223 multiline = a 1224 # unittest comment 1225 b 1226 ; comment with a different style in multiline 1227 c 1228 // and the ultimative comment 1229 d 1230 int_arr = 30 1231 40 1232 -60 1233 1111111111 1234 0x10 1235 ulong_arr = 0 1236 50 1237 18446744073709551615 1238 0xa123bcd 1239 float_arr = 10.2 1240 -25.3 1241 90 1242 0.000000001 1243 bool_arr = true 1244 false 1245 `.dup; 1246 ConfigSanity str1_expectations = 1247 { 1, 1248 [ "Section1" ], 1249 [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr" ] 1250 }; 1251 1252 Config.parseString(str1); 1253 parsedConfigSanityCheck(Config, str1_expectations, "basic string"); 1254 1255 scope l = Config.getListStrict("Section1", "multiline"); 1256 1257 test!("==")(l.length, 4); 1258 1259 test!("==")(l, ["a", "b", "c", "d"][]); 1260 1261 scope ints = Config.getListStrict!(int)("Section1", "int_arr"); 1262 test!("==")(ints, [30, 40, -60, 1111111111, 0x10][]); 1263 1264 scope ulong_arr = Config.getListStrict!(ulong)("Section1", "ulong_arr"); 1265 ulong[] ulong_array = [0, 50, ulong.max, 0xa123bcd]; 1266 test!("==")(ulong_arr, ulong_array); 1267 1268 scope float_arr = Config.getListStrict!(float)("Section1", "float_arr"); 1269 float[] float_array = [10.2, -25.3, 90, 0.000000001]; 1270 test!("==")(float_arr, float_array); 1271 1272 scope bool_arr = Config.getListStrict!(bool)("Section1", "bool_arr"); 1273 test!("==")(bool_arr, [true, false][]); 1274 1275 try 1276 { 1277 scope w_bool_arr = Config.getListStrict!(bool)("Section1", "int_arr"); 1278 } 1279 catch ( IllegalArgumentException e ) 1280 { 1281 test!("==")(e.message(), "Config.toBool :: invalid boolean value"[]); 1282 } 1283 1284 // Manually set a property (new category). 1285 Config.set("Section2", "set_key", "set_value"[]); 1286 1287 istring new_val; 1288 Config.getStrict(new_val, "Section2", "set_key"); 1289 test!("==")(new_val, "set_value"[]); 1290 1291 // Manually set a property (existing category, new key). 1292 Config.set("Section2", "another_set_key", "another_set_value"[]); 1293 1294 Config.getStrict(new_val, "Section2", "another_set_key"); 1295 test!("==")(new_val, "another_set_value"[]); 1296 1297 // Manually set a property (existing category, existing key). 1298 Config.set("Section2", "set_key", "new_set_value"); 1299 1300 Config.getStrict(new_val, "Section2", "set_key"); 1301 test!("==")(new_val, "new_set_value"[]); 1302 1303 // Check if the 'exists' function works as expected. 1304 test( Config.exists("Section1", "int_arr"), "exists API failure"); 1305 test(!Config.exists("Section420", "int_arr"), "exists API failure"); 1306 test(!Config.exists("Section1", "key420"), "exists API failure"); 1307 1308 ConfigSanity new_str1_expectations = 1309 { 2, 1310 [ "Section1", "Section2" ], 1311 [ "multiline", "int_arr", "ulong_arr", "float_arr", "bool_arr", 1312 "set_key", "another_set_key" ] 1313 }; 1314 parsedConfigSanityCheck(Config, new_str1_expectations, "modified string"); 1315 1316 // Remove properties from the config. 1317 Config.remove("Section2", "set_key"); 1318 Config.remove("Section2", "another_set_key"); 1319 parsedConfigSanityCheck(Config, str1_expectations, "back to basic string"); 1320 1321 // getList tests 1322 scope gl1 = Config.getList("Section1", "dummy", 1323 ["this", "is", "a", "list", "of", "default", "values"]); 1324 test!("==")(gl1.length, 7); 1325 test!("==")(gl1, ["this", "is", "a", "list", "of", "default", "values"][]); 1326 1327 scope gl2 = Config.getList("Section1", "multiline", 1328 ["this", "is", "a", "list", "of", "default", "values"]); 1329 test!("==")(gl2.length, 4); 1330 test!("==")(gl2, ["a", "b", "c", "d"][]); 1331 1332 // Whitespaces handling 1333 1334 istring white_str = 1335 ` 1336 [ Section1 ] 1337 key = val 1338 `; 1339 ConfigSanity white_str_expectations = 1340 { 1, 1341 [ "Section1" ], 1342 [ "key" ] 1343 }; 1344 1345 Config.parseString(white_str); 1346 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1347 1348 white_str = 1349 ` 1350 [Section1 ] 1351 key = val 1352 `; 1353 Config.parseString(white_str); 1354 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1355 1356 white_str = 1357 ` 1358 [ Section1] 1359 key = val 1360 `; 1361 Config.parseString(white_str); 1362 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1363 1364 white_str = 1365 ` 1366 [Section1] 1367 key = val 1368 `; 1369 Config.parseString(white_str); 1370 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1371 1372 white_str = 1373 ` 1374 [Section1] 1375 key = val 1376 `; 1377 Config.parseString(white_str); 1378 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1379 1380 white_str = 1381 ` 1382 [Section1] 1383 key = val 1384 `; 1385 Config.parseString(white_str); 1386 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1387 1388 white_str = 1389 ` 1390 [ Section1 ] 1391 key = val 1392 `; 1393 Config.parseString(white_str); 1394 parsedConfigSanityCheckN(Config, white_str_expectations, "white spaces"); 1395 1396 // Parse a new configuration 1397 1398 auto str2 = 1399 ` 1400 [German] 1401 one = eins 1402 two = zwei 1403 three = drei 1404 [Hindi] 1405 one = ek 1406 two = do 1407 three = teen 1408 `; 1409 ConfigSanity str2_expectations = 1410 { 2, 1411 [ "German", "Hindi" ], 1412 [ "one", "two", "three", "one", "two", "three" ], 1413 }; 1414 1415 Config.parseString(str2); 1416 parsedConfigSanityCheck(Config, str2_expectations, "new string"); 1417 1418 1419 /*************************************************************************** 1420 1421 Section 2: unit-tests to check memory usage 1422 1423 ***************************************************************************/ 1424 1425 // Test to ensure that an additional parse of the same configuration does 1426 // not allocate at all. 1427 1428 testNoAlloc(Config.parseString(str2)); 1429 1430 // Test to ensure that a few hundred additional parses of the same 1431 // configuration does not allocate at all. 1432 testNoAlloc({ 1433 static immutable num_parses = 200; 1434 for (int i; i < num_parses; i++) 1435 { 1436 Config.parseString(str2); 1437 } 1438 }()); 1439 1440 Config.clearParsingContext(); 1441 }