# See the License for the specific language governing permissions and
# limitations under the License.
-# Handle ini config files.
+# Read and write INI configuration files
module ini
-# A configuration tree that can read and store data in ini format
+import core
+intrude import core::collection::hash_collection
+
+# Read and write INI configuration files
+#
+# In an INI file, properties (or keys) are associated to values thanks to the
+# equals symbol (`=`).
+# Properties may be grouped into section marked between brackets (`[` and `]`).
+#
+# ~~~
+# var ini_string = """
+# ; Example INI
+# key=value1
+# [section1]
+# key=value2
+# [section2]
+# key=value3
+# """
+# ~~~
+#
+# The main class, `IniFile`, can be created from an INI string and allows easy
+# access to its content.
+#
+# ~~~
+# # Read INI from string
+# var ini = new IniFile.from_string(ini_string)
+#
+# # Check keys presence
+# assert ini.has_key("key")
+# assert ini.has_key("section1.key")
+# assert not ini.has_key("not.found")
+#
+# # Access values
+# assert ini["key"] == "value1"
+# assert ini["section2.key"] == "value3"
+# assert ini["not.found"] == null
+#
+# # Access sections
+# assert ini.sections.length == 2
+# assert ini.section("section1")["key"] == "value2"
+# ~~~
#
-# Write example:
+# `IniFile` can also be used to create new INI files from scratch, or edit
+# existing ones through its API.
#
-# var config = new ConfigTree("config.ini")
-# config["goo"] = "goo"
-# config["foo.bar"] = "foobar"
-# config["foo.baz"] = "foobaz"
-# config.save
-# assert config.to_map.length == 3
+# ~~~
+# # Create a new INI file and write it to disk
+# ini = new IniFile
+# ini["key"] = "value1"
+# ini["section1.key"] = "value2"
+# ini["section2.key"] = "value3"
+# ini.write_to_file("my_config.ini")
#
-# Read example:
+# # Load the INI file from disk
+# ini = new IniFile.from_file("my_config.ini")
+# assert ini["key"] == "value1"
+# assert ini["section1.key"] == "value2"
+# assert ini["section2.key"] == "value3"
#
-# config = new ConfigTree("config.ini")
-# assert config.has_key("foo.bar")
-# assert config["foo.bar"] == "foobar"
-class ConfigTree
+# "my_config.ini".to_path.delete
+# ~~~
+class IniFile
super Writable
+ super HashMap[String, nullable String]
- # The ini file used to read/store data
- var ini_file: String
-
- init do if ini_file.file_exists then load
+ # Create a IniFile from a `string` content
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key1=value1
+ # [section1]
+ # key2=value2
+ # """)
+ # assert ini["key1"] == "value1"
+ # assert ini["section1.key2"] == "value2"
+ # ~~~
+ #
+ # See also `stop_on_first_error` and `errors`.
+ init from_string(string: String, stop_on_first_error: nullable Bool) do
+ init stop_on_first_error or else false
+ load_string(string)
+ end
- # Get the config value for `key`
- #
- # var config = new ConfigTree("config.ini")
- # assert config["goo"] == "goo"
- # assert config["foo.bar"] == "foobar"
- # assert config["foo.baz"] == "foobaz"
- # assert config["fail.fail"] == null
- fun [](key: String): nullable String do
- var node = get_node(key)
- if node == null then return null
- return node.value
+ # Create a IniFile from a `file` content
+ #
+ # ~~~
+ # """
+ # key1=value1
+ # [section1]
+ # key2=value2
+ # """.write_to_file("my_config.ini")
+ #
+ # var ini = new IniFile.from_file("my_config.ini")
+ # assert ini["key1"] == "value1"
+ # assert ini["section1.key2"] == "value2"
+ #
+ # "my_config.ini".to_path.delete
+ # ~~~
+ #
+ # See also `stop_on_first_error` and `errors`.
+ init from_file(file: String, stop_on_first_error: nullable Bool) do
+ init stop_on_first_error or else false
+ load_file(file)
end
- # Get the config values under `key`
+ # Sections composing this IniFile
#
- # var config = new ConfigTree("config.ini")
- # var values = config.at("foo")
- # assert values.has_key("bar")
- # assert values.has_key("baz")
- # assert not values.has_key("goo")
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # [section1]
+ # key1=value1
+ # [ section 2 ]
+ # key2=value2
+ # """)
+ # assert ini.sections.length == 2
+ # assert ini.sections.first.name == "section1"
+ # assert ini.sections.last.name == "section 2"
+ # ~~~
+ var sections = new Array[IniSection]
+
+ # Get a section by its `name`
#
- # Return null if the key does not exists.
+ # Returns `null` if the section is not found.
#
- # assert config.at("fail.fail") == null
- fun at(key: String): nullable Map[String, String] do
- var node = get_node(key)
- if node == null then return null
- var map = new HashMap[String, String]
- for k, child in node.children do
- var value = child.value
- if value == null then continue
- map[k] = value
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # [section1]
+ # key1=value1
+ # [section2]
+ # key2=value2
+ # """)
+ # assert ini.section("section1") isa IniSection
+ # assert ini.section("section2").name == "section2"
+ # assert ini.section("not.found") == null
+ # ~~~
+ fun section(name: String): nullable IniSection do
+ for section in sections do
+ if section.name == name then return section
end
- return map
+ return null
end
- # Set `value` at `key`
- #
- # var config = new ConfigTree("config.ini")
- # assert config["foo.bar"] == "foobar"
- # config["foo.bar"] = "baz"
- # assert config["foo.bar"] == "baz"
- fun []=(key: String, value: nullable String) do
- set_node(key, value)
+ # Does this file contains no properties and no sections?
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("")
+ # assert ini.is_empty
+ #
+ # ini = new IniFile.from_string("""
+ # key=value
+ # """)
+ # assert not ini.is_empty
+ #
+ # ini = new IniFile.from_string("""
+ # [section]
+ # """)
+ # assert not ini.is_empty
+ # ~~~
+ redef fun is_empty do return super and sections.is_empty
+
+ # Is there a property located at `key`?
+ #
+ # Returns `true` if the `key` is not found of if its associated value is `null`.
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key=value1
+ # [section1]
+ # key=value2
+ # [section2]
+ # key=value3
+ # """)
+ # assert ini.has_key("key")
+ # assert ini.has_key("section1.key")
+ # assert ini.has_key("section2.key")
+ # assert not ini.has_key("section1")
+ # assert not ini.has_key("not.found")
+ # ~~~
+ redef fun has_key(key) do return self[key] != null
+
+ # Get the value associated with a property (`key`)
+ #
+ # Returns `null` if the key is not found.
+ # Section properties can be accessed with the `.` notation.
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key=value1
+ # [section1]
+ # key=value2
+ # [section2]
+ # key=value3
+ # """)
+ # assert ini["key"] == "value1"
+ # assert ini["section1.key"] == "value2"
+ # assert ini["section2.key"] == "value3"
+ # assert ini["section1"] == null
+ # assert ini["not.found"] == null
+ # ~~~
+ redef fun [](key) do
+ if key == null then return null
+ key = key.to_s.trim
+
+ # Look in root
+ var node = node_at(key)
+ if node != null then return node.value
+
+ # Look in sections
+ for section in sections do
+ # Matched if the section name is a prefix of the key
+ if not key.has_prefix(section.name) then continue
+ var skey = key.substring(section.name.length + 1, key.length)
+ if section.has_key(skey) then return section[skey]
+ end
+ return null
end
- # Is `key` in the config?
- #
- # var config = new ConfigTree("config.ini")
- # assert config.has_key("goo")
- # assert config.has_key("foo.bar")
- # assert not config.has_key("zoo")
- fun has_key(key: String): Bool do
- var parts = key.split(".").reversed
- var node = get_root(parts.pop)
- if node == null then return false
- while not parts.is_empty do
- node = node.get_child(parts.pop)
- if node == null then return false
+ # Set the `value` for the property locaated at `key`
+ #
+ # ~~~
+ # var ini = new IniFile
+ # ini["key"] = "value1"
+ # ini["section1.key"] = "value2"
+ # ini["section2.key"] = "value3"
+ #
+ # assert ini["key"] == "value1"
+ # assert ini["section1.key"] == "value2"
+ # assert ini["section2.key"] == "value3"
+ # assert ini.section("section1").name == "section1"
+ # assert ini.section("section2")["key"] == "value3"
+ # ~~~
+ redef fun []=(key, value) do
+ if value == null then return
+ var parts = key.split_once_on(".")
+
+ # No dot notation, store value in root
+ if parts.length == 1 then
+ super(key.trim, value.trim)
+ return
+ end
+
+ # First part matches a section, store value in it
+ var section = self.section(parts.first.trim)
+ if section != null then
+ section[parts.last.trim] = value.trim
+ return
end
- return true
+
+ # No section matched, create a new one and store value in it
+ section = new IniSection(parts.first.trim)
+ section[parts.last.trim] = value.trim
+ sections.add section
end
- # Get `self` as a Map of `key`, `value`
- #
- # var config = new ConfigTree("config.ini")
- # var map = config.to_map
- # assert map.has_key("goo")
- # assert map.has_key("foo.bar")
- # assert map.has_key("foo.baz")
- # assert map.length == 3
- fun to_map: Map[String, String] do
+ # Flatten `self` and its subsection in a `Map` of keys => values
+ #
+ # Properties from section are prefixed with their section names with the
+ # dot (`.`) notation.
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key=value1
+ # [section]
+ # key=value2
+ # """)
+ # assert ini.flatten.join(", ", ": ") == "key: value1, section.key: value2"
+ # ~~~
+ fun flatten: Map[String, String] do
var map = new HashMap[String, String]
- for node in leaves do
- var value = node.value
+ for key, value in self do
if value == null then continue
- map[node.key] = value
+ map[key] = value
+ end
+ for section in sections do
+ for key, value in section do
+ if value == null then continue
+ map["{section.name}.{key}"] = value
+ end
end
return map
end
- redef fun to_s do return to_map.join(", ", ":")
-
- # Write `self` in `stream`
- #
- # var config = new ConfigTree("config.ini")
- # var out = new StringWriter
- # config.write_to(out)
- # assert out.to_s == """
- # goo=goo
- # [foo]
- # bar=foobar
- # baz=foobaz
- # """
+ # Write `self` to a `stream`
+ #
+ # Key with `null` values are ignored.
+ # The empty string can be used to represent an empty value.
+ #
+ # ~~~
+ # var ini = new IniFile
+ # ini["key"] = "value1"
+ # ini["key2"] = null
+ # ini["key3"] = ""
+ # ini["section1.key"] = "value2"
+ # ini["section1.key2"] = null
+ # ini["section2.key"] = "value3"
+ #
+ # var stream = new StringWriter
+ # ini.write_to(stream)
+ #
+ # assert stream.to_s == """
+ # key=value1
+ # key3=
+ # [section1]
+ # key=value2
+ # [section2]
+ # key=value3
+ # """
+ # ~~~
redef fun write_to(stream) do
- var todo = new Array[ConfigNode].from(roots.reversed)
- while not todo.is_empty do
- var node = todo.pop
- if node.children.not_empty then
- todo.add_all node.children.values.to_a.reversed
- end
- if node.children.not_empty and node.parent == null then
- stream.write("[{node.name}]\n")
- end
- var value = node.value
+ for key, value in self do
if value == null then continue
- var path = node.path
- if path.length > 1 then path.shift
- stream.write("{path.join(".")}={value}\n")
+ stream.write "{key}={value}\n"
+ end
+ for section in sections do
+ stream.write "[{section.name}]\n"
+ for key, value in section do
+ if value == null then continue
+ stream.write "{key}={value}\n"
+ end
end
end
- # Reload config from file
- # Done automatically at init
- #
- # Example with hierarchical ini file:
- #
- # # init file
- # var str = """
- # foo.bar=foobar
- # foo.baz=foobaz
- # goo=goo"""
- # str.write_to_file("config1.ini")
- # # load file
- # var config = new ConfigTree("config1.ini")
- # assert config["foo.bar"] == "foobar"
- #
- # Example with sections:
- #
- # # init file
- # str = """
- # goo=goo
- # [foo]
- # bar=foobar
- # baz=foobaz
- # [boo]
- # bar=boobar"""
- # str.write_to_file("config2.ini")
- # # load file
- # config = new ConfigTree("config2.ini")
- # assert config["foo.bar"] == "foobar"
- # assert config["boo.bar"] == "boobar"
- #
- # Example with both hierarchy and section:
- #
- # # init file
- # str = """
- # goo=goo
- # [foo]
- # bar.baz=foobarbaz
- # [goo.boo]
- # bar=gooboobar
- # baz.bar=gooboobazbar"""
- # str.write_to_file("config3.ini")
- # # load file
- # config = new ConfigTree("config3.ini")
- # assert config["goo"] == "goo"
- # assert config["foo.bar.baz"] == "foobarbaz"
- # assert config["goo.boo.bar"] == "gooboobar"
- # assert config["goo.boo.baz.bar"] == "gooboobazbar"
- #
- # Using the array notation
- #
- # str = """
- # foo[]=a
- # foo[]=b
- # foo[]=c"""
- # str.write_to_file("config4.ini")
- # # load file
- # config = new ConfigTree("config4.ini")
- # print config.to_map.join(":", ",")
- # assert config["foo.0"] == "a"
- # assert config["foo.1"] == "b"
- # assert config["foo.2"] == "c"
- # assert config.at("foo").values.join(",") == "a,b,c"
- fun load do
- roots.clear
- var stream = new FileReader.open(ini_file)
- var path: nullable String = null
- var line_number = 0
+ # Read INI content from `string`
+ #
+ # ~~~
+ # var ini = new IniFile
+ # ini.load_string("""
+ # section1.key1=value1
+ # section1.key2=value2
+ # [section2]
+ # key=value3
+ # """)
+ # assert ini["section1.key1"] == "value1"
+ # assert ini["section1.key2"] == "value2"
+ # assert ini["section2.key"] == "value3"
+ # ~~~
+ #
+ # Returns `true` if the parsing finished correctly.
+ #
+ # See also `stop_on_first_error` and `errors`.
+ fun load_string(string: String): Bool do
+ var stream = new StringReader(string)
+ var last_section = null
+ var was_error = false
+ var i = 0
while not stream.eof do
- var line = stream.read_line
- line_number += 1
+ i += 1
+ var line = stream.read_line.trim
if line.is_empty then
continue
else if line.has_prefix(";") then
continue
+ else if line.has_prefix("#") then
+ continue
else if line.has_prefix("[") then
- line = line.trim
- var key = line.substring(1, line.length - 2)
- path = key
- set_node(path, null)
+ var section = new IniSection(line.substring(1, line.length - 2).trim)
+ sections.add section
+ last_section = section
+ continue
else
var parts = line.split_once_on("=")
- if parts.length == 1 then
+ if parts.length != 2 then
+ # FIXME silent skip?
+ # we definitely need exceptions...
+ was_error = true
+ errors.add new IniError("Unexpected string `{line}` at line {i}.")
+ if stop_on_first_error then return was_error
continue
end
var key = parts[0].trim
- var val = parts[1].trim
- if path != null then key = "{path}.{key}"
- if key.has_suffix("[]") then
- set_array(key, val)
+ var value = parts[1].trim
+
+ if last_section != null then
+ last_section[key] = value
else
- set_node(key,val)
+ self[key] = value
end
end
end
stream.close
+ return was_error
end
- # Save config to file
- fun save do write_to_file(ini_file)
-
- private var roots = new Array[ConfigNode]
-
- # Append `value` to array at `key`
- private fun set_array(key: String, value: nullable String) do
- key = key.substring(0, key.length - 2)
- var len = 0
- var node = get_node(key)
- if node != null then len = node.children.length
- set_node("{key}.{len.to_s}", value)
- end
-
- private fun set_node(key: String, value: nullable String) do
- var parts = key.split(".").reversed
- var k = parts.pop
- var root = get_root(k)
- if root == null then
- root = new ConfigNode(k)
- if parts.is_empty then
- root.value = value
- end
- roots.add root
- end
- while not parts.is_empty do
- k = parts.pop
- var node = root.get_child(k)
- if node == null then
- node = new ConfigNode(k)
- node.parent = root
- root.children[node.name] = node
- end
- if parts.is_empty then
- node.value = value
- end
- root = node
- end
- end
-
- private fun get_node(key: String): nullable ConfigNode do
- var parts = key.split(".").reversed
- var node = get_root(parts.pop)
- while node != null and not parts.is_empty do
- node = node.get_child(parts.pop)
- end
- return node
- end
+ # Load a `file` content as INI
+ #
+ # New properties will be appended to the `self`, existing properties will be
+ # overwrote by the values contained in `file`.
+ #
+ # ~~~
+ # var ini = new IniFile
+ # ini["key1"] = "value1"
+ # ini["key2"] = "value2"
+ #
+ # """
+ # key2=changed
+ # key3=added
+ # """.write_to_file("load_config.ini")
+ #
+ # ini.load_file("load_config.ini")
+ # assert ini["key1"] == "value1"
+ # assert ini["key2"] == "changed"
+ # assert ini["key3"] == "added"
+ #
+ # "load_config.ini".to_path.delete
+ # ~~~
+ #
+ # The process fails silently if the file does not exist.
+ #
+ # ~~~
+ # ini = new IniFile
+ # ini.load_file("ini_not_found.ini")
+ # assert ini.is_empty
+ # ~~~
+ #
+ # Returns `true` if the parsing finished correctly.
+ #
+ # See also `stop_on_first_error` and `errors`.
+ fun load_file(file: String): Bool do return load_string(file.to_path.read_all)
- private fun get_root(name: String): nullable ConfigNode do
- for root in roots do
- if root.name == name then return root
- end
- return null
- end
+ # Stop parsing on the first error
+ #
+ # By default, `load_string` will skip unparsable properties so the string can
+ # be loaded.
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key1=value1
+ # key2
+ # key3=value3
+ # """)
+ #
+ # assert ini.length == 2
+ # assert ini["key1"] == "value1"
+ # assert ini["key2"] == null
+ # assert ini["key3"] == "value3"
+ # ~~~
+ #
+ # Set `stop_on_first_error` to `true` to force the parsing to stop.
+ #
+ # ~~~
+ # ini = new IniFile
+ # ini.stop_on_first_error = true
+ # ini.load_string("""
+ # key1=value1
+ # key2
+ # key3=value3
+ # """)
+ #
+ # assert ini.length == 1
+ # assert ini["key1"] == "value1"
+ # assert ini["key2"] == null
+ # assert ini["key3"] == null
+ # ~~~
+ #
+ # See also `errors`.
+ var stop_on_first_error = false is optional, writable
- private fun leaves: Array[ConfigNode] do
- var res = new Array[ConfigNode]
- var todo = new Array[ConfigNode]
- todo.add_all roots
- while not todo.is_empty do
- var node = todo.pop
- if node.children.is_empty then
- res.add node
- else
- todo.add_all node.children.values
- end
- end
- return res
- end
+ # Errors found during parsing
+ #
+ # Wathever the value of `stop_on_first_error`, errors from parsing a string
+ # or a file are logged into `errors`.
+ #
+ # ~~~
+ # var ini = new IniFile.from_string("""
+ # key1=value1
+ # key2
+ # key3=value3
+ # """)
+ #
+ # assert ini.errors.length == 1
+ # assert ini.errors.first.message == "Unexpected string `key2` at line 2."
+ # ~~~
+ #
+ # `errors` is not cleared between two parsing:
+ #
+ # ~~~
+ # ini.load_string("""
+ # key4
+ # key5=value5
+ # """)
+ #
+ # assert ini.errors.length == 2
+ # assert ini.errors.last.message == "Unexpected string `key4` at line 1."
+ # ~~~
+ #
+ # See also `stop_on_first_error`.
+ var errors = new Array[IniError]
end
-private class ConfigNode
-
- var parent: nullable ConfigNode = null
- var children = new HashMap[String, ConfigNode]
- var name: String is writable
- var value: nullable String = null
+# A section in a IniFile
+#
+# Section properties values are strings associated keys.
+# Sections cannot be nested.
+#
+# ~~~
+# var section = new IniSection("section")
+# section["key1"] = "value1"
+# section["key2"] = "value2"
+#
+# assert section.length == 2
+# assert section["key1"] == "value1"
+# assert section["not.found"] == null
+# assert section.join(", ", ": ") == "key1: value1, key2: value2"
+#
+# var i = 0
+# for key, value in section do
+# assert key.has_prefix("key")
+# assert value.has_prefix("value")
+# i += 1
+# end
+# assert i == 2
+# ~~~
+class IniSection
+ super HashMap[String, nullable String]
- fun key: String do
- var parent = self.parent
- if parent == null then
- return name
- end
- return "{parent.key}.{name}"
- end
+ # Section name
+ var name: String
- fun path: Array[String] do
- var parent = self.parent
- if parent == null then
- return [name]
- end
- var res = new Array[String].from(parent.path)
- res.add name
- return res
+ # Get the value associated with `key`
+ #
+ # Returns `null` if the `key` is not found.
+ #
+ # ~~~
+ # var section = new IniSection("section")
+ # section["key"] = "value1"
+ # section["sub.key"] = "value2"
+ #
+ # assert section["key"] == "value1"
+ # assert section["sub.key"] == "value2"
+ # assert section["not.found"] == null
+ # ~~~
+ redef fun [](key) do
+ if not has_key(key) then return null
+ return super
end
+end
- fun get_child(name: String): nullable ConfigNode do
- if children.has_key(name) then
- return children[name]
- end
- return null
- end
+# Error for `IniFile` parsing
+class IniError
+ super Error
end