359a2ba6f8ac068125e10370c859656c5c65819e
[nit.git] / lib / ini / ini.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Handle ini config files.
16 module ini
17
18 # A configuration tree that can read and store data in ini format
19 #
20 # Write example:
21 #
22 # var config = new ConfigTree("config.ini")
23 # config["goo"] = "goo"
24 # config["foo.bar"] = "foobar"
25 # config["foo.baz"] = "foobaz"
26 # config.save
27 # assert config.to_map.length == 3
28 #
29 # Read example:
30 #
31 # config = new ConfigTree("config.ini")
32 # assert config.has_key("foo.bar")
33 # assert config["foo.bar"] == "foobar"
34 class ConfigTree
35 super Writable
36
37 # The ini file used to read/store data
38 var ini_file: String
39
40 init do if ini_file.file_exists then load
41
42 # Get the config value for `key`
43 #
44 # var config = new ConfigTree("config.ini")
45 # assert config["goo"] == "goo"
46 # assert config["foo.bar"] == "foobar"
47 # assert config["foo.baz"] == "foobaz"
48 # assert config["fail.fail"] == null
49 fun [](key: String): nullable String do
50 var node = get_node(key)
51 if node == null then return null
52 return node.value
53 end
54
55 # Get the config values under `key`
56 #
57 # var config = new ConfigTree("config.ini")
58 # var values = config.at("foo")
59 # assert values.has_key("bar")
60 # assert values.has_key("baz")
61 # assert not values.has_key("goo")
62 #
63 # Return null if the key does not exists.
64 #
65 # assert config.at("fail.fail") == null
66 fun at(key: String): nullable Map[String, String] do
67 var node = get_node(key)
68 if node == null then return null
69 var map = new HashMap[String, String]
70 for k, child in node.children do
71 var value = child.value
72 if value == null then continue
73 map[k] = value
74 end
75 return map
76 end
77
78 # Set `value` at `key`
79 #
80 # var config = new ConfigTree("config.ini")
81 # assert config["foo.bar"] == "foobar"
82 # config["foo.bar"] = "baz"
83 # assert config["foo.bar"] == "baz"
84 fun []=(key: String, value: nullable String) do
85 set_node(key, value)
86 end
87
88 # Is `key` in the config?
89 #
90 # var config = new ConfigTree("config.ini")
91 # assert config.has_key("goo")
92 # assert config.has_key("foo.bar")
93 # assert not config.has_key("zoo")
94 fun has_key(key: String): Bool do
95 var parts = key.split(".").reversed
96 var node = get_root(parts.pop)
97 if node == null then return false
98 while not parts.is_empty do
99 node = node.get_child(parts.pop)
100 if node == null then return false
101 end
102 return true
103 end
104
105 # Get `self` as a Map of `key`, `value`
106 #
107 # var config = new ConfigTree("config.ini")
108 # var map = config.to_map
109 # assert map.has_key("goo")
110 # assert map.has_key("foo.bar")
111 # assert map.has_key("foo.baz")
112 # assert map.length == 3
113 fun to_map: Map[String, String] do
114 var map = new HashMap[String, String]
115 for node in leaves do
116 var value = node.value
117 if value == null then continue
118 map[node.key] = value
119 end
120 return map
121 end
122
123 redef fun to_s do return to_map.join(", ", ":")
124
125 redef fun write_to(stream) do
126 for node in leaves do
127 if node.value == null then continue
128 stream.write("{node.key}={node.value.to_s}\n")
129 end
130 end
131
132 # Reload config from file
133 # Done automatically at init
134 #
135 # Example with hierarchical ini file:
136 #
137 # # init file
138 # var str = """
139 # foo.bar=foobar
140 # foo.baz=foobaz
141 # goo=goo"""
142 # str.write_to_file("config1.ini")
143 # # load file
144 # var config = new ConfigTree("config1.ini")
145 # assert config["foo.bar"] == "foobar"
146 #
147 # Example with sections:
148 #
149 # # init file
150 # str = """
151 # goo=goo
152 # [foo]
153 # bar=foobar
154 # baz=foobaz
155 # [boo]
156 # bar=boobar"""
157 # str.write_to_file("config2.ini")
158 # # load file
159 # config = new ConfigTree("config2.ini")
160 # assert config["foo.bar"] == "foobar"
161 # assert config["boo.bar"] == "boobar"
162 #
163 # Example with both hierarchy and section:
164 #
165 # # init file
166 # str = """
167 # goo=goo
168 # [foo]
169 # bar.baz=foobarbaz
170 # [goo.boo]
171 # bar=gooboobar
172 # baz.bar=gooboobazbar"""
173 # str.write_to_file("config3.ini")
174 # # load file
175 # config = new ConfigTree("config3.ini")
176 # assert config["goo"] == "goo"
177 # assert config["foo.bar.baz"] == "foobarbaz"
178 # assert config["goo.boo.bar"] == "gooboobar"
179 # assert config["goo.boo.baz.bar"] == "gooboobazbar"
180 #
181 # Using the array notation
182 #
183 # str = """
184 # foo[]=a
185 # foo[]=b
186 # foo[]=c"""
187 # str.write_to_file("config4.ini")
188 # # load file
189 # config = new ConfigTree("config4.ini")
190 # print config.to_map.join(":", ",")
191 # assert config["foo.0"] == "a"
192 # assert config["foo.1"] == "b"
193 # assert config["foo.2"] == "c"
194 # assert config.at("foo").values.join(",") == "a,b,c"
195 fun load do
196 roots.clear
197 var stream = new FileReader.open(ini_file)
198 var path: nullable String = null
199 var line_number = 0
200 while not stream.eof do
201 var line = stream.read_line
202 line_number += 1
203 if line.is_empty then
204 continue
205 else if line.has_prefix(";") then
206 continue
207 else if line.has_prefix("[") then
208 line = line.trim
209 var key = line.substring(1, line.length - 2)
210 path = key
211 set_node(path, null)
212 else
213 var parts = line.split_once_on("=")
214 if parts.length == 1 then
215 continue
216 end
217 var key = parts[0].trim
218 var val = parts[1].trim
219 if path != null then key = "{path}.{key}"
220 if key.has_suffix("[]") then
221 set_array(key, val)
222 else
223 set_node(key,val)
224 end
225 end
226 end
227 stream.close
228 end
229
230 # Save config to file
231 fun save do write_to_file(ini_file)
232
233 private var roots = new Array[ConfigNode]
234
235 # Append `value` to array at `key`
236 private fun set_array(key: String, value: nullable String) do
237 key = key.substring(0, key.length - 2)
238 var len = 0
239 var node = get_node(key)
240 if node != null then len = node.children.length
241 set_node("{key}.{len.to_s}", value)
242 end
243
244 private fun set_node(key: String, value: nullable String) do
245 var parts = key.split(".").reversed
246 var k = parts.pop
247 var root = get_root(k)
248 if root == null then
249 root = new ConfigNode(k)
250 if parts.is_empty then
251 root.value = value
252 end
253 roots.add root
254 end
255 while not parts.is_empty do
256 k = parts.pop
257 var node = root.get_child(k)
258 if node == null then
259 node = new ConfigNode(k)
260 node.parent = root
261 root.children[node.name] = node
262 end
263 if parts.is_empty then
264 node.value = value
265 end
266 root = node
267 end
268 end
269
270 private fun get_node(key: String): nullable ConfigNode do
271 var parts = key.split(".").reversed
272 var node = get_root(parts.pop)
273 while node != null and not parts.is_empty do
274 node = node.get_child(parts.pop)
275 end
276 return node
277 end
278
279 private fun get_root(name: String): nullable ConfigNode do
280 for root in roots do
281 if root.name == name then return root
282 end
283 return null
284 end
285
286 private fun leaves: Array[ConfigNode] do
287 var res = new Array[ConfigNode]
288 var todo = new Array[ConfigNode]
289 todo.add_all roots
290 while not todo.is_empty do
291 var node = todo.pop
292 if node.children.is_empty then
293 res.add node
294 else
295 todo.add_all node.children.values
296 end
297 end
298 return res
299 end
300 end
301
302 private class ConfigNode
303
304 var parent: nullable ConfigNode = null
305 var children = new HashMap[String, ConfigNode]
306 var name: String is writable
307 var value: nullable String = null
308
309 fun key: String do
310 var parent = self.parent
311 if parent == null then
312 return name
313 end
314 return "{parent.key}.{name}"
315 end
316
317 fun get_child(name: String): nullable ConfigNode do
318 if children.has_key(name) then
319 return children[name]
320 end
321 return null
322 end
323 end