All files / dotenv / parse.ts

96.88% Branches 31/32
100.00% Functions 3/3
100.00% Lines 59/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
 
x8
x8
x8
x8
 
x8
 
x51
x51
 
x51
x33
x40
x40
x40
x40
x40
x40
 
x40
x40
 
x40
x40
 
x33
x33
 
x50
x51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x22
 
x22
 
x22
x75
x75
 
x75
 
x1
x1
 
x1
x1
 
x75
x51
x51
 
x75
x75
x75
x75
x75
x75
 
 
x22
 
x22
x51
x51
 
x21
x22

























I
















































































// Copyright 2018-2026 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;
}