功能说明
该示例把 INI 文本解析成普通对象。分节名成为顶层键,分节内的键值对成为子对象;没有分节的键放在根对象上。它支持分号与井号注释、单双引号包裹的值,以及布尔、null、整数与浮点的自动类型转换。适合读取传统服务配置、CLI 配置文件或测试夹具。
关键代码
parseIni 按行遍历并 trim,跳过空行与注释。方括号行被识别为分节,使用 Object.create(null) 创建无原型对象,避免 proto 等键污染原型链。键值行按第一个等号拆分,键名重复或为空会抛错并带上行号。unquote 去掉成对的单双引号,coerce 再根据字面量转换成对应类型。typed 设为 false 时全部保留字符串。
使用注意
该解析器不处理反斜杠转义、多行值、变量插值或行尾注释,值中出现的井号会被当作内容。重复的分节或键会直接报错,而不是后者覆盖前者。所有对象都是无原型对象,直接调用 hasOwnProperty 会失败,应使用 Object.prototype.hasOwnProperty.call。返回结构可安全序列化为 JSON,但不要把它当作完整 INI 规范实现。
验证方式
解析包含全局键与 server 分节的文本,断言 name 为字符串 Demo App、debug 为布尔 false、retries 为数字 3、port 为数字 8080。测试引号去除与 typed 关闭后全部为字符串。构造重复键、重复分节、空分节名与缺少等号的行,分别断言抛错并包含行号。再验证 proto 键不会污染全局对象。
代码示例
export function parseIni(text, options = {}) {
if (typeof text !== "string") throw new TypeError("text must be a string");
const { typed = true } = options;
const result = Object.create(null);
let section = result;
const lines = text.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index].trim();
if (line === "" || line.startsWith(";") || line.startsWith("#")) continue;
if (line.startsWith("[") && line.endsWith("]")) {
const name = line.slice(1, -1).trim();
if (name === "") throw new Error(`empty section name on line ${index + 1}`);
if (Object.prototype.hasOwnProperty.call(result, name)) {
throw new Error(`duplicate section "${name}" on line ${index + 1}`);
}
section = Object.create(null);
result[name] = section;
continue;
}
const equals = line.indexOf("=");
if (equals <= 0) throw new Error(`invalid entry on line ${index + 1}`);
const key = line.slice(0, equals).trim();
let value = line.slice(equals + 1).trim();
if (key === "" || /[\[\]]/.test(key)) throw new Error(`invalid key on line ${index + 1}`);
if (Object.prototype.hasOwnProperty.call(section, key)) {
throw new Error(`duplicate key "${key}" on line ${index + 1}`);
}
value = unquote(value);
section[key] = typed ? coerce(value) : value;
}
return result;
}
function unquote(value) {
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
return value.slice(1, -1);
}
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
return value.slice(1, -1);
}
return value;
}
function coerce(value) {
if (value === "true") return true;
if (value === "false") return false;
if (value === "null") return null;
if (/^-?\d+$/.test(value)) return Number(value);
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
return value;
}
const config = parseIni(`
; application settings
name = "Demo App"
debug = false
retries = 3
[server]
host = 127.0.0.1
port = 8080
`);
console.log(config);