Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support special characters in default expansion #74

Merged
merged 4 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 58 additions & 39 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,72 @@
'use strict'

// like String.prototype.search but returns the last index
function _searchLast (str, rgx) {
const matches = Array.from(str.matchAll(rgx))
return matches.length > 0 ? matches.slice(-1)[0].index : -1
}

function _interpolate (envValue, environment, config) {
Copy link
Contributor Author

@FezVrasta FezVrasta Mar 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function now works as follows:

  1. Find the right-most group of variables (it can be a single $VARIABLE or a nested one (${VARIABLE:-${NESTED:-default}});
  2. Expand the inner-most variable;
  3. Repeat from point 1 until no more variables are found;

Example with { VAR1: "var1", VAR2: "var2", VAR3: undefined }

  1. FOO=${VAR1}something${VAR2:-$VAR3:-default}
  2. FOO=${VAR1}something${VAR2:-default}
  3. FOO=${VAR1}somethingvar2
  4. FOO=var1somethingvar2

const matches = envValue.match(/(.?\${*[\w]*(?::-[\w/]*)?}*)/g) || []

return matches.reduce(function (newEnv, match, index) {
const parts = /(.?)\${*([\w]*(?::-[\w/]*)?)?}*/g.exec(match)
if (!parts || parts.length === 0) {
return newEnv
}

const prefix = parts[1]

let value, replacePart

if (prefix === '\\') {
replacePart = parts[0]
value = replacePart.replace('\\$', '$')
} else {
const keyParts = parts[2].split(':-')
const key = keyParts[0]
replacePart = parts[0].substring(prefix.length)
// process.env value 'wins' over .env file's value
value = Object.prototype.hasOwnProperty.call(environment, key)
? environment[key]
: (config.parsed[key] || keyParts[1] || '')

// If the value is found, remove nested expansions.
if (keyParts.length > 1 && value) {
const replaceNested = matches[index + 1]
matches[index + 1] = ''

newEnv = newEnv.replace(replaceNested, '')
}
// Resolve recursive interpolations
value = _interpolate(value, environment, config)
}

return newEnv.replace(replacePart, value)
}, envValue)
// find the last unescaped dollar sign in the
// value so that we can evaluate it
const lastUnescapedDollarSignIndex = _searchLast(envValue, /(?!(?<=\\))\$/g)

// If we couldn't match any unescaped dollar sign
// let's return the string as is
if (lastUnescapedDollarSignIndex === -1) return envValue

// This is the right-most group of variables in the string
const rightMostGroup = envValue.slice(lastUnescapedDollarSignIndex)

/**
* This finds the inner most variable/group divided
* by variable name and default value (if present)
* (
* (?!(?<=\\))\$ // only match dollar signs that are not escaped
* {? // optional opening curly brace
* ([\w]+) // match the variable name
* (?::-([^}\\]*))? // match an optional default value
* }? // optional closing curly brace
* )
*/
const matchGroup = /((?!(?<=\\))\${?([\w]+)(?::-([^}\\]*))?}?)/
const match = rightMostGroup.match(matchGroup)

if (match != null) {
const [, group, variableName, defaultValue] = match

return _interpolate(
envValue.replace(
group,
environment[variableName] ||
defaultValue ||
config.parsed[variableName] ||
''
),
environment,
config
)
}

return envValue
}

function _resolveEscapeSequences (value) {
return value.replace(/\\\$/g, '$')
}

function expand (config) {
// if ignoring process.env, use a blank object
const environment = config.ignoreProcessEnv ? {} : process.env

for (const configKey in config.parsed) {
const value = Object.prototype.hasOwnProperty.call(environment, configKey) ? environment[configKey] : config.parsed[configKey]
const value = Object.prototype.hasOwnProperty.call(environment, configKey)
? environment[configKey]
: config.parsed[configKey]

config.parsed[configKey] = _interpolate(value, environment, config)
config.parsed[configKey] = _resolveEscapeSequences(
_interpolate(value, environment, config)
)
}

for (const processKey in config.parsed) {
Expand Down
Loading