Read and write INI configuration files

Introduced classes

class IniError

ini :: IniError

Error for IniFile parsing
class IniFile

ini :: IniFile

Read and write INI configuration files
class IniSection

ini :: IniSection

A section in a IniFile

All class definitions

class IniError

ini $ IniError

Error for IniFile parsing
class IniFile

ini $ IniFile

Read and write INI configuration files
class IniSection

ini $ IniSection

A section in a IniFile
package_diagram ini::ini ini core core ini::ini->core config::config config config::config->ini::ini popcorn::pop_config pop_config popcorn::pop_config->config::config markdown::nitmd nitmd markdown::nitmd->config::config markdown2::nitmd nitmd markdown2::nitmd->config::config nlp::nlp_index nlp_index nlp::nlp_index->config::config nlp::nlp_server nlp_server nlp::nlp_server->config::config vsm::example_vsm example_vsm vsm::example_vsm->config::config popcorn::pop_config... ... popcorn::pop_config...->popcorn::pop_config markdown::nitmd... ... markdown::nitmd...->markdown::nitmd markdown2::nitmd... ... markdown2::nitmd...->markdown2::nitmd nlp::nlp_index... ... nlp::nlp_index...->nlp::nlp_index nlp::nlp_server... ... nlp::nlp_server...->nlp::nlp_server vsm::example_vsm... ... vsm::example_vsm...->vsm::example_vsm

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module array

core :: array

This module introduces the standard array structure.
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module native

core :: native

Native structures for text and bytes
module numeric

core :: numeric

Advanced services for Numeric types
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module ropes

core :: ropes

Tree-based representation of a String.
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.

Children

module config

config :: config

Configuration options for nit tools and apps

Descendants

module a_star-m

a_star-m

module example_vsm

vsm :: example_vsm

Example using a FileIndex
module loader

github :: loader

module nitmd

markdown :: nitmd

A Markdown parser for Nit.
module nitmd

markdown2 :: nitmd

A Markdown parser for Nit.
module nlp_index

nlp :: nlp_index

Example showing how to use a NLPFileIndex.
module pop_config

popcorn :: pop_config

Configuration file and options for Popcorn apps
module pop_repos

popcorn :: pop_repos

Repositories for data management.
# Read and write INI configuration files
module ini

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"
# ~~~
#
# `IniFile` can also be used to create new INI files from scratch, or edit
# existing ones through its API.
#
# ~~~
# # 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")
#
# # 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"
#
# "my_config.ini".to_path.delete
# ~~~
class IniFile
	super Writable
	super HashMap[String, nullable String]

	# 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

	# 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

	# Sections composing this IniFile
	#
	# ~~~
	# 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`
	#
	# Returns `null` if the section is not found.
	#
	# ~~~
	# 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 null
	end

	# 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

	# 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

		# 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

	# 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 key, value in self do
			if value == null then continue
			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

	# 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
		for key, value in self do
			if value == null then continue
			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

	# 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
			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
				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 != 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 value = parts[1].trim

				if last_section != null then
					last_section[key] = value
				else
					self[key] = value
				end
			end
		end
		stream.close
		return was_error
	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)

	# 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

	# 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

# 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]

	# Section name
	var name: String

	# 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

# Error for `IniFile` parsing
class IniError
	super Error
end
lib/ini/ini.nit:15,1--558,3