INI Parser
Parse INI-style configuration files, a simple text-based format for configuration with sections and key-value pairs.
Key Concepts
- key-value pairs
- sections
- comments
- string-only values
- global vs sectioned values
Why Parse This Format?
INI files are one of the simplest configuration formats, widely used in Windows applications and many cross-platform tools. They're easy for humans to read and edit.
Use Cases
- Application configuration files
- Game settings and preferences
- Legacy Windows application config
- Simple tool configuration
- Environment-specific settings
Input Format
INI files have sections in square brackets and key=value pairs. Comments start with ; or #. All values are strings (no type inference).
[section]
key = value
; commentExamples
[database]
host = localhost
port = 5432VALIDSection with key-value pairs
# Global setting
debug = true
[server]
host = 0.0.0.0VALIDGlobal values before first section
[section.subsection]
key = valueVALIDDotted section names (hierarchical)
; This is a comment
[settings]
# Also a commentVALIDComments with ; or #
Output Format
The parser returns an IniConfig struct with global key-values and a map of sections, each containing their own key-value pairs.
Structure
globalHashMap<String, String> for key-values before any section
sectionsHashMap<String, HashMap<String, String>> for sectioned values
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Key-Value Pair Parsing
.rule("key", re("[a-zA-Z_][a-zA-Z0-9_]*"))
.rule("value", re("[^\\n]+"))
.rule(
"pair",
seq(vec![
dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")), // key
dynamic(re("[ \t]*=[ \t]*")), // =
dynamic(re("[^\\n]+")), // value
]),
)INI entries are `key = value` format: Whitespace around `=` is optional.
Section Header Parsing
.rule("section_name", re("[a-zA-Z_][a-zA-Z0-9_.]*"))
.rule(
"section",
seq(vec![
dynamic(str("[")),
dynamic(re("[a-zA-Z_][a-zA-Z0-9_.]*")),
dynamic(str("]")),
]),
)Sections group key-value pairs: Dots allow dotted section names like `[database.mysql]`.
Comment Handling
.rule("comment", re("[;#][^\\n]*"))Comments start with `;` or `#`: Comments extend to end of line and are ignored.
Line Processing
pub fn parse_ini(input: &str) -> Result<IniConfig, String> {
let mut config = IniConfig::default();
let mut current_section: Option<String> = None;
for line in input.lines() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
// Section header
if line.starts_with('[') && line.ends_with(']') {
current_section = Some(line[1..line.len() - 1].to_string());
continue;
}
// Key-value pair
if let Some((key, value)) = line.split_once('=') {
match ¤t_section {
Some(section) => config.sections.entry(section.clone()).or_default().insert(key, value),
None => config.global.insert(key, value),
};
}
}
Ok(config)
}INI files are processed line by line:
Line Processing
pub fn parse_ini(input: &str) -> Result<IniConfig, String> {
let mut config = IniConfig::default();
let mut current_section: Option<String> = None;
for line in input.lines() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
// Section header
if line.starts_with('[') && line.ends_with(']') {
current_section = Some(line[1..line.len() - 1].to_string());
continue;
}
// Key-value pair
if let Some((key, value)) = line.split_once('=') {
match ¤t_section {
Some(section) => config.sections.entry(section.clone()).or_default().insert(key, value),
None => config.global.insert(key, value),
};
}
}
Ok(config)
}INI files are processed line by line:
Source Code
//! INI Configuration Parser Example
//!
//! This example demonstrates parsing INI-style configuration files.
//! Shows handling of sections, key-value pairs, and comments.
//!
//! Run with: cargo run --example ini_parser --no-default-features
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder},
AstArena, Grammar, PortableParser,
};
use std::collections::HashMap;
/// Build an INI grammar
fn build_ini_grammar() -> Grammar {
GrammarBuilder::new()
// Whitespace
.rule("space", re("[ \t]+"))
// Comment: ; or # to end of line
.rule("comment", re("[;#][^\\n]*"))
// Key: alphanumeric with underscores
.rule("key", re("[a-zA-Z_][a-zA-Z0-9_]*"))
// Value: anything until end of line (trimmed)
.rule("value", re("[^\\n]+"))
// Key-value pair: key = value
.rule(
"pair",
seq(vec![
dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")), // key
dynamic(re("[ \t]*=[ \t]*")), // =
dynamic(re("[^\\n]+")), // value
]),
)
// Section: [name]
.rule("section_name", re("[a-zA-Z_][a-zA-Z0-9_.]*"))
.rule(
"section",
seq(vec![
dynamic(str("[")),
dynamic(re("[a-zA-Z_][a-zA-Z0-9_.]*")),
dynamic(str("]")),
]),
)
// Line: pair, section, comment, or blank
.rule(
"line",
choice(vec![
dynamic(seq(vec![
dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(re("[ \t]*=[ \t]*")),
dynamic(re("[^\\n]+")),
])),
dynamic(seq(vec![
dynamic(str("[")),
dynamic(re("[a-zA-Z_][a-zA-Z0-9_.]*")),
dynamic(str("]")),
])),
dynamic(re("[;#][^\\n]*")), // comment
dynamic(re("[ \t]*")), // blank
]),
)
// INI file: multiple lines
.rule("ini", re("([^\\n]*\\n)*[^\\n]*"))
.build()
}
/// Parsed INI configuration
#[derive(Debug, Clone, Default)]
pub struct IniConfig {
/// Global key-value pairs (before any section)
pub global: HashMap<String, String>,
/// Sections with their key-value pairs
pub sections: HashMap<String, HashMap<String, String>>,
}
/// Parse an INI configuration string
pub fn parse_ini(input: &str) -> Result<IniConfig, String> {
let grammar = build_ini_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let _ast = parser
.parse()
.map_err(|e| format!("Parse error: {:?}", e))?;
// Manual parsing for clarity
let mut config = IniConfig::default();
let mut current_section: Option<String> = None;
for line in input.lines() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
// Section header
if line.starts_with('[') && line.ends_with(']') {
current_section = Some(line[1..line.len() - 1].to_string());
config
.sections
.entry(current_section.clone().unwrap())
.or_insert_with(HashMap::new);
continue;
}
// Key-value pair
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_string();
let value = value.trim().to_string();
match ¤t_section {
Some(section) => {
config
.sections
.entry(section.clone())
.or_insert_with(HashMap::new)
.insert(key, value);
}
None => {
config.global.insert(key, value);
}
}
}
}
Ok(config)
}
fn main() {
println!("INI Configuration Parser Example");
println!("=================================\n");
let ini_content = r#"
# Database configuration
[database]
host = localhost
port = 5432
name = myapp_db
user = admin
; Server settings
[server]
host = 0.0.0.0
port = 8080
debug = true
# Feature flags
[features]
enable_cache = true
max_connections = 100
"#;
println!("Input INI:");
println!("----------");
println!("{}\n", ini_content);
match parse_ini(ini_content) {
Ok(config) => {
println!("Parsed Configuration:");
println!("---------------------");
if !config.global.is_empty() {
println!("[global]");
for (key, value) in &config.global {
println!(" {} = {}", key, value);
}
}
for (section, pairs) in &config.sections {
println!("[{}]", section);
for (key, value) in pairs {
println!(" {} = {}", key, value);
}
}
}
Err(e) => println!("Error: {}", e),
}
// Example: accessing specific values
println!("\nAccessing specific values:");
if let Ok(config) = parse_ini(ini_content) {
if let Some(db) = config.sections.get("database") {
println!("Database host: {:?}", db.get("host"));
println!("Database port: {:?}", db.get("port"));
}
if let Some(server) = config.sections.get("server") {
println!("Server debug mode: {:?}", server.get("debug"));
}
}
}
# frozen_string_literal: true
# INI Parser Example - Basic Parsing
#
# This example demonstrates parsing INI configuration files.
# Shows section headers, key-value pairs, and comments.
#
# Run with: ruby -Ilib example/ini/basic.rb
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol"
# Step 1: Define the INI file grammar
class IniParser < Parsanol::Parser
root :ini
rule(:ini) do
(section | key_value | comment).repeat
end
# Section header: [section_name]
rule(:section) do
(space? >> str("[") >> section_name.as(:name) >> str("]") >> space? >> str("\n").maybe).as(:section)
end
rule(:section_name) do
match('[^]\n]').repeat(1)
end
# Key-Value pair: key = value
rule(:key_value) do
(space? >> key.as(:key) >> space? >> str("=") >> space? >> value.as(:value) >> space? >> str("\n").maybe).as(:kv)
end
rule(:key) do
match('[^\s=]').repeat(1)
end
rule(:value) do
match('[^\n]').repeat
end
# Comment: # or ; at start of line
rule(:comment) do
(space? >> (str("#") | str(";")) >> match('[^\n]').repeat >> str("\n").maybe).as(:comment)
end
rule(:space?) { match('\s').repeat }
end
# Step 2: INI data structures
class IniFile
attr_reader :sections
def initialize
@sections = {}
@current_section = nil
end
def set_section(name)
@current_section = name
@sections[name] ||= {}
end
def set_key_value(key, value)
section = @sections[@current_section] || {}
section[key.to_s.strip] = value.to_s.strip
@sections[@current_section] = section
end
def get(section, key = nil)
if key
@sections[section]&.[](key)
else
@sections[section] || {}
end
end
def to_h
@sections
end
end
# Step 3: Parse and transform
def parse_ini(input)
parser = IniParser.new
tree = parser.parse(input)
puts "Parse tree: #{tree.inspect[0..500]}..."
ini = IniFile.new
tree.each do |item|
case item
when Hash
if item[:section]
name = item[:section][:name].to_s.strip
ini.set_section(name)
puts " Section: [#{name}]"
elsif item[:kv]
key = item[:kv][:key].to_s
value = item[:kv][:value].to_s
ini.set_key_value(key, value)
puts " #{key} = #{value}"
elsif item[:comment]
# Skip comments
end
end
end
ini
end
# Example usage
if __FILE__ == $PROGRAM_NAME
puts "=" * 60
puts "INI Parser - Basic Parsing"
puts "=" * 60
puts
ini_content = <<~INI
# This is a comment
; This is also a comment
[database]
host = localhost
port = 5432
[server]
host = 0.0.0.0
port = 8080
debug = true
[cache]
enabled = true
ttl = 3600
INI
puts "Input:"
puts "-" * 40
puts ini_content
puts
puts "Parsed:"
puts "-" * 40
ini = parse_ini(ini_content)
puts
puts "=" * 60
puts "Accessing parsed data:"
puts "=" * 60
puts "database.host: #{ini.get('database', 'host')}"
puts "database.port: #{ini.get('database', 'port')}"
puts "server.debug: #{ini.get('server', 'debug')}"
end