Merge: Follow the INI specification
authorJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:48 +0000 (14:50 -0400)
committerJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:48 +0000 (14:50 -0400)
commit3e634a74ffbeb9d67045c8f793379bfdcf721224
tree1b8234ef2b8ae6b3691f04d4a1b1fdc442cc1c2c
parent18c9dd595dec255b7f01cb11f8ec8266cc77c1a7
parentf30e568ebb87a6c9c8d770d291cad62d881d45e7
Merge: Follow the INI specification

This version follows more closely the INI specification (https://en.wikipedia.org/wiki/INI_file) and adds some improvements to the API.

Spec changes:
* Allow `#` and `;` for comments
* No more sections nesting as by the spec (which actually change nothing see below)

API changes:
* Renaming `ConfigTree` -> `IniFile` (as it's no more a "tree")
* No more coupling with a file path, use utility methods `load_string`, `load_file`, `write_to`  instead
* Ability to iterate keys, values, sections and section content
* Ability to create `IniSection` by hand
* `IniFile` and `IniSection` implements `Map[String, nullable String]`

The biggest change is that sub-sections are now flattened.

Before, with the following ini:

~~~ini
[section1]
key1=value1
[section1.section2]
key2=value2
~~~

You would get this hierarchy:

~~~
section1
  |   key1=value1
  `- section2
      `-  key2=value2
~~~

Now you get:

~~~
section1
  `- key1=value1
section1.section2
  `-  key2=value2
~~~

Two independent (unnested) sections, one called `section1` and the other called `section1.section2`.
This actually change nothing if the client was using the `[]` operator with the `.` notation as:

~~~
ini["section1.section2.key2"] # still returns `value2`
~~~

Documentation and specification from the README:

# `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:

~~~nit
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:

~~~nit
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:

~~~nit
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.

~~~nit
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.

~~~nit
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`:

~~~nit
assert ini["unknown"] == null
~~~

Properties can be iterated over:

~~~nit
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.

~~~nit
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.

~~~nit
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.

~~~nit
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:

~~~nit
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.

~~~nit
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.

~~~nit
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.

~~~nit
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:

~~~nit
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.

~~~nit
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:

~~~nit
ini = new IniFile.from_string("""
property❤=héhé
""")
assert ini["property❤"] == "héhé"
~~~

Pull-Request: #2752
Reviewed-by: Jean Privat <jean@pryen.org>
lib/github/loader.nit