Skip to content
← Back to Examples

INI Parser

RustRubydata formatsbeginner

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 ; comment

Examples

[database] host = localhost port = 5432VALID

Section with key-value pairs

# Global setting debug = true [server] host = 0.0.0.0VALID

Global values before first section

[section.subsection] key = valueVALID

Dotted section names (hierarchical)

; This is a comment [settings] # Also a commentVALID

Comments 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

global

HashMap<String, String> for key-values before any section

sections

HashMap<String, HashMap<String, String>> for sectioned values

Visual Overview

INI Parser diagram

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 &current_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 &current_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

Rust
//! 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 &current_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"));
        }
    }
}
Ruby
# 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