Dotenv File Parser

Parse .env files with comments, export prefixes, quoted values, escapes and multiline strings without mutating process.env.

TypescriptConfig Management

Code

snippet
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;
}