All files / dotenv / parse.ts

94.44% Branches 17/18
94.92% Lines 56/59
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
 
 
 
 
 
 
 
 
 
x6
x6
 
x6
 
x6
x6
 
x6
x6
x6
x6
x6
 
x14
 
 
 
 
x14
 
x57
x57
 
x57
x90
x130
x130
x130
x130
x130
x130
 
x130
x130
 
x130
x130
 
x90
x90
 
x107
x57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x28
 
x28
 
x28
x103
x103
 
x103
 
x104
x104
 
x104
x104
 
x103
x154
x154
 
x103
x103
x103
x103
x103
x103
 
 
x84
 
x28
x79
x79
 
x49
x28

























I
















































































// Copyright 2018-2025 the Deno authors. MIT license.

type LineParseResult = {
  key: string;
  unquoted: string;
  interpolated: string;
  notInterpolated: string;
};

const KEY_VALUE_REGEXP =
  /^\s*(?:export\s+)?(?<key>[^\s=#]+?)\s*=[\ \t]*('\r?\n?(?<notInterpolated>(.|\r\n|\n)*?)\r?\n?'|"\r?\n?(?<interpolated>(.|\r\n|\n)*?)\r?\n?"|(?<unquoted>[^\r\n#]*)) *#*.*$/gm;

const VALID_KEY_REGEXP = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

const EXPAND_VALUE_REGEXP =
  /(\${(?<inBrackets>.+?)(\:-(?<inBracketsDefault>.+))?}|(?<!\\)\$(?<notInBrackets>\w+)(\:-(?<notInBracketsDefault>.+))?)/g;

const CHARACTERS_MAP: { [key: string]: string } = {
  "\\n": "\n",
  "\\r": "\r",
  "\\t": "\t",
};

function expandCharacters(str: string): string {
  return str.replace(
    /\\([nrt])/g,
    ($1: keyof typeof CHARACTERS_MAP): string => CHARACTERS_MAP[$1] ?? "",
  );
}

function expand(str: string, variablesMap: Record<string, string>): string {
  let current = str;

  while (EXPAND_VALUE_REGEXP.test(current)) {
    current = current.replace(EXPAND_VALUE_REGEXP, (...params) => {
      const {
        inBrackets,
        inBracketsDefault,
        notInBrackets,
        notInBracketsDefault,
      } = params.at(-1);

      const expandValue = inBrackets ?? notInBrackets;
      const defaultValue = inBracketsDefault ?? notInBracketsDefault;

      return (
        variablesMap[expandValue] ?? Deno.env.get(expandValue) ?? defaultValue
      );
    });
  }

  return current;
}

/**
 * Parse `.env` file output in an object.
 *
 * Note: The key needs to match the pattern /^[a-zA-Z_][a-zA-Z0-9_]*$/.
 *
 * @example Usage
 * ```ts
 * import { parse } from "@std/dotenv/parse";
 * import { assertEquals } from "@std/assert";
 *
 * const env = parse("GREETING=hello world");
 * assertEquals(env, { GREETING: "hello world" });
 * ```
 *
 * @param text The text to parse.
 * @returns The parsed object.
 */
export function parse(text: string): Record<string, string> {
  const env: Record<string, string> = Object.create(null);

  const keysForExpandCheck = [];

  for (const match of text.matchAll(KEY_VALUE_REGEXP)) {
    const { key, interpolated, notInterpolated, unquoted } = match
      ?.groups as LineParseResult;

    if (!VALID_KEY_REGEXP.test(key)) {
      // deno-lint-ignore no-console
      console.warn(
        `Ignored the key "${key}" as it is not a valid identifier: The key need to match the pattern /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,
      );
      continue;
    }

    if (unquoted) {
      keysForExpandCheck.push(key);
    }

    env[key] = typeof notInterpolated === "string"
      ? notInterpolated
      : typeof interpolated === "string"
      ? expandCharacters(interpolated)
      : unquoted.trim();
  }

  //https://github.com/motdotla/dotenv-expand/blob/ed5fea5bf517a09fd743ce2c63150e88c8a5f6d1/lib/main.js#L23
  const variablesMap = { ...env };

  for (const key of keysForExpandCheck) {
    env[key] = expand(env[key]!, variablesMap);
  }

  return env;
}