Merge: nitrpg: Move `nitrpg` to its own repository
[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 # Read and write INI configuration files
16 module ini
17
18 import core
19 intrude import core::collection::hash_collection
20
21 # Read and write INI configuration files
22 #
23 # In an INI file, properties (or keys) are associated to values thanks to the
24 # equals symbol (`=`).
25 # Properties may be grouped into section marked between brackets (`[` and `]`).
26 #
27 # ~~~
28 # var ini_string = """
29 # ; Example INI
30 # key=value1
31 # [section1]
32 # key=value2
33 # [section2]
34 # key=value3
35 # """
36 # ~~~
37 #
38 # The main class, `IniFile`, can be created from an INI string and allows easy
39 # access to its content.
40 #
41 # ~~~
42 # # Read INI from string
43 # var ini = new IniFile.from_string(ini_string)
44 #
45 # # Check keys presence
46 # assert ini.has_key("key")
47 # assert ini.has_key("section1.key")
48 # assert not ini.has_key("not.found")
49 #
50 # # Access values
51 # assert ini["key"] == "value1"
52 # assert ini["section2.key"] == "value3"
53 # assert ini["not.found"] == null
54 #
55 # # Access sections
56 # assert ini.sections.length == 2
57 # assert ini.section("section1")["key"] == "value2"
58 # ~~~
59 #
60 # `IniFile` can also be used to create new INI files from scratch, or edit
61 # existing ones through its API.
62 #
63 # ~~~
64 # # Create a new INI file and write it to disk
65 # ini = new IniFile
66 # ini["key"] = "value1"
67 # ini["section1.key"] = "value2"
68 # ini["section2.key"] = "value3"
69 # ini.write_to_file("my_config.ini")
70 #
71 # # Load the INI file from disk
72 # ini = new IniFile.from_file("my_config.ini")
73 # assert ini["key"] == "value1"
74 # assert ini["section1.key"] == "value2"
75 # assert ini["section2.key"] == "value3"
76 #
77 # "my_config.ini".to_path.delete
78 # ~~~
79 class IniFile
80 super Writable
81 super HashMap[String, nullable String]
82
83 # Create a IniFile from a `string` content
84 #
85 # ~~~
86 # var ini = new IniFile.from_string("""
87 # key1=value1
88 # [section1]
89 # key2=value2
90 # """)
91 # assert ini["key1"] == "value1"
92 # assert ini["section1.key2"] == "value2"
93 # ~~~
94 #
95 # See also `stop_on_first_error` and `errors`.
96 init from_string(string: String, stop_on_first_error: nullable Bool) do
97 init stop_on_first_error or else false
98 load_string(string)
99 end
100
101 # Create a IniFile from a `file` content
102 #
103 # ~~~
104 # """
105 # key1=value1
106 # [section1]
107 # key2=value2
108 # """.write_to_file("my_config.ini")
109 #
110 # var ini = new IniFile.from_file("my_config.ini")
111 # assert ini["key1"] == "value1"
112 # assert ini["section1.key2"] == "value2"
113 #
114 # "my_config.ini".to_path.delete
115 # ~~~
116 #
117 # See also `stop_on_first_error` and `errors`.
118 init from_file(file: String, stop_on_first_error: nullable Bool) do
119 init stop_on_first_error or else false
120 load_file(file)
121 end
122
123 # Sections composing this IniFile
124 #
125 # ~~~
126 # var ini = new IniFile.from_string("""
127 # [section1]
128 # key1=value1
129 # [ section 2 ]
130 # key2=value2
131 # """)
132 # assert ini.sections.length == 2
133 # assert ini.sections.first.name == "section1"
134 # assert ini.sections.last.name == "section 2"
135 # ~~~
136 var sections = new Array[IniSection]
137
138 # Get a section by its `name`
139 #
140 # Returns `null` if the section is not found.
141 #
142 # ~~~
143 # var ini = new IniFile.from_string("""
144 # [section1]
145 # key1=value1
146 # [section2]
147 # key2=value2
148 # """)
149 # assert ini.section("section1") isa IniSection
150 # assert ini.section("section2").name == "section2"
151 # assert ini.section("not.found") == null
152 # ~~~
153 fun section(name: String): nullable IniSection do
154 for section in sections do
155 if section.name == name then return section
156 end
157 return null
158 end
159
160 # Does this file contains no properties and no sections?
161 #
162 # ~~~
163 # var ini = new IniFile.from_string("")
164 # assert ini.is_empty
165 #
166 # ini = new IniFile.from_string("""
167 # key=value
168 # """)
169 # assert not ini.is_empty
170 #
171 # ini = new IniFile.from_string("""
172 # [section]
173 # """)
174 # assert not ini.is_empty
175 # ~~~
176 redef fun is_empty do return super and sections.is_empty
177
178 # Is there a property located at `key`?
179 #
180 # Returns `true` if the `key` is not found of if its associated value is `null`.
181 #
182 # ~~~
183 # var ini = new IniFile.from_string("""
184 # key=value1
185 # [section1]
186 # key=value2
187 # [section2]
188 # key=value3
189 # """)
190 # assert ini.has_key("key")
191 # assert ini.has_key("section1.key")
192 # assert ini.has_key("section2.key")
193 # assert not ini.has_key("section1")
194 # assert not ini.has_key("not.found")
195 # ~~~
196 redef fun has_key(key) do return self[key] != null
197
198 # Get the value associated with a property (`key`)
199 #
200 # Returns `null` if the key is not found.
201 # Section properties can be accessed with the `.` notation.
202 #
203 # ~~~
204 # var ini = new IniFile.from_string("""
205 # key=value1
206 # [section1]
207 # key=value2
208 # [section2]
209 # key=value3
210 # """)
211 # assert ini["key"] == "value1"
212 # assert ini["section1.key"] == "value2"
213 # assert ini["section2.key"] == "value3"
214 # assert ini["section1"] == null
215 # assert ini["not.found"] == null
216 # ~~~
217 redef fun [](key) do
218 if key == null then return null
219 key = key.to_s.trim
220
221 # Look in root
222 var node = node_at(key)
223 if node != null then return node.value
224
225 # Look in sections
226 for section in sections do
227 # Matched if the section name is a prefix of the key
228 if not key.has_prefix(section.name) then continue
229 var skey = key.substring(section.name.length + 1, key.length)
230 if section.has_key(skey) then return section[skey]
231 end
232 return null
233 end
234
235 # Set the `value` for the property locaated at `key`
236 #
237 # ~~~
238 # var ini = new IniFile
239 # ini["key"] = "value1"
240 # ini["section1.key"] = "value2"
241 # ini["section2.key"] = "value3"
242 #
243 # assert ini["key"] == "value1"
244 # assert ini["section1.key"] == "value2"
245 # assert ini["section2.key"] == "value3"
246 # assert ini.section("section1").name == "section1"
247 # assert ini.section("section2")["key"] == "value3"
248 # ~~~
249 redef fun []=(key, value) do
250 if value == null then return
251 var parts = key.split_once_on(".")
252
253 # No dot notation, store value in root
254 if parts.length == 1 then
255 super(key.trim, value.trim)
256 return
257 end
258
259 # First part matches a section, store value in it
260 var section = self.section(parts.first.trim)
261 if section != null then
262 section[parts.last.trim] = value.trim
263 return
264 end
265
266 # No section matched, create a new one and store value in it
267 section = new IniSection(parts.first.trim)
268 section[parts.last.trim] = value.trim
269 sections.add section
270 end
271
272 # Flatten `self` and its subsection in a `Map` of keys => values
273 #
274 # Properties from section are prefixed with their section names with the
275 # dot (`.`) notation.
276 #
277 # ~~~
278 # var ini = new IniFile.from_string("""
279 # key=value1
280 # [section]
281 # key=value2
282 # """)
283 # assert ini.flatten.join(", ", ": ") == "key: value1, section.key: value2"
284 # ~~~
285 fun flatten: Map[String, String] do
286 var map = new HashMap[String, String]
287 for key, value in self do
288 if value == null then continue
289 map[key] = value
290 end
291 for section in sections do
292 for key, value in section do
293 if value == null then continue
294 map["{section.name}.{key}"] = value
295 end
296 end
297 return map
298 end
299
300 # Write `self` to a `stream`
301 #
302 # Key with `null` values are ignored.
303 # The empty string can be used to represent an empty value.
304 #
305 # ~~~
306 # var ini = new IniFile
307 # ini["key"] = "value1"
308 # ini["key2"] = null
309 # ini["key3"] = ""
310 # ini["section1.key"] = "value2"
311 # ini["section1.key2"] = null
312 # ini["section2.key"] = "value3"
313 #
314 # var stream = new StringWriter
315 # ini.write_to(stream)
316 #
317 # assert stream.to_s == """
318 # key=value1
319 # key3=
320 # [section1]
321 # key=value2
322 # [section2]
323 # key=value3
324 # """
325 # ~~~
326 redef fun write_to(stream) do
327 for key, value in self do
328 if value == null then continue
329 stream.write "{key}={value}\n"
330 end
331 for section in sections do
332 stream.write "[{section.name}]\n"
333 for key, value in section do
334 if value == null then continue
335 stream.write "{key}={value}\n"
336 end
337 end
338 end
339
340 # Read INI content from `string`
341 #
342 # ~~~
343 # var ini = new IniFile
344 # ini.load_string("""
345 # section1.key1=value1
346 # section1.key2=value2
347 # [section2]
348 # key=value3
349 # """)
350 # assert ini["section1.key1"] == "value1"
351 # assert ini["section1.key2"] == "value2"
352 # assert ini["section2.key"] == "value3"
353 # ~~~
354 #
355 # Returns `true` if the parsing finished correctly.
356 #
357 # See also `stop_on_first_error` and `errors`.
358 fun load_string(string: String): Bool do
359 var stream = new StringReader(string)
360 var last_section = null
361 var was_error = false
362 var i = 0
363 while not stream.eof do
364 i += 1
365 var line = stream.read_line.trim
366 if line.is_empty then
367 continue
368 else if line.has_prefix(";") then
369 continue
370 else if line.has_prefix("#") then
371 continue
372 else if line.has_prefix("[") then
373 var section = new IniSection(line.substring(1, line.length - 2).trim)
374 sections.add section
375 last_section = section
376 continue
377 else
378 var parts = line.split_once_on("=")
379 if parts.length != 2 then
380 # FIXME silent skip?
381 # we definitely need exceptions...
382 was_error = true
383 errors.add new IniError("Unexpected string `{line}` at line {i}.")
384 if stop_on_first_error then return was_error
385 continue
386 end
387 var key = parts[0].trim
388 var value = parts[1].trim
389
390 if last_section != null then
391 last_section[key] = value
392 else
393 self[key] = value
394 end
395 end
396 end
397 stream.close
398 return was_error
399 end
400
401 # Load a `file` content as INI
402 #
403 # New properties will be appended to the `self`, existing properties will be
404 # overwrote by the values contained in `file`.
405 #
406 # ~~~
407 # var ini = new IniFile
408 # ini["key1"] = "value1"
409 # ini["key2"] = "value2"
410 #
411 # """
412 # key2=changed
413 # key3=added
414 # """.write_to_file("load_config.ini")
415 #
416 # ini.load_file("load_config.ini")
417 # assert ini["key1"] == "value1"
418 # assert ini["key2"] == "changed"
419 # assert ini["key3"] == "added"
420 #
421 # "load_config.ini".to_path.delete
422 # ~~~
423 #
424 # The process fails silently if the file does not exist.
425 #
426 # ~~~
427 # ini = new IniFile
428 # ini.load_file("ini_not_found.ini")
429 # assert ini.is_empty
430 # ~~~
431 #
432 # Returns `true` if the parsing finished correctly.
433 #
434 # See also `stop_on_first_error` and `errors`.
435 fun load_file(file: String): Bool do return load_string(file.to_path.read_all)
436
437 # Stop parsing on the first error
438 #
439 # By default, `load_string` will skip unparsable properties so the string can
440 # be loaded.
441 #
442 # ~~~
443 # var ini = new IniFile.from_string("""
444 # key1=value1
445 # key2
446 # key3=value3
447 # """)
448 #
449 # assert ini.length == 2
450 # assert ini["key1"] == "value1"
451 # assert ini["key2"] == null
452 # assert ini["key3"] == "value3"
453 # ~~~
454 #
455 # Set `stop_on_first_error` to `true` to force the parsing to stop.
456 #
457 # ~~~
458 # ini = new IniFile
459 # ini.stop_on_first_error = true
460 # ini.load_string("""
461 # key1=value1
462 # key2
463 # key3=value3
464 # """)
465 #
466 # assert ini.length == 1
467 # assert ini["key1"] == "value1"
468 # assert ini["key2"] == null
469 # assert ini["key3"] == null
470 # ~~~
471 #
472 # See also `errors`.
473 var stop_on_first_error = false is optional, writable
474
475 # Errors found during parsing
476 #
477 # Wathever the value of `stop_on_first_error`, errors from parsing a string
478 # or a file are logged into `errors`.
479 #
480 # ~~~
481 # var ini = new IniFile.from_string("""
482 # key1=value1
483 # key2
484 # key3=value3
485 # """)
486 #
487 # assert ini.errors.length == 1
488 # assert ini.errors.first.message == "Unexpected string `key2` at line 2."
489 # ~~~
490 #
491 # `errors` is not cleared between two parsing:
492 #
493 # ~~~
494 # ini.load_string("""
495 # key4
496 # key5=value5
497 # """)
498 #
499 # assert ini.errors.length == 2
500 # assert ini.errors.last.message == "Unexpected string `key4` at line 1."
501 # ~~~
502 #
503 # See also `stop_on_first_error`.
504 var errors = new Array[IniError]
505 end
506
507 # A section in a IniFile
508 #
509 # Section properties values are strings associated keys.
510 # Sections cannot be nested.
511 #
512 # ~~~
513 # var section = new IniSection("section")
514 # section["key1"] = "value1"
515 # section["key2"] = "value2"
516 #
517 # assert section.length == 2
518 # assert section["key1"] == "value1"
519 # assert section["not.found"] == null
520 # assert section.join(", ", ": ") == "key1: value1, key2: value2"
521 #
522 # var i = 0
523 # for key, value in section do
524 # assert key.has_prefix("key")
525 # assert value.has_prefix("value")
526 # i += 1
527 # end
528 # assert i == 2
529 # ~~~
530 class IniSection
531 super HashMap[String, nullable String]
532
533 # Section name
534 var name: String
535
536 # Get the value associated with `key`
537 #
538 # Returns `null` if the `key` is not found.
539 #
540 # ~~~
541 # var section = new IniSection("section")
542 # section["key"] = "value1"
543 # section["sub.key"] = "value2"
544 #
545 # assert section["key"] == "value1"
546 # assert section["sub.key"] == "value2"
547 # assert section["not.found"] == null
548 # ~~~
549 redef fun [](key) do
550 if not has_key(key) then return null
551 return super
552 end
553 end
554
555 # Error for `IniFile` parsing
556 class IniError
557 super Error
558 end