功能说明
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());
}