1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Read and write INI configuration files
19 intrude import core
::collection
::hash_collection
21 # Read and write INI configuration files
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 `]`).
28 # var ini_string = """
38 # The main class, `IniFile`, can be created from an INI string and allows easy
39 # access to its content.
42 # # Read INI from string
43 # var ini = new IniFile.from_string(ini_string)
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")
51 # assert ini["key"] == "value1"
52 # assert ini["section2.key"] == "value3"
53 # assert ini["not.found"] == null
56 # assert ini.sections.length == 2
57 # assert ini.section("section1")["key"] == "value2"
60 # `IniFile` can also be used to create new INI files from scratch, or edit
61 # existing ones through its API.
64 # # Create a new INI file and write it to disk
66 # ini["key"] = "value1"
67 # ini["section1.key"] = "value2"
68 # ini["section2.key"] = "value3"
69 # ini.write_to_file("my_config.ini")
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"
77 # "my_config.ini".to_path.delete
81 super HashMap[String, nullable String]
83 # Create a IniFile from a `string` content
86 # var ini = new IniFile.from_string("""
91 # assert ini["key1"] == "value1"
92 # assert ini["section1.key2"] == "value2"
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
101 # Create a IniFile from a `file` content
108 # """.write_to_file("my_config.ini")
110 # var ini = new IniFile.from_file("my_config.ini")
111 # assert ini["key1"] == "value1"
112 # assert ini["section1.key2"] == "value2"
114 # "my_config.ini".to_path.delete
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
123 # Sections composing this IniFile
126 # var ini = new IniFile.from_string("""
132 # assert ini.sections.length == 2
133 # assert ini.sections.first.name == "section1"
134 # assert ini.sections.last.name == "section 2"
136 var sections
= new Array[IniSection]
138 # Get a section by its `name`
140 # Returns `null` if the section is not found.
143 # var ini = new IniFile.from_string("""
149 # assert ini.section("section1") isa IniSection
150 # assert ini.section("section2").name == "section2"
151 # assert ini.section("not.found") == null
153 fun section
(name
: String): nullable IniSection do
154 for section
in sections
do
155 if section
.name
== name
then return section
160 # Does this file contains no properties and no sections?
163 # var ini = new IniFile.from_string("")
164 # assert ini.is_empty
166 # ini = new IniFile.from_string("""
169 # assert not ini.is_empty
171 # ini = new IniFile.from_string("""
174 # assert not ini.is_empty
176 redef fun is_empty
do return super and sections
.is_empty
178 # Is there a property located at `key`?
180 # Returns `true` if the `key` is not found of if its associated value is `null`.
183 # var ini = new IniFile.from_string("""
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")
196 redef fun has_key
(key
) do return self[key
] != null
198 # Get the value associated with a property (`key`)
200 # Returns `null` if the key is not found.
201 # Section properties can be accessed with the `.` notation.
204 # var ini = new IniFile.from_string("""
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
218 if key
== null then return null
222 var node
= node_at
(key
)
223 if node
!= null then return node
.value
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
]
235 # Set the `value` for the property locaated at `key`
238 # var ini = new IniFile
239 # ini["key"] = "value1"
240 # ini["section1.key"] = "value2"
241 # ini["section2.key"] = "value3"
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"
249 redef fun []=(key
, value
) do
250 if value
== null then return
251 var parts
= key
.split_once_on
(".")
253 # No dot notation, store value in root
254 if parts
.length
== 1 then
255 super(key
.trim
, value
.trim
)
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
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
272 # Flatten `self` and its subsection in a `Map` of keys => values
274 # Properties from section are prefixed with their section names with the
275 # dot (`.`) notation.
278 # var ini = new IniFile.from_string("""
283 # assert ini.flatten.join(", ", ": ") == "key: value1, section.key: value2"
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
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
300 # Write `self` to a `stream`
302 # Key with `null` values are ignored.
303 # The empty string can be used to represent an empty value.
306 # var ini = new IniFile
307 # ini["key"] = "value1"
310 # ini["section1.key"] = "value2"
311 # ini["section1.key2"] = null
312 # ini["section2.key"] = "value3"
314 # var stream = new StringWriter
315 # ini.write_to(stream)
317 # assert stream.to_s == """
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"
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"
340 # Read INI content from `string`
343 # var ini = new IniFile
344 # ini.load_string("""
345 # section1.key1=value1
346 # section1.key2=value2
350 # assert ini["section1.key1"] == "value1"
351 # assert ini["section1.key2"] == "value2"
352 # assert ini["section2.key"] == "value3"
355 # Returns `true` if the parsing finished correctly.
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
363 while not stream
.eof
do
365 var line
= stream
.read_line
.trim
366 if line
.is_empty
then
368 else if line
.has_prefix
(";") then
370 else if line
.has_prefix
("#") then
372 else if line
.has_prefix
("[") then
373 var section
= new IniSection(line
.substring
(1, line
.length
- 2).trim
)
375 last_section
= section
378 var parts
= line
.split_once_on
("=")
379 if parts
.length
!= 2 then
381 # we definitely need exceptions...
383 errors
.add
new IniError("Unexpected string `{line}` at line {i}.")
384 if stop_on_first_error
then return was_error
387 var key
= parts
[0].trim
388 var value
= parts
[1].trim
390 if last_section
!= null then
391 last_section
[key
] = value
401 # Load a `file` content as INI
403 # New properties will be appended to the `self`, existing properties will be
404 # overwrote by the values contained in `file`.
407 # var ini = new IniFile
408 # ini["key1"] = "value1"
409 # ini["key2"] = "value2"
414 # """.write_to_file("load_config.ini")
416 # ini.load_file("load_config.ini")
417 # assert ini["key1"] == "value1"
418 # assert ini["key2"] == "changed"
419 # assert ini["key3"] == "added"
421 # "load_config.ini".to_path.delete
424 # The process fails silently if the file does not exist.
428 # ini.load_file("ini_not_found.ini")
429 # assert ini.is_empty
432 # Returns `true` if the parsing finished correctly.
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
)
437 # Stop parsing on the first error
439 # By default, `load_string` will skip unparsable properties so the string can
443 # var ini = new IniFile.from_string("""
449 # assert ini.length == 2
450 # assert ini["key1"] == "value1"
451 # assert ini["key2"] == null
452 # assert ini["key3"] == "value3"
455 # Set `stop_on_first_error` to `true` to force the parsing to stop.
459 # ini.stop_on_first_error = true
460 # ini.load_string("""
466 # assert ini.length == 1
467 # assert ini["key1"] == "value1"
468 # assert ini["key2"] == null
469 # assert ini["key3"] == null
473 var stop_on_first_error
= false is optional
, writable
475 # Errors found during parsing
477 # Wathever the value of `stop_on_first_error`, errors from parsing a string
478 # or a file are logged into `errors`.
481 # var ini = new IniFile.from_string("""
487 # assert ini.errors.length == 1
488 # assert ini.errors.first.message == "Unexpected string `key2` at line 2."
491 # `errors` is not cleared between two parsing:
494 # ini.load_string("""
499 # assert ini.errors.length == 2
500 # assert ini.errors.last.message == "Unexpected string `key4` at line 1."
503 # See also `stop_on_first_error`.
504 var errors
= new Array[IniError]
507 # A section in a IniFile
509 # Section properties values are strings associated keys.
510 # Sections cannot be nested.
513 # var section = new IniSection("section")
514 # section["key1"] = "value1"
515 # section["key2"] = "value2"
517 # assert section.length == 2
518 # assert section["key1"] == "value1"
519 # assert section["not.found"] == null
520 # assert section.join(", ", ": ") == "key1: value1, key2: value2"
523 # for key, value in section do
524 # assert key.has_prefix("key")
525 # assert value.has_prefix("value")
531 super HashMap[String, nullable String]
536 # Get the value associated with `key`
538 # Returns `null` if the `key` is not found.
541 # var section = new IniSection("section")
542 # section["key"] = "value1"
543 # section["sub.key"] = "value2"
545 # assert section["key"] == "value1"
546 # assert section["sub.key"] == "value2"
547 # assert section["not.found"] == null
550 if not has_key
(key
) then return null
555 # Error for `IniFile` parsing