--- /dev/null
+# `ini` - Read and write INI configuration files
+
+[INI files](https://en.wikipedia.org/wiki/INI_file) are simple text files with
+a basic structure composed of sections, properties and values used to store
+configuration parameters.
+
+Here's an example from the `package.ini` of this package:
+
+~~~
+import ini
+
+var package_ini = """
+[package]
+name=ini
+desc=Read and write INI configuration files.
+[upstream]
+git=https://github.com/nitlang/nit.git
+git.directory=lib/ini/
+"""
+~~~
+
+## Basic usage
+
+`IniFile` is used to parse INI strings and access their content:
+
+~~~
+var ini = new IniFile.from_string(package_ini)
+assert ini["package.name"] == "ini"
+assert ini["upstream.git.directory"] == "lib/ini/"
+assert ini["unknown.unknown"] == null
+~~~
+
+`IniFile` can also load INI configuration from a file:
+
+~~~
+package_ini.write_to_file("my_package.ini")
+
+ini = new IniFile.from_file("my_package.ini")
+assert ini["package.name"] == "ini"
+assert ini["upstream.git.directory"] == "lib/ini/"
+
+"my_package.ini".to_path.delete
+~~~
+
+INI content can be added or edited through the `IniFile` API then written to
+a stream or a file.
+
+~~~
+ini["package.name"] = "new name"
+ini["upstream.git.directory"] = "/dev/null"
+ini["section.key"] = "value"
+
+var stream = new StringWriter
+ini.write_to(stream)
+
+assert stream.to_s == """
+[package]
+name=new name
+desc=Read and write INI configuration files.
+[upstream]
+git=https://github.com/nitlang/nit.git
+git.directory=/dev/null
+[section]
+key=value
+"""
+~~~
+
+## INI content
+
+### Properties
+
+Properties are the basic element of the INI format.
+Every property correspond to a *key* associated to a *value* thanks to the equal (`=`) sign.
+
+~~~
+ini = new IniFile.from_string("""
+key1=value1
+key2=value2
+""")
+assert ini["key1"] == "value1"
+assert ini["key2"] == "value2"
+assert ini.length == 2
+~~~
+
+Accessing an unknown property returns `null`:
+
+~~~
+assert ini["unknown"] == null
+~~~
+
+Properties can be iterated over:
+
+~~~
+var i = 1
+for key, value in ini do
+ assert key == "key{i}"
+ assert value == "value{i}"
+ i += 1
+end
+~~~
+
+Property keys cannot contain the character `=`.
+Values can contain any character.
+Spaces are trimmed.
+
+~~~
+ini = new IniFile.from_string("""
+prop=erty1=value1
+ property2 = value2
+property3=value3 ; with semicolon
+""")
+assert ini[";property1"] == null
+assert ini["prop=erty1"] == null
+assert ini["prop"] == "erty1=value1"
+assert ini["property2"] == "value2"
+assert ini[" property2 "] == "value2"
+assert ini["property3"] == "value3 ; with semicolon"
+~~~
+
+Both keys and values are case sensitive.
+
+~~~
+ini = new IniFile.from_string("""
+Property1=value1
+property2=Value2
+""")
+assert ini["property1"] == null
+assert ini["Property1"] == "value1"
+assert ini["property2"] != "value2"
+assert ini["property2"] == "Value2"
+~~~
+
+### Sections
+
+Properties may be grouped into arbitrary sections.
+The section name appears on a line by itself between square brackets (`[` and `]`).
+
+All keys after the section declaration are associated with that section.
+The is no explicit "end of section" delimiter; sections end at the next section
+declaration or the end of the file.
+Sections cannot be nested.
+
+~~~
+var content = """
+key1=value1
+key2=value2
+[section1]
+key1=value3
+key2=value4
+[section2]
+key1=value5
+"""
+
+ini = new IniFile.from_string(content)
+assert ini["key1"] == "value1"
+assert ini["unknown"] == null
+assert ini["section1.key1"] == "value3"
+assert ini["section1.unknown"] == null
+assert ini["section2.key1"] == "value5"
+~~~
+
+Sections can be iterated over:
+
+~~~
+i = 1
+for section in ini.sections do
+ assert section.name == "section{i}"
+ assert section["key1"].has_prefix("value")
+ i += 1
+end
+~~~
+
+When iterating over a file properties, only properties at root are returned.
+`flatten` can be used to iterate over all properties including the one from
+sections.
+
+~~~
+assert ini.join(", ", ": ") == "key1: value1, key2: value2"
+assert ini.flatten.join(", ", ": ") ==
+ "key1: value1, key2: value2, section1.key1: value3, section1.key2: value4, section2.key1: value5"
+
+i = 0
+for key, value in ini do
+ i += 1
+ assert key == "key{i}" and value == "value{i}"
+end
+assert i == 2
+
+~~~
+
+Sections name may contain any character including brackets (`[` and `]`).
+Spaces are trimmed.
+
+~~~
+ini = new IniFile.from_string("""
+[[section1]]
+key=value1
+[ section 2 ]
+key=value2
+[section1.section3]
+key=value3
+""")
+assert ini.sections.length == 3
+assert ini["[section1].key"] == "value1"
+assert ini["section 2.key"] == "value2"
+assert ini["section1.section3.key"] == "value3"
+assert ini.sections.last.name == "section1.section3"
+~~~
+
+The dot `.` notation is used to create new sections with `[]=`.
+Unknown sections will be created on the fly.
+
+~~~
+ini = new IniFile
+ini["key"] = "value1"
+ini["section1.key"] = "value2"
+ini["section2.key"] = "value3"
+
+stream = new StringWriter
+ini.write_to(stream)
+assert stream.to_s == """
+key=value1
+[section1]
+key=value2
+[section2]
+key=value3
+"""
+~~~
+
+Sections can also be created manually:
+
+~~~
+ini = new IniFile
+ini["key"] = "value1"
+
+var section = new IniSection("section1")
+section["key"] = "value2"
+ini.sections.add section
+
+stream = new StringWriter
+ini.write_to(stream)
+assert stream.to_s == """
+key=value1
+[section1]
+key=value2
+"""
+~~~
+
+### Comments
+
+Comments are indicated by semicolon (`;`) or a number sign (`#`) at the begining
+of the line. Commented lines are ignored as well as empty lines.
+
+~~~
+ini = new IniFile.from_string("""
+; This is a comment.
+; property1=value1
+
+# This is another comment.
+# property2=value2
+""")
+assert ini.is_empty
+~~~
+
+### Unicode support
+
+INI files support Unicode:
+
+~~~
+ini = new IniFile.from_string("""
+property❤=héhé
+""")
+assert ini["property❤"] == "héhé"
+~~~
+
+## Error handling
+
+By default `IniFile` does not stop when it cannot parse a line in a string (loaded
+by `from_string` or `load_string`) or a file (loaded by `from_file` or `load_file`).
+
+~~~
+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"
+~~~
+
+
+This behaviour can be modified by setting `stop_on_first_error` to `true`.
+
+~~~
+ini = new IniFile.from_string("""
+key1=value1
+key2
+key3=value3
+""", stop_on_first_error = true)
+
+assert ini.length == 1
+assert ini["key1"] == "value1"
+assert ini["key2"] == null
+assert ini["key3"] == null
+~~~
+
+Wathever the value set for `stop_on_first_error`, errors can be checked thanks
+to the `errors` array:
+
+~~~
+assert ini.errors.length == 1
+assert ini.errors.first.message == "Unexpected string `key2` at line 2."
+~~~
# 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
var ini_path = ini_path
if ini_path == null then return
- var ini = new ConfigTree(ini_path)
+ var ini = new IniFile.from_file(ini_path)
- ini.check_key(toolcontext, self, "package.name", name)
- ini.check_key(toolcontext, self, "package.desc")
- ini.check_key(toolcontext, self, "package.tags")
+ ini.check_key(ini_path, toolcontext, self, "package.name", name)
+ ini.check_key(ini_path, toolcontext, self, "package.desc")
+ ini.check_key(ini_path, toolcontext, self, "package.tags")
# FIXME since `git reflog --follow` seems bugged
- ini.check_key(toolcontext, self, "package.maintainer")
+ ini.check_key(ini_path, toolcontext, self, "package.maintainer")
# var maint = mpackage.maintainer
# if maint != null then
# ini.check_key(toolcontext, self, "package.maintainer", maint)
# ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
# end
- ini.check_key(toolcontext, self, "package.license", license)
- ini.check_key(toolcontext, self, "upstream.browse", browse_url)
- ini.check_key(toolcontext, self, "upstream.git", git_url)
- ini.check_key(toolcontext, self, "upstream.git.directory", git_dir)
- ini.check_key(toolcontext, self, "upstream.homepage", homepage_url)
- ini.check_key(toolcontext, self, "upstream.issues", issues_url)
+ ini.check_key(ini_path, toolcontext, self, "package.license", license)
+ ini.check_key(ini_path, toolcontext, self, "upstream.browse", browse_url)
+ ini.check_key(ini_path, toolcontext, self, "upstream.git", git_url)
+ ini.check_key(ini_path, toolcontext, self, "upstream.git.directory", git_dir)
+ ini.check_key(ini_path, toolcontext, self, "upstream.homepage", homepage_url)
+ ini.check_key(ini_path, toolcontext, self, "upstream.issues", issues_url)
- for key in ini.to_map.keys do
+ for key in ini.flatten.keys do
if not allowed_ini_keys.has(key) then
toolcontext.warning(location, "unknown-ini-key",
- "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
+ "Warning: ignoring unknown `{key}` key in `{ini_path}`")
end
end
end
private fun gen_ini: String do
var ini_path = self.ini_path.as(not null)
- var ini = new ConfigTree(ini_path)
+ var ini = new IniFile.from_file(ini_path)
ini.update_value("package.name", name)
ini.update_value("package.desc", "")
ini.update_value("upstream.homepage", homepage_url)
ini.update_value("upstream.issues", issues_url)
- ini.save
+ ini.write_to_file(ini_path)
return ini_path
end
end
end
-redef class ConfigTree
- private fun check_key(toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
+redef class IniFile
+ private fun check_key(ini_file: String, toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
if not has_key(key) then
toolcontext.warning(mpackage.location, "missing-ini-key",
"Warning: missing `{key}` key in `{ini_file}`")