1 /** 2 * Parsing contents of ini-like files via range-based interface. 3 * Authors: 4 * $(LINK2 https://github.com/MyLittleRobo, 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 && is(ElementType!Range : const(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 byFirstLines() 35 { 36 return _range.until!(isGroupHeader); 37 } 38 39 /** 40 * Iterate thorugh groups of ini-like file. 41 */ 42 auto byGroup() 43 { 44 static struct ByGroup 45 { 46 static struct Group 47 { 48 this(Range range, ElementType!Range originalLine) 49 { 50 _range = range; 51 _originalLine = originalLine; 52 } 53 54 auto name() { 55 return parseGroupHeader(_originalLine); 56 } 57 58 auto originalLine() { 59 return _originalLine; 60 } 61 62 auto byEntry() 63 { 64 return _range.until!(isGroupHeader); 65 } 66 67 private: 68 ElementType!Range _originalLine; 69 Range _range; 70 } 71 72 this(Range range) 73 { 74 _range = range.find!(isGroupHeader); 75 ElementType!Range line; 76 if (!_range.empty) { 77 line = _range.front; 78 _range.popFront(); 79 } 80 _currentGroup = Group(_range, line); 81 } 82 83 auto front() 84 { 85 return _currentGroup; 86 } 87 88 bool empty() 89 { 90 return _currentGroup.name.empty; 91 } 92 93 void popFront() 94 { 95 _range = _range.find!(isGroupHeader); 96 ElementType!Range line; 97 if (!_range.empty) { 98 line = _range.front; 99 _range.popFront(); 100 } 101 _currentGroup = Group(_range, line); 102 } 103 private: 104 Group _currentGroup; 105 Range _range; 106 } 107 108 return ByGroup(_range.find!(isGroupHeader)); 109 } 110 111 private: 112 Range _range; 113 } 114 115 /** 116 * Convenient function for creation of IniLikeReader instance. 117 * Params: 118 * range = input range of strings (strings must be without trailing new line characters) 119 * Returns: IniLikeReader for given range. 120 * See_Also: iniLikeFileReader, iniLikeStringReader 121 */ 122 auto iniLikeRangeReader(Range)(Range range) 123 { 124 return IniLikeReader!Range(range); 125 } 126 127 /// 128 unittest 129 { 130 string contents = 131 `First comment 132 Second comment 133 [First group] 134 KeyValue1 135 KeyValue2 136 [Second group] 137 KeyValue3 138 KeyValue4 139 [Empty group] 140 [Third group] 141 KeyValue5 142 KeyValue6`; 143 auto r = iniLikeRangeReader(contents.splitLines()); 144 145 auto byFirstLines = r.byFirstLines; 146 147 assert(byFirstLines.front == "First comment"); 148 assert(byFirstLines.equal(["First comment", "Second comment"])); 149 150 auto byGroup = r.byGroup; 151 152 assert(byGroup.front.name == "First group"); 153 assert(byGroup.front.originalLine == "[First group]"); 154 //assert(byGroup.map!(g => g.name).equal(["First group", "Second group", "Empty group", "Third group"])); 155 156 157 assert(byGroup.front.byEntry.front == "KeyValue1"); 158 assert(byGroup.front.byEntry.equal(["KeyValue1", "KeyValue2"])); 159 byGroup.popFront(); 160 assert(byGroup.front.name == "Second group"); 161 byGroup.popFront(); 162 assert(byGroup.front.name == "Empty group"); 163 assert(byGroup.front.byEntry.empty); 164 byGroup.popFront(); 165 assert(byGroup.front.name == "Third group"); 166 byGroup.popFront(); 167 assert(byGroup.empty); 168 } 169 170 /** 171 * Convenient function for reading ini-like contents from the file. 172 * Throws: $(B ErrnoException) if file could not be opened. 173 * Note: This function uses byLineCopy internally. Fallbacks to byLine on older compilers. 174 * See_Also: iniLikeRangeReader, iniLikeStringReader 175 */ 176 @trusted auto iniLikeFileReader(string fileName) 177 { 178 import std.stdio : File; 179 static if( __VERSION__ < 2067 ) { 180 return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup)); 181 } else { 182 return iniLikeRangeReader(File(fileName, "r").byLineCopy()); 183 } 184 } 185 186 /** 187 * Convenient function for reading ini-like contents from string. 188 * Note: on frontends < 2.067 it uses splitLines thereby allocates strings. 189 * See_Also: iniLikeRangeReader, iniLikeFileReader 190 */ 191 @trusted auto iniLikeStringReader(String)(String contents) if (is(String : const(char)[])) 192 { 193 static if( __VERSION__ < 2067 ) { 194 return iniLikeRangeReader(contents.splitLines()); 195 } else { 196 return iniLikeRangeReader(contents.lineSplitter()); 197 } 198 }