← 中文资源

简体中文版本

INI 配置文件解析

把 INI 的节与键值对解析成有序映射,去掉值两端的引号,并拒绝畸形节头和空键。

阅读英文原文

功能说明

INI 是许多传统服务和桌面程序使用的配置格式,结构简单但边界情况不少:节头必须成对出现、键值以第一个等号分隔、注释可以出现在行首、值可能带引号。这个解析器把内容读成两层映射,外层是节名,内层是键值,二者都按字典序排列,输出稳定便于快照测试。

没有写节头的键归入 default 节。查询接口返回 Option,调用方可以区分缺失与空值。遇到未闭合的节头、空节名、缺少等号或空键时,错误信息会带上行号,方便定位。

关键代码

解析前先插入 default 节,保证默认键有归属。逐行 trim 后跳过空行和两种注释前缀;以左方括号开头的行必须是完整节头,取中间部分作为节名并确保该节存在;其余行用 split_once 在第一个等号处切分,键不能为空,值去除首尾空白后再去掉两端双引号。BTreeMap 的 entry 与 or_default 让首次出现的节自动创建。

使用注意

值两端的引号会被全部去掉,若内容本身以引号开头结尾需要转义或改用其他格式。不支持行内注释,井号出现在值中间会原样保留。重复键以后出现的为准。节名大小写敏感。该解析器不支持多行值和变量插值,复杂配置建议使用 TOML 或 YAML。读取不可信文件时要限制大小,避免超长行占用内存。

验证方式

示例文档解析后,server 节的 host 为 0.0.0.0,database 节的 pool_size 为十,查询不存在的键返回 None。传入缺少右方括号的文档会返回错误,打印为 true。可以再加入重复键与空值,确认后者被解析为空字符串而不是被丢弃。

代码示例

use std::collections::BTreeMap;

#[derive(Debug, Default)]
struct IniFile {
    sections: BTreeMap<String, BTreeMap<String, String>>,
}

impl IniFile {
    fn get(&self, section: &str, key: &str) -> Option<&str> {
        self.sections.get(section)?.get(key).map(String::as_str)
    }
}

fn parse_ini(input: &str) -> Result<IniFile, String> {
    let mut ini = IniFile::default();
    let mut section = String::from("default");
    ini.sections.insert(section.clone(), BTreeMap::new());

    for (index, raw_line) in input.lines().enumerate() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
            continue;
        }
        if line.starts_with('[') {
            if !line.ends_with(']') {
                return Err(format!("line {}: unclosed section header", index + 1));
            }
            let name = line[1..line.len() - 1].trim();
            if name.is_empty() {
                return Err(format!("line {}: empty section name", index + 1));
            }
            section = name.to_string();
            ini.sections.entry(section.clone()).or_default();
            continue;
        }
        let (key, value) = match line.split_once('=') {
            Some(pair) => pair,
            None => return Err(format!("line {}: expected key=value", index + 1)),
        };
        let key = key.trim();
        if key.is_empty() {
            return Err(format!("line {}: empty key", index + 1));
        }
        ini.sections
            .entry(section.clone())
            .or_default()
            .insert(key.to_string(), value.trim().trim_matches('"').to_string());
    }
    Ok(ini)
}

fn main() {
    let document = r#"
; application settings
[server]
host = "0.0.0.0"
port = 8080

[database]
url = postgres://localhost/app
pool_size = 10
"#;
    let ini = parse_ini(document).expect("valid ini");
    println!("host: {:?}", ini.get("server", "host"));
    println!("pool_size: {:?}", ini.get("database", "pool_size"));
    println!("missing: {:?}", ini.get("server", "missing"));
    println!("error: {:?}", parse_ini("[broken\nkey=1").is_err());
}