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

handle $var1$var2 syntax #108

Merged
merged 5 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. See [standa

## [Unreleased](https://github.com/motdotla/dotenv-expand/compare/v11.0.0...master)

## [11.0.0](https://github.com/motdotla/dotenv-expand/compare/v10.0.0...v11.0.0) (2024-02-08)
## [11.0.0](https://github.com/motdotla/dotenv-expand/compare/v10.0.0...v11.0.0) (2024-02-10)

### Added

Expand All @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. See [standa
### Changed

- Do not expand prior `process.env` environment variables. NOTE: make sure to see updated README regarding `dotenv.config({ processEnv: {} })` ([#104](https://github.com/motdotla/dotenv-expand/pull/104))
- 🐞 handle `$var1$var2` ([#103](https://github.com/motdotla/dotenv-expand/issues/103), [#104](https://github.com/motdotla/dotenv-expand/pull/104))

### Removed

Expand Down
134 changes: 85 additions & 49 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,93 @@
'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 (value, processEnv, parsed) {
// find the last unescaped dollar sign in the value to evaluate
const lastUnescapedDollarSignIndex = _searchLast(value, /(?!(?<=\\))\$/g)

// return early unless unescaped dollar sign
if (lastUnescapedDollarSignIndex === -1) {
return value
}

// This is the right-most group of variables in the string
const rightMostGroup = value.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, key, defaultValue] = match
const replacementString = processEnv[key] || defaultValue || parsed[key] || ''
const modifiedValue = value.replace(group, replacementString)

// return early for scenario like processEnv.PASSWORD = 'pas$word'
if (processEnv[key] && modifiedValue === processEnv[key]) {
return modifiedValue
}

return _interpolate(modifiedValue, processEnv, parsed)
}

return value
}
// // 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 (value, processEnv, parsed) {
// // find the last unescaped dollar sign in the value to evaluate
// const lastUnescapedDollarSignIndex = _searchLast(value, /(?!(?<=\\))\$/g)
//
// // return early unless unescaped dollar sign
// if (lastUnescapedDollarSignIndex === -1) {
// return value
// }
//
// // This is the right-most group of variables in the string
// const rightMostGroup = value.slice(lastUnescapedDollarSignIndex)
//
// console.log('rightMostGroup', rightMostGroup)
//
// /**
// * 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, key, defaultValue] = match
// const replacementString = processEnv[key] || defaultValue || parsed[key] || ''
// const modifiedValue = value.replace(group, replacementString)
//
// // return early for scenario like processEnv.PASSWORD = 'pas$word'
// if (processEnv[key] && modifiedValue === processEnv[key]) {
// return modifiedValue
// }
//
// return _interpolate(modifiedValue, processEnv, parsed)
// }
//
// return value
// }

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

function interpolate (value, processEnv, parsed) {
// * /
// * (\\)? # is it escaped with a backslash?
// * (\$) # literal $
// * (?!\() # shouldnt be followed by parenthesis
// * (\{?) # first brace wrap opening
// * ([\w.]+) # key
// * (?::-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))? # optional default nested 3 times
// * (\}?) # last brace warp closing
// * /xi

const SUB_REGEX = /(\\)?(\$)(?!\()(\{?)([\w.]+)(?::-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))?(\}?)/gi

return value.replace(SUB_REGEX, (match, escaped, dollarSign, openBrace, key, defaultValue, closeBrace) => {
if (escaped === '\\') {
return match.slice(1)
} else {
if (processEnv[key]) {
return processEnv[key]
}

if (defaultValue) {
if (defaultValue.startsWith('$')) {
return interpolate(defaultValue, processEnv, parsed)
} else {
return defaultValue
}
}

return parsed[key] || ''
}
})
}

function expand (options) {
let processEnv = process.env
if (options && options.processEnv != null) {
Expand All @@ -61,11 +97,11 @@ function expand (options) {
for (const key in options.parsed) {
let value = options.parsed[key]

// don't interpolate the processEnv value if it exists there already
// don't interpolate if it exists already in processEnv
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
value = processEnv[key]
} else {
value = _interpolate(value, processEnv, options.parsed)
value = interpolate(value, processEnv, options.parsed)
}

options.parsed[key] = _resolveEscapeSequences(value)
Expand Down
42 changes: 26 additions & 16 deletions tests/.env.test
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
NODE_ENV=test
BASIC=basic
BASIC_EXPAND=$BASIC
MACHINE=machine_env
MACHINE_EXPAND=$MACHINE
UNDEFINED_EXPAND=$UNDEFINED_ENV_KEY

ESCAPED_EXPAND=\$ESCAPED
DEFINED_EXPAND_WITH_DEFAULT=${MACHINE:-default}
DEFINED_EXPAND_WITH_DEFAULT_NESTED=${MACHINE:-${UNDEFINED_ENV_KEY:-default}}
UNDEFINED_EXPAND_WITH_DEFINED_NESTED=${UNDEFINED_ENV_KEY:-${MACHINE:-default}}
UNDEFINED_EXPAND_WITH_DEFAULT=${UNDEFINED_ENV_KEY:-default}
UNDEFINED_EXPAND_WITH_DEFAULT_NESTED=${UNDEFINED_ENV_KEY:-${UNDEFINED_ENV_KEY_2:-default}}
DEFINED_EXPAND_WITH_DEFAULT_NESTED_TWICE=${UNDEFINED_ENV_KEY:-${MACHINE}${UNDEFINED_ENV_KEY_3:-default}}
UNDEFINED_EXPAND_WITH_DEFAULT_NESTED_TWICE=${UNDEFINED_ENV_KEY:-${UNDEFINED_ENV_KEY_2:-${UNDEFINED_ENV_KEY_3:-default}}}
DEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS=${MACHINE:-/default/path:with/colon}
UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS=${UNDEFINED_ENV_KEY:-/default/path:with/colon}
UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS_NESTED=${UNDEFINED_ENV_KEY:-${UNDEFINED_ENV_KEY_2:-/default/path:with/colon}}

EXPAND_DEFAULT=${MACHINE:-default}
EXPAND_DEFAULT_NESTED=${MACHINE:-${UNDEFINED:-default}}
EXPAND_DEFAULT_NESTED_TWICE=${UNDEFINED:-${MACHINE}${UNDEFINED:-default}}
EXPAND_DEFAULT_SPECIAL_CHARACTERS=${MACHINE:-/default/path:with/colon}

UNDEFINED_EXPAND=$UNDEFINED
UNDEFINED_EXPAND_NESTED=${UNDEFINED:-${MACHINE:-default}}
UNDEFINED_EXPAND_DEFAULT=${UNDEFINED:-default}
UNDEFINED_EXPAND_DEFAULT_NESTED=${UNDEFINED:-${UNDEFINED:-default}}
UNDEFINED_EXPAND_DEFAULT_NESTED_TWICE=${UNDEFINED:-${UNDEFINED:-${UNDEFINED:-default}}}
UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS=${UNDEFINED:-/default/path:with/colon}
UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS_NESTED=${UNDEFINED:-${UNDEFINED_2:-/default/path:with/colon}}

MONGOLAB_DATABASE=heroku_db
MONGOLAB_USER=username
MONGOLAB_PASSWORD=password
Expand All @@ -25,12 +28,19 @@ MONGOLAB_URI=mongodb://${MONGOLAB_USER}:${MONGOLAB_PASSWORD}@${MONGOLAB_DOMAIN}:
MONGOLAB_USER_RECURSIVELY=${MONGOLAB_USER}:${MONGOLAB_PASSWORD}
MONGOLAB_URI_RECURSIVELY=mongodb://${MONGOLAB_USER_RECURSIVELY}@${MONGOLAB_DOMAIN}:${MONGOLAB_PORT}/${MONGOLAB_DATABASE}

WITHOUT_CURLY_BRACES_URI=mongodb://$MONGOLAB_USER:$MONGOLAB_PASSWORD@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE
WITHOUT_CURLY_BRACES_USER_RECURSIVELY=$MONGOLAB_USER:$MONGOLAB_PASSWORD
WITHOUT_CURLY_BRACES_URI_RECURSIVELY=mongodb://$MONGOLAB_USER_RECURSIVELY@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE
WITHOUT_CURLY_BRACES_UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS=$UNDEFINED_ENV_KEY:-/default/path:with/colon
NO_CURLY_BRACES_URI=mongodb://$MONGOLAB_USER:$MONGOLAB_PASSWORD@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE
NO_CURLY_BRACES_USER_RECURSIVELY=$MONGOLAB_USER:$MONGOLAB_PASSWORD
NO_CURLY_BRACES_URI_RECURSIVELY=mongodb://$MONGOLAB_USER_RECURSIVELY@$MONGOLAB_DOMAIN:$MONGOLAB_PORT/$MONGOLAB_DATABASE
NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS=$UNDEFINED:-/default/path:with/colon

POSTGRESQL.BASE.USER=postgres
POSTGRESQL.MAIN.USER=${POSTGRESQL.BASE.USER}

DOLLAR=$

ONE=one
TWO=two
ONETWO=${ONE}${TWO}
ONETWO_SIMPLE=${ONE}$TWO
ONETWO_SIMPLE2=$ONE${TWO}
ONETWO_SUPER_SIMPLE=$ONE$TWO
40 changes: 26 additions & 14 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ t.test('does not expand environment variables existing already on the machine th
t.test('expands missing environment variables to an empty string', ct => {
const dotenv = {
parsed: {
UNDEFINED_EXPAND: '$UNDEFINED_ENV_KEY'
UNDEFINED_EXPAND: '$UNDEFINED'
}
}
const parsed = dotenvExpand.expand(dotenv).parsed
Expand Down Expand Up @@ -194,7 +194,7 @@ t.test('expands environment variables existing already on the machine even with
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
dotenvExpand.expand(dotenv)

ct.equal(process.env.DEFINED_EXPAND_WITH_DEFAULT, 'machine')
ct.equal(process.env.EXPAND_DEFAULT, 'machine')

ct.end()
})
Expand All @@ -205,7 +205,7 @@ t.test('expands environment variables existing already on the machine even with
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
dotenvExpand.expand(dotenv)

ct.equal(process.env.DEFINED_EXPAND_WITH_DEFAULT_NESTED, 'machine')
ct.equal(process.env.EXPAND_DEFAULT_NESTED, 'machine')

ct.end()
})
Expand All @@ -216,7 +216,7 @@ t.test('expands environment variables undefined with one already on the machine
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
dotenvExpand.expand(dotenv)

ct.equal(process.env.UNDEFINED_EXPAND_WITH_DEFINED_NESTED, 'machine')
ct.equal(process.env.UNDEFINED_EXPAND_NESTED, 'machine')

ct.end()
})
Expand All @@ -225,7 +225,7 @@ t.test('expands missing environment variables to an empty string but replaces wi
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.UNDEFINED_EXPAND_WITH_DEFAULT, 'default')
ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT, 'default')

ct.end()
})
Expand All @@ -234,7 +234,7 @@ t.test('expands environent variables and concats with default nested', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.DEFINED_EXPAND_WITH_DEFAULT_NESTED_TWICE, 'machinedefault')
ct.equal(parsed.EXPAND_DEFAULT_NESTED_TWICE, 'machinedefault')

ct.end()
})
Expand All @@ -243,7 +243,7 @@ t.test('expands missing environment variables to an empty string but replaces wi
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.UNDEFINED_EXPAND_WITH_DEFAULT_NESTED, 'default')
ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_NESTED, 'default')

