README: document nit_env.sh
[nit.git] / lib / 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 if child.value == null then continue
72 map[k] = child.value.to_s
73 end
74 return map
75 end
76
77 # Set `value` at `key`
78 #
79 # var config = new ConfigTree("config.ini")
80 # assert config["foo.bar"] == "foobar"
81 # config["foo.bar"] = "baz"
82 # assert config["foo.bar"] == "baz"
83 fun []=(key: String, value: nullable String) do
84 set_node(key, value)
85 end
86
87 # Is `key` in the config?
88 #
89 # var config = new ConfigTree("config.ini")
90 # assert config.has_key("goo")
91 # assert config.has_key("foo.bar")
92 # assert not config.has_key("zoo")
93 fun has_key(key: String): Bool do
94 var parts = key.split(".").reversed
95 var node = get_root(parts.pop)
96 if node == null then return false
97 while not parts.is_empty do
98 node = node.get_child(parts.pop)
99 if node == null then return false
100 end
101 return true
102 end
103
104 # Get `self` as a Map of `key`, `value`
105 #
106 # var config = new ConfigTree("config.ini")
107 # var map = config.to_map
108 # assert map.has_key("goo")
109 # assert map.has_key("foo.bar")
110 # assert map.has_key("foo.baz")
111 # assert map.length == 3
112 fun to_map: Map[String, String] do
113 var map = new HashMap[String, String]
114 for node in leaves do
115 if node.value == null then continue
116 map[node.key] = node.value.to_s
117 end
118 return map
119 end
120
121 redef fun to_s do return to_map.join(", ", ":")
122
123 redef fun write_to(stream) do
124 for node in leaves do
125 if node.value == null then continue
126 stream.write("{node.key}={node.value.to_s}\n")
127 end
128 end
129
130 # Reload config from file
131 # Done automatically at init
132 #
133 # Example with hierarchical ini file:
134 #
135 # # init file
136 # var str = """
137 # foo.bar=foobar
138 # foo.baz=foobaz
139 # goo=goo"""
140 # str.write_to_file("config1.ini")
141 # # load file
142 # var config = new ConfigTree("config1.ini")
143 # assert config["foo.bar"] == "foobar"
144 #
145 # Example with sections:
146 #
147 # # init file
148 # str = """
149 # goo=goo
150 # [foo]
151 # bar=foobar
152 # baz=foobaz
153 # [boo]
154 # bar=boobar"""
155 # str.write_to_file("config2.ini")
156 # # load file
157 # config = new ConfigTree("config2.ini")
158 # assert config["foo.bar"] == "foobar"
159 # assert config["boo.bar"] == "boobar"
160 #
161 # Example with both hierarchy and section:
162 #
163 # # init file
164 # str = """
165 # goo=goo
166 # [foo]
167 # bar.baz=foobarbaz
168 # [goo.boo]
169 # bar=gooboobar
170 # baz.bar=gooboobazbar"""
171 # str.write_to_file("config3.ini")
172 # # load file
173 # config = new ConfigTree("config3.ini")
174 # assert config["goo"] == "goo"
175 # assert config["foo.bar.baz"] == "foobarbaz"
176 # assert config["goo.boo.bar"] == "gooboobar"
177 # assert config["goo.boo.baz.bar"] == "gooboobazbar"
178 #
179 # Using the array notation
180 #
181 # str = """
182 # foo[]=a
183 # foo[]=b
184 # foo[]=c"""
185 # str.write_to_file("config4.ini")
186 # # load file
187 # config = new ConfigTree("config4.ini")
188 # print config.to_map.join(":", ",")
189 # assert config["foo.0"] == "a"
190 # assert config["foo.1"] == "b"
191 # assert config["foo.2"] == "c"
192 # assert config.at("foo").values.join(",") == "a,b,c"
193 fun load do
194 roots.clear
195 var stream = new FileReader.open(ini_file)
196 var path: nullable String = null
197 var line_number = 0
198 while not stream.eof do
199 var line = stream.read_line
200 line_number += 1
201 if line.is_empty then
202 continue
203 else if line.has_prefix(";") then
204 continue
205 else if line.has_prefix("[") then
206 line = line.trim
207 var key = line.substring(1, line.length - 2)
208 path = key
209 set_node(path, null)
210 else
211 var parts = line.split("=")
212 assert parts.length > 1 else
213 print "Error: malformed ini at line {line_number}"
214 end
215 var key = parts[0].trim
216 var val = parts[1].trim
217 if path != null then key = "{path}.{key}"
218 if key.has_suffix("[]") then
219 set_array(key, val)
220 else
221 set_node(key,val)
222 end
223 end
224 end
225 stream.close
226 end
227
228 # Save config to file
229 fun save do write_to_file(ini_file)
230
231 private var roots = new Array[ConfigNode]
232
233 # Append `value` to array at `key`
234 private fun set_array(key: String, value: nullable String) do
235 key = key.substring(0, key.length - 2)
236 var len = 0
237 if has_key(key) then
238 len = get_node(key).children.length
239 end
240 set_node("{key}.{len.to_s}", value)
241 end
242
243 private fun set_node(key: String, value: nullable String) do
244 var parts = key.split(".").reversed
245 var k = parts.pop
246 var root = get_root(k)
247 if root == null then
248 root = new ConfigNode(k)
249 if parts.is_empty then
250 root.value = value
251 end
252 roots.add root
253 end
254 while not parts.is_empty do
255 k = parts.pop
256 var node = root.get_child(k)
257 if node == null then
258 node = new ConfigNode(k)
259 node.parent = root
260 root.children[node.name] = node
261 end
262 if parts.is_empty then
263 node.value = value
264 end
265 root = node
266 end
267 end
268
269 private fun get_node(key: String): nullable ConfigNode do
270 var parts = key.split(".").reversed
271 var node = get_root(parts.pop)
272 while node != null and not parts.is_empty do
273 node = node.get_child(parts.pop)
274 end
275 return node
276 end
277
278 private fun get_root(name: String): nullable ConfigNode do
279 for root in roots do
280 if root.name == name then return root
281 end
282 return null
283 end
284
285 private fun leaves: Array[ConfigNode] do
286 var res = new Array[ConfigNode]
287 var todo = new Array[ConfigNode]
288 todo.add_all roots
289 while not todo.is_empty do
290 var node = todo.pop
291 if node.children.is_empty then
292 res.add node
293 else
294 todo.add_all node.children.values
295 end
296 end
297 return res
298 end
299 end
300
301 private class ConfigNode
302
303 var parent: nullable ConfigNode = null
304 var children = new HashMap[String, ConfigNode]
305 var name: String is writable
306 var value: nullable String = null
307
308 fun key: String do
309 if parent == null then
310 return name
311 end
312 return "{parent.key}.{name}"
313 end
314
315 fun get_child(name: String): nullable ConfigNode do
316 if children.has_key(name) then
317 return children[name]
318 end
319 return null
320 end
321 end