1 /*
2 *	baussini.d provides a thread-safe by choice inifile wrapper.
3 *	It supports template writing/reading for values.
4 *	Please view example.d for an example.
5 */
6 module baussini;
7 
8 // Imports ...
9 import std.conv : to;
10 import std..string : format;
11 import std.array : replace, split, join;
12 import std.algorithm : canFind, startsWith, endsWith, stripLeft, stripRight;
13 import std.traits : isIntegral, isFloatingPoint;
14 
15 import std.file;
16 // Aliasing write, read and exists from std.file to avoid name conflicts.
17 alias fwrite = std.file.write;
18 alias fread = std.file.readText;
19 alias fexists = std.file.exists;
20 
21 /**
22 *	An inifile exception.
23 */
24 public class IniException : Throwable {
25 	/**
26 	*	Creates a new instance of IniException.
27 	*	Params:
28 	*		msg =	The message of the exception.
29 	*/
30 	this(string msg) {
31 		super(msg);
32 	}
33 }
34 
35 /**
36 *	std.traits doesn't have isArithmetic.
37 */
38 private deprecated("isArithmetic is deprecated. write/read supports all types std.conv.to supports.") enum bool isArithmetic(T) =
39 	isIntegral!T || isFloatingPoint!T;
40 
41 /**
42 *	An inifile section wrapper.
43 */
44 class IniSection(bool sync) {
45 private:
46 	/**
47 	*	The parent inifile.
48 	*/
49 	IniFile!(sync) m_parent;
50 	/**
51 	*	The section name.
52 	*/
53 	string m_name;
54 	/**
55 	*	The section entries.
56 	*/
57 	string[string] m_entries;
58 	
59 	/**
60 	*	Gets the file-writable string
61 	*	Returns: A string compatible with an inifile.
62 	*/
63 	string getWritable() {
64 		string[] s = [format("[%s]", m_name)];
65 		foreach (key, value; m_entries) {
66 			s ~= format("%s=%s", key, value);
67 		}
68 		return join(s, "\r\n");
69 	}
70 	
71 	/**
72 	*	Gets a string value.
73 	*	Params:
74 	*		key =			The key to get.
75 	*		defaultValue =	(lazy) The default value.
76 	*	Returns: The value of the key if found, defaultValue otherwise.
77 	*/
78 	string getValue(string key, lazy string defaultValue) {
79 		static if (sync) {
80 			synchronized {
81 				auto val = m_entries.get(key, defaultValue);
82 				return val;
83 			}
84 		}
85 		else {
86 			auto val = m_entries.get(key, defaultValue);
87 			return val;
88 		}
89 	}
90 	
91 	/**
92 	*	Closes the section and clears all entries.
93 	*/
94 	void close() {
95 		m_entries = null;
96 	}
97 public:
98 	/**
99 	*	Creates a new instance of IniSection.
100 	*	Params:
101 	*		name =		The name of the section.
102 	*		parent =	The parent inifile.
103 	*/
104 	this(string name, IniFile!(sync) parent) {
105 		m_name = name;
106 		m_parent = parent;
107 	}
108 	
109 	/**
110 	*	Reads a value from the section.
111 	*	Params:
112 	*		key =			The key to read.
113 	*		defaultValue =	(lazy) The default value.
114 	*	Returns: The value if found, defaultValue otherwise.
115 	*/
116 	auto read(T)(string key, lazy string defaultValue) {
117 		static if (sync) {
118 			synchronized {
119 				auto val = getValue(key, defaultValue);
120 				return to!T(val);
121 			}
122 		}
123 		else {
124 			auto val = getValue(key, defaultValue);
125 			return to!T(val);
126 		}
127 	}
128 	
129 	/**
130 	*	Reads a value from the section.
131 	*	Params:
132 	*		key =			The key to read.
133 	*		defaultValue =	(lazy) The default value.
134 	*		value =			(out) The value if found, defaultValue otherwise.
135 	*	Returns: The section.
136 	*/	
137 	auto read(T)(string key, lazy string defaultValue, out T value) {
138 		static if (sync) {
139 			synchronized {
140 				auto val = getValue(key, defaultValue);
141 				value = to!T(val);
142 				return this;
143 			}
144 		}
145 		else {
146 			auto val = getValue(key, defaultValue);
147 			value = to!T(val);
148 			return this;
149 		}
150 	}
151 	
152 	/**
153 	*	Reads a value from the section.
154 	*	Params:
155 	*		key =	The key to read.
156 	*	Throws: IniException if the key wasn't found or if the value is empty.
157 	*	Returns: The value if found.
158 	*/	
159 	auto read(T)(string key) {
160 		static if (sync) {
161 			synchronized {
162 				if(!hasKey(key))
163 					throw new IniException(format("%s does not exist in the section.", key));
164 				return to!T(getValue(key, ""));
165 			}
166 		}
167 		else {
168 			if(!hasKey(key))
169 				throw new IniException(format("%s does not exist in the section.", key));
170 			return to!T(getValue(key, ""));
171 		}
172 	}
173 	
174 	/**
175 	*	Reads a value from the section.
176 	*	Params:
177 	*		key =	The key to read.
178 	*		value =	The value if found.
179 	*	Throws: IniException if the key wasn't found or if the value is empty.
180 	*	Returns: The section.
181 	*/
182 	auto read(T)(string key, out T value) {
183 		static if (sync) {
184 			synchronized {
185 				if(!hasKey(key))
186 					throw new IniException(format("%s does not exist in the section.", key));
187 				value = to!T(getValue(key, ""));
188 				return this;
189 			}
190 		}
191 		else {
192 			if(!hasKey(key))
193 				throw new IniException(format("%s does not exist in the section.", key));
194 			value = to!T(getValue(key, ""));
195 			return this;
196 		}
197 	}
198 	
199 	/**
200 	*	Writes an entry to the section.
201 	*	Params:
202 	*		key =	The key of the entry.
203 	*		value =	The value of the entry.
204 	*	Returns: The section.
205 	*/
206 	auto write(T)(string key, T value) {
207 		static if (sync) {
208 			synchronized {
209 				m_entries[key] = to!string(value);
210 				m_parent.m_changed = true;
211 				return this;
212 			}
213 		}
214 		else {
215 			m_entries[key] = to!string(value);
216 			m_parent.m_changed = true;
217 			return this;
218 		}
219 	}
220 	
221 	/**
222 	*	Checks whether the section has a key or not.
223 	*	Params:
224 	*		key =	The key to check for.
225 	*	Returns: True if the key exists, false otherwise.
226 	*/
227 	bool hasKey(string key) {
228 		static if (sync) {
229 			synchronized {
230 				return (key in m_entries) !is null;
231 			}
232 		}
233 		else {
234 			return (key in m_entries) !is null;
235 		}
236 	}
237 	
238 	@property {
239 		/**
240 		*	Gets the name of the section.
241 		*/
242 		string name() {
243 			static if (sync) {
244 				synchronized {
245 					return m_name;
246 				}
247 			}
248 			else {
249 				return m_name;
250 			}
251 		}
252 		
253 		/**
254 		*	Gets the keys of the section.
255 		*/
256 		string[] keys() {
257 			static if (sync) {
258 				synchronized {
259 					return m_entries.keys;
260 				}
261 			}
262 			else {
263 				return m_entries.keys;
264 			}
265 		}
266 
267 		/**
268 		*	Gets the values of the section.
269 		*/
270 		string[] values() {
271 			static if (sync) {
272 				synchronized {
273 					return m_entries.values;
274 				}
275 			}
276 			else {
277 				return m_entries.values;
278 			}	
279 		}
280 		
281 		/**
282 		*	Gets the parental inifile.
283 		*/
284 		IniFile!(sync) parent() {
285 			static if (sync) {
286 				synchronized {
287 					return m_parent;
288 				}
289 			}
290 			else {
291 				return m_parent;
292 			}
293 		}
294 	}
295 }
296 
297 /**
298 *	An inifile wrapper.
299 */
300 class IniFile(bool sync) {
301 private:
302 	/**
303 	*	The sections of the inifile.
304 	*/
305 	IniSection!(sync)[string] m_sections;
306 	/**
307 	*	The file name of the inifile.
308 	*/
309 	string m_fileName;
310 	/**
311 	*	A boolean determining whether the inifile has got any changes.
312 	*/
313 	bool m_changed;
314 	
315 	/**
316 	*	Parses the inifile from text.
317 	*	Params:
318 	*		text =	The text to parse.
319 	*/
320 	void parseFromText(string text) {
321 		text = replace(text, "\r", "");
322 		scope auto lines = split(text, "\n");
323 		IniSection!(sync) currentSection;
324 		foreach (sline; lines) {
325 			auto line = stripLeft(sline, ' ');
326 			line = stripLeft(sline, '\t');
327 			
328 			if (startsWith(line, ";"))
329 					continue;
330 			
331 			if (line && line.length) {
332 				if (startsWith(line, "[") && endsWith(line, "]")) {
333 					currentSection = new IniSection!(sync)(line[1 .. $-1], this);
334 					m_sections[currentSection.name] = currentSection;
335 				}
336 				else if (canFind(line, "=") && currentSection) {
337 					auto data = split(line, "=");
338 					if (data.length == 2) {
339 						auto key = stripRight(data[0], ' ');
340 						key = stripLeft(key, ' ');
341 						auto value = split(data[1], ";")[0];
342 						value = stripRight(value, ' ');
343 						value = stripLeft(value, ' ');
344 						currentSection.write(key, value);
345 					}
346 				}
347 			}
348 		}
349 	}
350 	
351 	/**
352 	*	Parses the inifile to text.
353 	*	Returns: A string representing the text.
354 	*/
355 	string parseToText() {
356 		string s;
357 		foreach (section; m_sections.values) {
358 			s ~= section.getWritable() ~ "\r\n\r\n";
359 		}
360 		if (s && s.length >= 2) {
361 			s.length -= 1; // EOF has to be 1 one new line only.
362 			return s;
363 		}
364 		else
365 			return "";
366 	}
367 public:
368 	/**
369 	*	Creates a new instance of IniFile.
370 	*	Params:
371 	*		fileName =	The file name of the inifile.
372 	*/
373 	this(string fileName) {
374 		m_fileName = fileName;
375 		m_changed = false;
376 	}
377 	
378 	/**
379 	*	Checks whether the inifile exists or not.
380 	*	Returns: True if the file exists, false otherwise.
381 	*/
382 	bool exists() {
383 		static if (sync) {
384 			synchronized {
385 				return fexists(m_fileName);
386 			}
387 		}
388 		else {
389 			return fexists(m_fileName);
390 		}
391 	}
392 	
393 	/**
394 	*	Opens the inifile and parses its text.
395 	*/
396 	void open() {
397 		static if (sync) {
398 			synchronized {
399 				parseFromText(
400 					fread(m_fileName)
401 				);
402 			}
403 		}
404 		else {
405 			parseFromText(
406 				fread(m_fileName)
407 			);
408 		}
409 	}
410 	
411 	/**
412 	*	Closes the inifile and writes its text if any changes has occured.
413 	*/
414 	void close() {
415 		static if (sync) {
416 			synchronized {
417 				if (!m_changed)
418 					return;
419 				
420 				fwrite(m_fileName, parseToText());
421 				foreach (section; m_sections.values)
422 					section.close();
423 				m_sections = null;
424 			}
425 		}
426 		else {
427 			if (!m_changed)
428 				return;
429 				
430 			fwrite(m_fileName, parseToText());
431 			foreach (section; m_sections.values)
432 				section.close();
433 			m_sections = null;
434 		}
435 	}
436 	
437 	/**
438 	*	Checks whether the inifile has a specific section.
439 	*	Params:
440 	*		section =	The section to check for existence.
441 	*	Returns: True if the section exists, false otherwise.
442 	*/
443 	bool hasSection(string section) {
444 		static if (sync) {
445 			synchronized {
446 				return m_sections.get(section, null) !is null;
447 			}
448 		}
449 		else {
450 			return m_sections.get(section, null) !is null;
451 		}
452 	}
453 	
454 	/**
455 	*	Gets a specific section of the inifile.
456 	*	Params:
457 	*		section = The section to get.
458 	*	Returns: The section.
459 	*/
460 	auto getSection(string section) {
461 		static if (sync) {
462 			synchronized {
463 				if(!hasSection(section))
464 					throw new IniException(format("%s is not an existing section.", section));
465 				return m_sections[section];
466 			}
467 		}
468 		else {
469 			if(!hasSection(section))
470 				throw new IniException(format("%s is not an existing section.", section));
471 			return m_sections[section];
472 		}
473 	}
474 	
475 	/**
476 	*	Adds a new section to the inifile.
477 	*	Params:
478 	*		section = The section to add.
479 	*/
480 	void addSection(string section) {
481 		static if (sync) {
482 			synchronized {
483 				m_sections[section] = new IniSection!(sync)(section, this);
484 			}
485 		}
486 		else {
487 			m_sections[section] = new IniSection!(sync)(section, this);
488 		}
489 	}
490 	
491 	/**
492 	*	Reads an entry from the inifile.
493 	*	Params:
494 	*		section =	The section to read from.
495 	*		key =		The key of the entry to read.
496 	*	Returns: The value read.
497 	*/
498 	auto read(T)(string section, string key) {
499 		static if (sync) {
500 			synchronized {
501 				if(!hasSection(section))
502 					throw new IniException(format("%s is not an existing section.", section));
503 				return m_sections[section].read!T(key);
504 			}
505 		}
506 		else {
507 			if(!hasSection(section))
508 				throw new IniException(format("%s is not an existing section.", section));
509 			return m_sections[section].read!T(key);
510 		}
511 	}
512 	
513 	/**
514 	*	Writes an entry to the inifile.
515 	*	Params:
516 	*		section =	The section to write the entry to.
517 	*		key =		The key of the entry.
518 	*		value =		The value of the entry.
519 	*/
520 	void write(T)(string section, string key, T value) {
521 		static if (sync) {
522 			synchronized {
523 				if(!hasSection(section))
524 					throw new IniException(format("%s is not an existing section.", section));
525 				m_sections[section].write!T(key, value);
526 			}
527 		}
528 		else {
529 			if(!hasSection(section))
530 				throw new IniException(format("%s is not an existing section.", section));
531 			m_sections[section].write!T(key, value);
532 		}
533 	}
534 	
535 	/**
536 	*	Checks whether the inifile has a specific key.
537 	*	Params:
538 	*		section =	The section to check within.
539 	*		key =		The key to check for existence.
540 	*	Returns: True if the key exists, falses otherwise.
541 	*/
542 	bool hasKey(string section, string key) {
543 		static if (sync) {
544 			synchronized {
545 				if(!hasSection(section))
546 					throw new IniException(format("%s is not an existing section.", section));
547 				return m_sections[section].hasKey(key);
548 			}
549 		}
550 		else {
551 			if(!hasSection(section))
552 				throw new IniException(format("%s is not an existing section.", section));
553 			return m_sections[section].hasKey(key);
554 		}
555 	}
556 	
557 	@property {
558 		/**
559 		*	Gets the filename of the inifile.
560 		*/
561 		string fileName() {
562 			static if (sync) {
563 				synchronized {
564 					return m_fileName;
565 				}
566 			}
567 			else {
568 				return m_fileName;
569 			}
570 		}
571 		
572 		/**
573 		*	Gets all the section names.
574 		*/
575 		string[] sectionNames() {
576 			static if (sync) {
577 				synchronized {
578 					return m_sections.keys;
579 				}
580 			}
581 			else {
582 				return m_sections.keys;
583 			}
584 		}
585 		
586 		/**
587 		*	Gets all the sections.
588 		*/
589 		IniSection!(sync)[] sections() {
590 			static if (sync) {
591 				synchronized {
592 					return m_sections.values;
593 				}
594 			}
595 			else {
596 				return m_sections.values;
597 			}
598 		}
599 	}
600 }