1 /** 2 * Reading ini-like files without usage of $(D inilike.file.IniLikeFile) class. 3 * Authors: 4 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 5 * Copyright: 6 * Roman Chistokhodov, 2017 7 * License: 8 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 9 * See_Also: 10 * $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification) 11 */ 12 13 module inilike.read; 14 public import inilike.range; 15 public import inilike.exception; 16 import inilike.common; 17 18 /// What to do when encounter some group name in onGroup callback of $(D readIniLike). 19 enum ActionOnGroup { 20 skip, /// Skip this group entries, don't do any processing. 21 proceed, /// Process the group entries as usual. 22 stopAfter, /// Stop after processing this group (don't parse next groups) 23 } 24 25 /** 26 * Read ini-like file entries via the set of callbacks. Callbacks can be null, but basic format validation is still run in this case. 27 * Params: 28 * reader = $(D inilike.range.IniLikeReader) object as returned by $(D inilike.range.iniLikeRangeReader) or similar function. 29 * onLeadingComment = Delegate to call after leading comment (i.e. the one before any group) is read. The parameter is either comment of empty line. 30 * onGroup = Delegate to call after group header is read. The parameter is group name (without brackets). Must return $(D ActionOnGroup). Providing the null callback is equal to providing the callback that always returns $(D ActionOnGroup.skip). 31 * onKeyValue = Delegate to call after key-value entry is read and parsed. Parameters are key, value and the current group name. It's recommended to throw $(D inilike.exceptions.IniLikeEntryException) from this function in case if the key-value pair is invalid. 32 * onCommentInGroup = Delegate to call after comment or empty line is read inside group section. The first parameter is either comment or empty line. The second parameter is the current group name. 33 * fileName = Optional file name parameter to use in thrown exceptions. 34 * Throws: 35 * $(D inilike.exception.IniLikeReadException) if error occured while parsing. Any exception thrown by callbacks will be transformed to $(D inilike.exception.IniLikeReadException). 36 */ 37 void readIniLike(IniLikeReader)(IniLikeReader reader, scope void delegate(string) onLeadingComment = null, scope ActionOnGroup delegate(string) onGroup = null, 38 scope void delegate(string, string, string) onKeyValue = null, scope void delegate(string, string) onCommentInGroup = null, string fileName = null 39 ) { 40 size_t lineNumber = 0; 41 42 version(DigitalMars) { 43 static void foo(size_t ) {} 44 } 45 46 try { 47 foreach(line; reader.byLeadingLines) 48 { 49 lineNumber++; 50 if (line.isComment || line.strip.empty) { 51 if (onLeadingComment !is null) 52 onLeadingComment(line); 53 } else { 54 throw new IniLikeException("Expected comment or empty line before any group"); 55 } 56 } 57 58 foreach(g; reader.byGroup) 59 { 60 lineNumber++; 61 string groupName = g.groupName; 62 63 version(DigitalMars) { 64 foo(lineNumber); //fix dmd codgen bug with -O 65 } 66 67 auto actionOnGroup = onGroup is null ? ActionOnGroup.skip : onGroup(groupName); 68 final switch(actionOnGroup) 69 { 70 case ActionOnGroup.stopAfter: 71 case ActionOnGroup.proceed: 72 { 73 foreach(line; g.byEntry) 74 { 75 lineNumber++; 76 77 if (line.isComment || line.strip.empty) { 78 if (onCommentInGroup !is null) 79 onCommentInGroup(line, groupName); 80 } else { 81 const t = parseKeyValue(line); 82 83 string key = t.key.stripRight; 84 string value = t.value.stripLeft; 85 86 if (key.length == 0 && value.length == 0) { 87 throw new IniLikeException("Expected comment, empty line or key value inside group"); 88 } else { 89 if (onKeyValue !is null) 90 onKeyValue(key, value, groupName); 91 } 92 } 93 } 94 if (actionOnGroup == ActionOnGroup.stopAfter) { 95 return; 96 } 97 } 98 break; 99 case ActionOnGroup.skip: 100 { 101 foreach(line; g.byEntry) {} 102 } 103 break; 104 } 105 } 106 } 107 catch(IniLikeEntryException e) { 108 throw new IniLikeReadException(e.msg, lineNumber, fileName, e, e.file, e.line, e.next); 109 } 110 catch (Exception e) { 111 throw new IniLikeReadException(e.msg, lineNumber, fileName, null, e.file, e.line, e.next); 112 } 113 } 114 115 /// 116 unittest 117 { 118 string contents = 119 `# Comment 120 [ToSkip] 121 KeyInSkippedGroup=Value 122 [ToProceed] 123 KeyInNormalGroup=Value2 124 # Comment2 125 [ToStopAfter] 126 KeyInStopAfterGroup=Value3 127 # Comment3 128 [NeverGetThere] 129 KeyNeverGetThere=Value4 130 # Comment4`; 131 auto onLeadingComment = delegate void(string line) { 132 assert(line == "# Comment"); 133 }; 134 auto onGroup = delegate ActionOnGroup(string groupName) { 135 if (groupName == "ToSkip") { 136 return ActionOnGroup.skip; 137 } else if (groupName == "ToStopAfter") { 138 return ActionOnGroup.stopAfter; 139 } else { 140 return ActionOnGroup.proceed; 141 } 142 }; 143 auto onKeyValue = delegate void(string key, string value, string groupName) { 144 assert((groupName == "ToProceed" && key == "KeyInNormalGroup" && value == "Value2") || 145 (groupName == "ToStopAfter" && key == "KeyInStopAfterGroup" && value == "Value3")); 146 }; 147 auto onCommentInGroup = delegate void(string line, string groupName) { 148 assert((groupName == "ToProceed" && line == "# Comment2") || (groupName == "ToStopAfter" && line == "# Comment3")); 149 }; 150 readIniLike(iniLikeStringReader(contents), onLeadingComment, onGroup, onKeyValue, onCommentInGroup); 151 readIniLike(iniLikeStringReader(contents)); 152 153 import std.exception : assertThrown; 154 contents = 155 `Not a comment 156 [Group name] 157 Key=Value`; 158 assertThrown!IniLikeReadException(readIniLike(iniLikeStringReader(contents))); 159 }