← 中文资源

简体中文版本

解析带分节与类型的 INI 配置

把 INI 文本解析为无原型对象,支持分节、注释、引号值与类型转换,并检测重复项。

阅读英文原文

功能说明

该示例把 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);