1 /** 2 * Reading and writing ini-like files used in some Unix systems and Freedesktop specifications. 3 * ini-like is informal name for the file format that look like this: 4 * --- 5 # Comment 6 [Group name] 7 Key=Value 8 # Comment inside group 9 AnotherKey=Value 10 11 [Another group] 12 Key=English value 13 Key[fr_FR]=Francais value 14 15 * --- 16 * Authors: 17 * $(LINK2 https://github.com/MyLittleRobo, Roman Chistokhodov) 18 * Copyright: 19 * Roman Chistokhodov, 2015-2016 20 * License: 21 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 22 * See_Also: 23 * $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification) 24 */ 25 26 module inilike; 27 28 public import inilike.common; 29 public import inilike.range; 30 public import inilike.file; 31 32 unittest 33 { 34 import std.exception; 35 36 final class DesktopEntry : IniLikeGroup 37 { 38 this() { 39 super("Desktop Entry"); 40 } 41 protected: 42 @trusted override void validateKey(string key, string value) const { 43 if (!isValidKey(key)) { 44 throw new IniLikeEntryException("key is invalid", groupName(), key, value); 45 } 46 } 47 } 48 49 final class DesktopFile : IniLikeFile 50 { 51 //Flags to manage .ini like file reading 52 enum ReadOptions 53 { 54 noOptions = 0, // Read all groups, skip comments and empty lines, stop on any error. 55 preserveComments = 2, // Preserve comments and empty lines. Use this when you want to keep them across writing. 56 ignoreGroupDuplicates = 4, // Ignore group duplicates. The first found will be used. 57 ignoreInvalidKeys = 8, // Skip invalid keys during parsing. 58 ignoreKeyDuplicates = 16, // Ignore key duplicates. The first found will be used. 59 ignoreUnknownGroups = 32, // Don't throw on unknown groups. Still save them. 60 skipUnknownGroups = 64, // Don't save unknown groups. 61 skipExtensionGroups = 128 // Skip groups started with X- 62 } 63 64 @trusted this(IniLikeReader)(IniLikeReader reader, ReadOptions options = ReadOptions.noOptions) 65 { 66 _options = options; 67 super(reader); 68 enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0)); 69 _options = ReadOptions.noOptions; 70 } 71 72 @safe override bool removeGroup(string groupName) nothrow { 73 if (groupName == "Desktop Entry") { 74 return false; 75 } 76 return super.removeGroup(groupName); 77 } 78 79 @trusted override string appendLeadingComment(string line) nothrow { 80 if (_options & ReadOptions.preserveComments) { 81 return super.appendLeadingComment(line); 82 } 83 return null; 84 } 85 86 protected: 87 @trusted override void addCommentForGroup(string comment, IniLikeGroup currentGroup, string groupName) 88 { 89 if (currentGroup && (_options & ReadOptions.preserveComments)) { 90 currentGroup.appendComment(comment); 91 } 92 } 93 94 @trusted override void addKeyValueForGroup(string key, string value, IniLikeGroup currentGroup, string groupName) 95 { 96 if (currentGroup) { 97 if (!isValidKey(key) && (_options & ReadOptions.ignoreInvalidKeys)) { 98 return; 99 } 100 if (currentGroup.contains(key)) { 101 if (_options & ReadOptions.ignoreKeyDuplicates) { 102 return; 103 } else { 104 throw new Exception("key already exists"); 105 } 106 } 107 currentGroup[key] = value; 108 } 109 } 110 111 @trusted override IniLikeGroup createGroup(string groupName) 112 { 113 if (group(groupName) !is null) { 114 if (_options & ReadOptions.ignoreGroupDuplicates) { 115 return null; 116 } else { 117 throw new Exception("group already exists"); 118 } 119 } 120 121 if (groupName == "Desktop Entry") { 122 _desktopEntry = new DesktopEntry(); 123 return _desktopEntry; 124 } else if (groupName.startsWith("X-")) { 125 if (_options & ReadOptions.skipExtensionGroups) { 126 return null; 127 } 128 return createEmptyGroup(groupName); 129 } else { 130 if (_options & ReadOptions.ignoreUnknownGroups) { 131 if (_options & ReadOptions.skipUnknownGroups) { 132 return null; 133 } else { 134 return createEmptyGroup(groupName); 135 } 136 } else { 137 throw new Exception("Unknown group"); 138 } 139 } 140 } 141 142 inout(DesktopEntry) desktopEntry() inout { 143 return _desktopEntry; 144 } 145 146 private: 147 DesktopEntry _desktopEntry; 148 ReadOptions _options; 149 } 150 151 string contents = 152 `# First comment 153 [Desktop Entry] 154 Key=Value 155 # Comment in group`; 156 157 auto df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions); 158 assert(!df.removeGroup("Desktop Entry")); 159 assert(!df.removeGroup("NonExistent")); 160 assert(df.group("Desktop Entry") !is null); 161 assert(df.desktopEntry() !is null); 162 assert(df.leadingComments().empty); 163 assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value")])); 164 165 df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.preserveComments); 166 assert(equal(df.leadingComments(), ["# First comment"])); 167 assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value"), IniLikeLine.fromComment("# Comment in group")])); 168 169 contents = 170 `[X-SomeGroup] 171 Key=Value`; 172 173 auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); 174 assert(thrown !is null); 175 assert(thrown.lineNumber == 0); 176 177 contents = 178 `[Desktop Entry] 179 Valid=Key 180 $=Invalid`; 181 182 thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); 183 assert(thrown !is null); 184 assert(thrown.entryException !is null); 185 assert(thrown.entryException.key == "$"); 186 assert(thrown.entryException.value == "Invalid"); 187 188 assertNotThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreInvalidKeys)); 189 190 contents = 191 `[Desktop Entry] 192 Key=Value1 193 Key=Value2`; 194 195 assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); 196 assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreKeyDuplicates)); 197 assert(df.desktopEntry().value("Key") == "Value1"); 198 199 contents = 200 `[Desktop Entry] 201 Name=Name 202 [Unknown] 203 Key=Value`; 204 205 assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); 206 assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreUnknownGroups)); 207 assert(df.group("Unknown") !is null); 208 209 df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreUnknownGroups|DesktopFile.ReadOptions.skipUnknownGroups); 210 assert(df.group("Unknown") is null); 211 212 contents = 213 `[Desktop Entry] 214 Name=Name1 215 [Desktop Entry] 216 Name=Name2`; 217 218 assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); 219 assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.ignoreGroupDuplicates)); 220 221 assert(df.desktopEntry().value("Name") == "Name1"); 222 223 contents = 224 `[Desktop Entry] 225 Name=Name1 226 [X-Extension] 227 Name=Name2`; 228 229 df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.skipExtensionGroups); 230 assert(df.group("X-Extension") is null); 231 }