← 中文资源

简体中文版本

Dotenv 文件解析器

解析 .env 文本中的注释、export 前缀、引号值、转义与多行字符串,不修改进程环境。

阅读英文原文

功能说明

parseDotenv 把 .env 文件内容解析为普通对象,支持注释、空行、export 前缀、单双引号、双引号转义与跨行值。它只做解析,不写入 process.env,便于在加载前做校验、合并不同来源或在测试中构造配置。适合 CLI 工具、本地开发启动器、配置检查脚本和多环境部署工具。

关键代码

先把换行统一为 LF,再逐行处理。去除首尾空白后跳过空行与注释,识别 export 前缀并用正则校验键名与等号。双引号值会持续向后拼接行,直到找到未被反斜杠转义的闭合引号,再还原换行、制表符、引号与反斜杠转义;单引号不做转义;裸值只去掉空白后的行内注释。解析结果以键值对象返回。

使用注意

该实现不做变量插值,类似美元符号加花括号写法的引用会保持字面值;需要展开时应显式实现并限制递归深度。重复键以后出现的值为准,这与部分 dotenv 实现的先到先得不同。裸值中的井号只有在空白后才视为注释,值内部井号会保留。解析成功不代表配置合法,键名与值仍需用 Schema 校验后再注入环境。

验证方式

测试注释、空行、export、裸值、单引号、双引号、转义换行与多行双引号。验证裸值中的井号与空白注释的区分、重复键覆盖、键名包含点号与连字符。测试缺少等号、非法键名、未闭合引号会抛错。最后确认函数不会读写 process.env,且 CRLF 与 LF 输入得到相同结果。

代码示例

export function parseDotenv(input: string): Record<string, string> {
  if (typeof input !== "string") throw new TypeError("input must be a string");
  const escapes: Record<string, string> = { "\\": "\\", n: "\n", r: "\r", t: "\t", '"': '"' };
  const result: Record<string, string> = {};
  const lines = input.replace(/\r\n?/g, "\n").split("\n");
  for (let index = 0; index < lines.length; index += 1) {
    let line = lines[index]!.trim();
    if (line === "" || line.startsWith("#")) continue;
    if (line.startsWith("export ")) line = line.slice("export ".length).trim();
    const match = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line);
    if (!match) throw new SyntaxError("invalid dotenv line " + (index + 1));
    const key = match[1]!;
    let raw = match[2]!;
    let value: string;
    if (raw.startsWith('"')) {
      while (!/(^|[^\\])"\s*(#.*)?$/.test(raw)) {
        index += 1;
        if (index >= lines.length) throw new SyntaxError("unterminated quoted value for " + key);
        raw += "\n" + lines[index]!;
      }
      const end = raw.lastIndexOf('"');
      value = raw.slice(1, end).replace(/\\([\\nrt"])/g, (_, character: string) => {
        return escapes[character] ?? character;
      });
    } else if (raw.startsWith("'")) {
      let end = 1;
      while (end < raw.length) {
        if (raw[end] === "'") {
          if (raw[end + 1] === "'") {
            end += 2;
            continue;
          }
          break;
        }
        end += 1;
      }
      if (end >= raw.length) throw new SyntaxError("unterminated quoted value for " + key);
      value = raw.slice(1, end).replace(/''/g, "'");
    } else {
      value = raw.replace(/\s+#.*$/, "").trim();
    }
    result[key] = value;
  }
  return result;
}