|
1 | 1 | 'use strict'
|
2 | 2 |
|
3 |
| -// * / |
4 |
| -// * (\\)? # is it escaped with a backslash? |
5 |
| -// * (\$) # literal $ |
6 |
| -// * (?!\() # shouldnt be followed by parenthesis |
7 |
| -// * (\{?) # first brace wrap opening |
8 |
| -// * ([\w.]+) # key |
9 |
| -// * (?::-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))? # optional default nested 3 times |
10 |
| -// * (\}?) # last brace warp closing |
11 |
| -// * /xi |
12 |
| - |
13 |
| -const DOTENV_SUBSTITUTION_REGEX = /(\\)?(\$)(?!\()(\{?)([\w.]+)(?::?-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))?(\}?)/gi |
14 |
| - |
15 | 3 | function _resolveEscapeSequences (value) {
|
16 | 4 | return value.replace(/\\\$/g, '$')
|
17 | 5 | }
|
18 | 6 |
|
19 |
| -function interpolate (value, processEnv, parsed) { |
20 |
| - return value.replace(DOTENV_SUBSTITUTION_REGEX, (match, escaped, dollarSign, openBrace, key, defaultValue, closeBrace) => { |
21 |
| - if (escaped === '\\') { |
22 |
| - return match.slice(1) |
23 |
| - } else { |
24 |
| - if (processEnv[key]) { |
25 |
| - if (processEnv[key] === parsed[key]) { |
26 |
| - return processEnv[key] |
27 |
| - } else { |
28 |
| - // scenario: PASSWORD_EXPAND_NESTED=${PASSWORD_EXPAND} |
29 |
| - return interpolate(processEnv[key], processEnv, parsed) |
30 |
| - } |
31 |
| - } |
| 7 | +function expandValue (value, processEnv, runningParsed) { |
| 8 | + const env = { ...runningParsed, ...processEnv } // process.env wins |
32 | 9 |
|
33 |
| - if (parsed[key]) { |
34 |
| - // avoid recursion from EXPAND_SELF=$EXPAND_SELF |
35 |
| - if (parsed[key] !== value) { |
36 |
| - return interpolate(parsed[key], processEnv, parsed) |
37 |
| - } |
38 |
| - } |
| 10 | + const regex = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g |
| 11 | + |
| 12 | + let result = value |
| 13 | + let match |
| 14 | + const seen = new Set() // self-referential checker |
| 15 | + |
| 16 | + while ((match = regex.exec(result)) !== null) { |
| 17 | + seen.add(result) |
| 18 | + |
| 19 | + const [template, bracedExpression, unbracedExpression] = match |
| 20 | + const expression = bracedExpression || unbracedExpression |
| 21 | + |
| 22 | + // match the operators `:+`, `+`, `:-`, and `-` |
| 23 | + const opRegex = /(:\+|\+|:-|-)/ |
| 24 | + // find first match |
| 25 | + const opMatch = expression.match(opRegex) |
| 26 | + const splitter = opMatch ? opMatch[0] : null |
| 27 | + |
| 28 | + const r = expression.split(splitter) |
| 29 | + |
| 30 | + let defaultValue |
| 31 | + let value |
| 32 | + |
| 33 | + const key = r.shift() |
| 34 | + |
| 35 | + if ([':+', '+'].includes(splitter)) { |
| 36 | + defaultValue = env[key] ? r.join(splitter) : '' |
| 37 | + value = null |
| 38 | + } else { |
| 39 | + defaultValue = r.join(splitter) |
| 40 | + value = env[key] |
| 41 | + } |
39 | 42 |
|
40 |
| - if (defaultValue) { |
41 |
| - if (defaultValue.startsWith('$')) { |
42 |
| - return interpolate(defaultValue, processEnv, parsed) |
43 |
| - } else { |
44 |
| - return defaultValue |
45 |
| - } |
| 43 | + if (value) { |
| 44 | + // self-referential check |
| 45 | + if (seen.has(value)) { |
| 46 | + result = result.replace(template, defaultValue) |
| 47 | + } else { |
| 48 | + result = result.replace(template, value) |
46 | 49 | }
|
| 50 | + } else { |
| 51 | + result = result.replace(template, defaultValue) |
| 52 | + } |
47 | 53 |
|
48 |
| - return '' |
| 54 | + // if the result equaled what was in process.env and runningParsed then stop expanding |
| 55 | + if (result === processEnv[key] && result === runningParsed[key]) { |
| 56 | + break |
49 | 57 | }
|
50 |
| - }) |
| 58 | + |
| 59 | + regex.lastIndex = 0 // reset regex search position to re-evaluate after each replacement |
| 60 | + } |
| 61 | + |
| 62 | + return result |
51 | 63 | }
|
52 | 64 |
|
53 | 65 | function expand (options) {
|
| 66 | + // for use with progressive expansion |
| 67 | + const runningParsed = {} |
| 68 | + |
54 | 69 | let processEnv = process.env
|
55 | 70 | if (options && options.processEnv != null) {
|
56 | 71 | processEnv = options.processEnv
|
57 | 72 | }
|
58 | 73 |
|
| 74 | + // dotenv.config() ran before this so the assumption is process.env has already been set |
59 | 75 | for (const key in options.parsed) {
|
60 | 76 | let value = options.parsed[key]
|
61 | 77 |
|
62 |
| - const inProcessEnv = Object.prototype.hasOwnProperty.call(processEnv, key) |
63 |
| - if (inProcessEnv) { |
64 |
| - if (processEnv[key] === options.parsed[key]) { |
65 |
| - // assume was set to processEnv from the .env file if the values match and therefore interpolate |
66 |
| - value = interpolate(value, processEnv, options.parsed) |
67 |
| - } else { |
68 |
| - // do not interpolate - assume processEnv had the intended value even if containing a $. |
69 |
| - value = processEnv[key] |
70 |
| - } |
| 78 | + // short-circuit scenario: process.env was already set prior to the file value |
| 79 | + if (processEnv[key] && processEnv[key] !== value) { |
| 80 | + value = processEnv[key] |
71 | 81 | } else {
|
72 |
| - // not inProcessEnv so assume interpolation for this .env key |
73 |
| - value = interpolate(value, processEnv, options.parsed) |
| 82 | + value = expandValue(value, processEnv, runningParsed) |
74 | 83 | }
|
75 | 84 |
|
76 | 85 | options.parsed[key] = _resolveEscapeSequences(value)
|
| 86 | + |
| 87 | + // for use with progressive expansion |
| 88 | + runningParsed[key] = _resolveEscapeSequences(value) |
77 | 89 | }
|
78 | 90 |
|
79 | 91 | for (const processKey in options.parsed) {
|
|
0 commit comments