ct.end()
})
Expand All @@ -252,7 +252,7 @@ t.test('expands missing environment variables to an empty string but replaces wi
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.UNDEFINED_EXPAND_WITH_DEFAULT_NESTED_TWICE, 'default')
ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_NESTED_TWICE, 'default')

ct.end()
})
Expand Down Expand Up @@ -290,7 +290,7 @@ t.test('multiple expand', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.WITHOUT_CURLY_BRACES_URI, 'mongodb://username:[email protected]:12345/heroku_db')
ct.equal(parsed.NO_CURLY_BRACES_URI, 'mongodb://username:[email protected]:12345/heroku_db')

ct.end()
})
Expand All @@ -299,7 +299,7 @@ t.test('should expand recursively', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.WITHOUT_CURLY_BRACES_URI_RECURSIVELY, 'mongodb://username:[email protected]:12345/heroku_db')
ct.equal(parsed.NO_CURLY_BRACES_URI_RECURSIVELY, 'mongodb://username:[email protected]:12345/heroku_db')

ct.end()
})
Expand All @@ -326,7 +326,7 @@ t.test('expands environment variables existing already on the machine even with
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.DEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS, 'machine')
ct.equal(parsed.EXPAND_DEFAULT_SPECIAL_CHARACTERS, 'machine')

ct.end()
})
Expand All @@ -335,8 +335,8 @@ t.test('should expand with default value correctly', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS, '/default/path:with/colon')
ct.equal(parsed.WITHOUT_CURLY_BRACES_UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS, '/default/path:with/colon')
ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS, '/default/path:with/colon')
ct.equal(parsed.NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS, '/default/path:with/colon')

ct.end()
})
Expand All @@ -345,7 +345,7 @@ t.test('should expand with default nested value correctly', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.UNDEFINED_EXPAND_WITH_DEFAULT_WITH_SPECIAL_CHARACTERS_NESTED, '/default/path:with/colon')
ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS_NESTED, '/default/path:with/colon')

ct.end()
})
Expand All @@ -367,3 +367,15 @@ t.test('handles value of only $', ct => {

ct.end()
})

t.test('handles $one$two', ct => {
const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} })
const parsed = dotenvExpand.expand(dotenv).parsed

ct.equal(parsed.ONETWO, 'onetwo')
ct.equal(parsed.ONETWO_SIMPLE, 'onetwo')
ct.equal(parsed.ONETWO_SIMPLE2, 'onetwo')
ct.equal(parsed.ONETWO_SUPER_SIMPLE, 'onetwo')

ct.end()
})
Loading