1 /** 2 * Parsing contents of ini-like files via range-based interface. 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.range; 14 15 import inilike.common; 16 17 18 /** 19 * Object for iterating through ini-like file entries. 20 */ 21 struct IniLikeReader(Range) if (isInputRange!Range && isSomeString!(ElementType!Range) && is(ElementEncodingType!(ElementType!Range) : char)) 22 { 23 /** 24 * Construct from other range of strings. 25 */ 26 this(Range range) 27 { 28 _range = range; 29 } 30 31 /** 32 * Iterate through lines before any group header. It does not check if all lines are comments or empty lines. 33 */ 34 auto byLeadingLines() 35 { 36 return _range.until!(isGroupHeader); 37 } 38 39 /** 40 * Object representing single group (section) being parsed in .ini-like file. 41 */ 42 static struct Group(Range) 43 { 44 private this(Range range, ElementType!Range originalLine) 45 { 46 _range = range; 47 _originalLine = originalLine; 48 } 49 50 /** 51 * Name of group being parsed (without brackets). 52 * Note: This can become invalid during parsing the Input Range 53 * (e.g. if string buffer storing this value is reused in later reads). 54 */ 55 auto groupName() { 56 return parseGroupHeader(_originalLine); 57 } 58 59 /** 60 * Original line of group header (i.e. name with brackets). 61 * Note: This can become invalid during parsing the Input Range 62 * (e.g. if string buffer storing this value is reused in later reads). 63 */ 64 auto originalLine() { 65 return _originalLine; 66 } 67 68 /** 69 * Iterate over group entries - may be key-value pairs as well as comments or empty lines. 70 */ 71 auto byEntry() 72 { 73 return _range.until!(isGroupHeader); 74 } 75 76 private: 77 ElementType!Range _originalLine; 78 Range _range; 79 } 80 81 /** 82 * Iterate thorugh groups of .ini-like file. 83 * Returns: Range of Group objects. 84 */ 85 auto byGroup() 86 { 87 static struct ByGroup 88 { 89 this(Range range) 90 { 91 _range = range.find!(isGroupHeader); 92 ElementType!Range line; 93 if (!_range.empty) { 94 line = _range.front; 95 _range.popFront(); 96 } 97 _currentGroup = Group!Range(_range, line); 98 } 99 100 auto front() 101 { 102 return _currentGroup; 103 } 104 105 bool empty() 106 { 107 return _currentGroup.groupName.empty; 108 } 109 110 void popFront() 111 { 112 _range = _range.find!(isGroupHeader); 113 ElementType!Range line; 114 if (!_range.empty) { 115 line = _range.front; 116 _range.popFront(); 117 } 118 _currentGroup = Group!Range(_range, line); 119 } 120 private: 121 Group!Range _currentGroup; 122 Range _range; 123 } 124 125 return ByGroup(_range.find!(isGroupHeader)); 126 } 127 private: 128 Range _range; 129 } 130 131 /** 132 * Convenient function for creation of IniLikeReader instance. 133 * Params: 134 * range = input range of strings (strings must be without trailing new line characters) 135 * Returns: IniLikeReader for given range. 136 * See_Also: $(D iniLikeFileReader), $(D iniLikeStringReader) 137 */ 138 auto iniLikeRangeReader(Range)(Range range) 139 { 140 return IniLikeReader!Range(range); 141 } 142 143 /// 144 unittest 145 { 146 string contents = 147 `First comment 148 Second comment 149 [First group] 150 KeyValue1 151 KeyValue2 152 [Second group] 153 KeyValue3 154 KeyValue4 155 [Empty group] 156 [Third group] 157 KeyValue5 158 KeyValue6`; 159 auto r = iniLikeRangeReader(contents.splitLines()); 160 161 auto byLeadingLines = r.byLeadingLines; 162 163 assert(byLeadingLines.front == "First comment"); 164 assert(byLeadingLines.equal(["First comment", "Second comment"])); 165 166 auto byGroup = r.byGroup; 167 168 assert(byGroup.front.groupName == "First group"); 169 assert(byGroup.front.originalLine == "[First group]"); 170 171 172 assert(byGroup.front.byEntry.front == "KeyValue1"); 173 assert(byGroup.front.byEntry.equal(["KeyValue1", "KeyValue2"])); 174 byGroup.popFront(); 175 assert(byGroup.front.groupName == "Second group"); 176 byGroup.popFront(); 177 assert(byGroup.front.groupName == "Empty group"); 178 assert(byGroup.front.byEntry.empty); 179 byGroup.popFront(); 180 assert(byGroup.front.groupName == "Third group"); 181 byGroup.popFront(); 182 assert(byGroup.empty); 183 } 184 185 /** 186 * Convenient function for reading ini-like contents from the file. 187 * Throws: $(B ErrnoException) if file could not be opened. 188 * Note: This function uses byLineCopy internally. Fallbacks to byLine on older compilers. 189 * See_Also: $(D iniLikeRangeReader), $(D iniLikeStringReader) 190 */ 191 @trusted auto iniLikeFileReader(string fileName) 192 { 193 import std.stdio : File; 194 static if( __VERSION__ < 2067 ) { 195 return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup)); 196 } else { 197 return iniLikeRangeReader(File(fileName, "r").byLineCopy()); 198 } 199 } 200 201 /** 202 * Convenient function for reading ini-like contents from string. 203 * Note: on frontends < 2.067 it uses splitLines thereby allocates strings. 204 * See_Also: $(D iniLikeRangeReader), $(D iniLikeFileReader) 205 */ 206 @trusted auto iniLikeStringReader(String)(String contents) if (isSomeString!String && is(ElementEncodingType!String : char)) 207 { 208 static if( __VERSION__ < 2067 ) { 209 return iniLikeRangeReader(contents.splitLines()); 210 } else { 211 return iniLikeRangeReader(contents.lineSplitter()); 212 } 213 }