///|
/// INI file value type
pub(all) enum IniValue {
String(String)
Int(Int)
Double(Double)
Bool(Bool)
} derive(Show, Eq)
///|
/// INI file structure: sections map to key-value pairs
pub struct IniFile {
sections : Map[String, Map[String, IniValue]]
} derive(Show)
///|
/// Create a new empty INI file
pub fn IniFile::new() -> IniFile {
{ sections: {} }
}
///|
/// Get value from a section
pub fn IniFile::get(
self : IniFile,
section : String,
key : String,
) -> IniValue? {
match self.sections.get(section) {
Some(section_map) => section_map.get(key)
None => None
}
}
///|
/// Get string value from a section
pub fn IniFile::get_string(
self : IniFile,
section : String,
key : String,
) -> String? {
match self.get(section, key) {
Some(String(s)) => Some(s)
_ => None
}
}
///|
/// Get int value from a section
pub fn IniFile::get_int(self : IniFile, section : String, key : String) -> Int? {
match self.get(section, key) {
Some(Int(i)) => Some(i)
_ => None
}
}
///|
/// Get double value from a section
pub fn IniFile::get_double(
self : IniFile,
section : String,
key : String,
) -> Double? {
match self.get(section, key) {
Some(Double(d)) => Some(d)
_ => None
}
}
///|
/// Get bool value from a section
pub fn IniFile::get_bool(
self : IniFile,
section : String,
key : String,
) -> Bool? {
match self.get(section, key) {
Some(Bool(b)) => Some(b)
_ => None
}
}
///|
/// Set a value in a section
pub fn IniFile::set(
self : IniFile,
section : String,
key : String,
value : IniValue,
) -> Unit {
match self.sections.get(section) {
Some(section_map) => section_map[key] = value
None => {
let section_map : Map[String, IniValue] = {}
section_map[key] = value
self.sections[section] = section_map
}
}
}
///|
/// Check if a section exists
pub fn IniFile::has_section(self : IniFile, section : String) -> Bool {
self.sections.contains(section)
}
///|
/// Get all section names
pub fn IniFile::sections(self : IniFile) -> Array[String] {
let result : Array[String] = []
for section, _ in self.sections {
result.push(section)
}
result
}
///|
/// Get all keys in a section
pub fn IniFile::keys(self : IniFile, section : String) -> Array[String] {
match self.sections.get(section) {
Some(section_map) => {
let result : Array[String] = []
for key, _ in section_map {
result.push(key)
}
result
}
None => []
}
}
///|
/// Parse error type
pub(all) suberror ParseError {
InvalidSection(Int, String)
InvalidKeyValue(Int, String)
EmptyKey(Int)
} derive(Show, Eq)
///|
/// Parse an INI file from a string
pub fn parse(input : String) -> IniFile raise ParseError {
let ini = IniFile::new()
let mut current_section = ""
let mut line_number = 0
for line_view in input.split("\n") {
line_number += 1
// Convert to string for easier processing
let line = line_view.to_string()
let trimmed = line.trim_space().to_string()
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.has_prefix(";") || trimmed.has_prefix("#") {
continue
}
// Parse section header [section]
if trimmed.has_prefix("[") {
if not(trimmed.has_suffix("]")) {
raise ParseError::InvalidSection(line_number, line)
}
let section_name = try! trimmed[1:trimmed.length() - 1]
.trim_space()
.to_string()
if section_name.is_empty() {
raise ParseError::InvalidSection(line_number, line)
}
current_section = section_name
if not(ini.has_section(current_section)) {
ini.sections[current_section] = {}
}
continue
}
// Parse key=value
match trimmed.find("=") {
Some(eq_pos) => {
let key = try! trimmed[0:eq_pos].trim_space().to_string()
let value_str = try! trimmed[eq_pos + 1:trimmed.length()]
.trim_space()
.to_string()
if key.is_empty() {
raise ParseError::EmptyKey(line_number)
}
let value = parse_value(value_str)
ini.set(current_section, key, value)
}
None => raise ParseError::InvalidKeyValue(line_number, line)
}
}
ini
}
///|
/// Parse a value string into an IniValue
fn parse_value(s : String) -> IniValue {
let trimmed = s.trim_space().to_string()
// Try to parse as boolean
let lower = trimmed.to_lower()
if lower == "true" || lower == "yes" || lower == "on" || lower == "1" {
return Bool(true)
}
if lower == "false" || lower == "no" || lower == "off" || lower == "0" {
return Bool(false)
}
// Try to parse as integer
let int_result = try? @strconv.parse_int(trimmed)
if int_result is Ok(i) {
return Int(i)
}
// Try to parse as double
let double_result = try? @strconv.parse_double(trimmed)
if double_result is Ok(d) {
return Double(d)
}
// Default to string
// Remove quotes if present
if (trimmed.has_prefix("\"") && trimmed.has_suffix("\"")) ||
(trimmed.has_prefix("'") && trimmed.has_suffix("'")) {
String((try! trimmed[1:trimmed.length() - 1]).to_string())
} else {
String(trimmed)
}
}
///|
/// Convert IniFile to string format
pub fn IniFile::to_string(self : IniFile) -> String {
let buffer = @buffer.new()
let mut first_section = true
for section, pairs in self.sections {
if not(first_section) {
buffer.write_char('\n')
}
first_section = false
buffer.write_char('[')
buffer.write_string(section)
buffer.write_string("]\n")
for key, value in pairs {
buffer.write_string(key)
buffer.write_string(" = ")
match value {
String(s) =>
// Quote strings that contain special characters
if s.contains(" ") ||
s.contains("=") ||
s.contains(";") ||
s.contains("#") {
buffer.write_char('"')
buffer.write_string(s)
buffer.write_char('"')
} else {
buffer.write_string(s)
}
Int(i) => buffer.write_string(i.to_string())
Double(d) => buffer.write_string(d.to_string())
Bool(b) => buffer.write_string(if b { "true" } else { "false" })
}
buffer.write_char('\n')
}
}
buffer.to_string()
}