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