1 module parsing.ini_parser;
2 
3 import std..string;
4 import std.algorithm;
5 import std.array;
6 
7 /// Parse a list of key value pairs from a file in the format (aka ini file):
8 /// ```ini
9 /// # comment
10 /// some_key = some_value
11 /// # also supports sections, sections cna be named anything
12 /// [some_section]
13 /// # sections namespace keys to access the follwing key access the key "some_section.some_key"
14 /// some_key = some value
15 /// ```
16 /// Comment, key value delimiter and space stripping can be configured with
17 /// constructor parameters this parser will not accept duplicate keys by default
18 /// to allow duplicate keys pass allowOverride to the constructor, it will enforce
19 /// the last instance of a key is the final value for that key.
20 class IniParser
21 {
22 	string[string] items;
23 	this(
24 		string inBuffer,
25 		bool paseSections = false,
26 		bool stripSpaces = true,
27 		bool allowOverride = false,
28 		string commentChar = "#",
29 		string keyValueSep = "="
30 	)
31 	{
32 		auto currentSection = "";
33 		// strip the inBuffer of any white space or spurious whitespace before processing
34 		inBuffer = strip(inBuffer);
35 		// Split with lineSplitter as a lazy range, and alloc only once
36 		// ignore any comment lines
37 		auto lines = inBuffer.lineSplitter()
38 			.map!(line => strip(line))
39 			.filter!(line => !line.startsWith(commentChar))
40 			.map!((line) {
41 				// taking care of inline comments
42 				if (auto split = line.findSplit(commentChar))
43 					return split[0];
44 				else
45 					return line;
46 			})
47 			.map!(line => strip(line))
48 			.array();
49 
50 		foreach (line; lines)
51 		{
52 			// We will parse sections for namespacing purposes
53 			if (line.startsWith("[") && paseSections)
54 			{
55 				if (auto split = line.findSplit("]"))
56 					currentSection = strip(split[0][1 .. $]) ~ ".";
57 				else
58 					throw new Exception(format("Malformed section line: %s", line));
59 				continue;
60 			}
61 
62 			if (auto split = line.findSplit(keyValueSep))
63 			{
64 				string key = currentSection ~ strip(split[0]);
65 				if (key in items && !allowOverride)
66 					throw new Exception(format("key %s already seen", line));
67 				// split any
68 				if (stripSpaces)
69 					items[key] = strip(split[2]);
70 				else
71 					items[key] = split[2];
72 			}
73 			else
74 				throw new Exception(format("Malformed line: %s", line));
75 		}
76 	}
77 
78 	string opIndex(string key)
79 	{
80 		return items[key];
81 	}
82 
83 	ulong lenght()
84 	{
85 		return items.length;
86 	}
87 
88 	unittest
89 	{
90 		import std.exception;
91 
92 		auto testStr = "
93 		# some comment
94 		mainDrive = /dev/sda # some inline comment
95 		secondaryDrive = /dev/sdb
96 		";
97 		auto testStrSections = "
98 		# some comment
99 		[scsi]
100 		mainDrive = /dev/sda # some inline comment
101 		[ sata ]
102 		mainDrive = /dev/sdb
103 		";
104 		auto testStrInvalid = "
105 		# some comment
106 		mainDrive = /dev/sda # some inline comment
107 		secondaryDrive = /dev/sdb
108 		/dev/sdc
109 		";
110 		IniParser cl = new IniParser(testStr);
111 		assert(cl["mainDrive"] == "/dev/sda", "inline comment not stripped");
112 		assert(cl["secondaryDrive"] == "/dev/sdb", "second line wasn't parsed correctly");
113 		assertThrown(new IniParser(testStrInvalid));
114 		IniParser cls = new IniParser(testStrSections, true);
115 		assert(cls["scsi.mainDrive"] == "/dev/sda", "inline comment not stripped");
116 		assert(cls["sata.mainDrive"] == "/dev/sdb", "second line wasn't parsed correctly");
117 	}
118 }