Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ coverage
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
dist

# Users Environment Variables
.lock-wscript
Expand Down
18 changes: 13 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

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

### Added

- NEW 🎉: Command substitution and encryption are now supported automatically during expansion.

> These improvements bring work developed in [dotenvx](https://dotenvx.com) to dotenv-expand through [`@dotenvx/primitives`](https://www.npmjs.com/package/@dotenvx/primitives).

### Changed

- BREAKING: Require Node.js 16 or later.
- FIXED: Correct empty-variable expansion to distinguish between variables that are unset (`-` and `+`) and variables that are empty (`:-` and `:+`).
- License updated from BSD-2-Clause to BSD-3-Clause.

## [13.0.0](https://github.com/motdotla/dotenv-expand/compare/v12.0.3...v13.0.0) (2026-04-17)

Expand Down Expand Up @@ -177,7 +189,3 @@ All notable changes to this project will be documented in this file. See [standa
## 5.1.0 and prior

Please see commit history.




18 changes: 11 additions & 7 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
BSD 3-Clause License

Copyright (c) 2016, Scott Motte
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
Expand All @@ -21,4 +26,3 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<a href="https://dotenvx.com/?utm_source=github&utm_medium=readme&utm_campaign=motdotla-dotenv&utm_content=banner"><img src="https://dotenvx.com/dotenv-banner.png" alt="dotenvx" /></a>
> [dotenvx](https://dotenvx.com/?utm_source=github&utm_medium=readme&utm_campaign=motdotla-dotenv&utm_content=banner) — a secure dotenv, from the creator of `dotenv` for the agentic age. Run anywhere, encrypt secrets, and commit safely.

# dotenv-expand [![NPM version](https://img.shields.io/npm/v/dotenv-expand.svg?style=flat-square)](https://www.npmjs.com/package/dotenv-expand)
# dotenv-expand [![NPM version](https://img.shields.io/npm/v/dotenv-expand.svg?style=flat-square)](https://www.npmjs.com/package/dotenv-expand) [![downloads](https://img.shields.io/npm/dw/dotenv-expand)](https://www.npmjs.com/package/dotenv-expand)

<img src="https://raw.githubusercontent.com/motdotla/dotenv-expand/master/dotenv-expand.png" alt="dotenv-expand" align="right" width="200" />

Expand Down Expand Up @@ -127,19 +127,43 @@ console.log(process.env.HELLO) // undefined

## FAQ

### What rules does the expansion engine follow?
#### What rules does the expansion engine follow?

See a full list of rules [here](https://dotenvx.com/docs/env-file#interpolation).

### How can I avoid expanding pre-existing envs (already in my `process.env`, for example `pas$word`)?
#### How does command substitution work?

Use `$(command)` in your `.env` file. dotenv-expand runs the command and replaces the expression with its output.

```ini
NODE_VERSION=$(node --version)
```

Only use command substitution with `.env` files you trust. It executes commands with the permissions of the current process.

#### How does encryption work?

Use [dotenvx](https://dotenvx.com) to encrypt your `.env` file. dotenv-expand automatically decrypts `encrypted:` values when `DOTENV_PRIVATE_KEY` is set.

```sh
DOTENV_PRIVATE_KEY="<private key>" node -r dotenv-expand/config your_script.js
```

Keep the private key separate from the encrypted `.env` file—for example, in your cloud platform's secrets manager. Read the [dotenvx whitepaper](https://dotenvx.com/dotenvx.pdf?v=README) for more details.

#### Is it safe to commit an encrypted `.env` file?

Yes, as long as the corresponding `DOTENV_PRIVATE_KEY` is stored separately and never committed with it. Use [dotenvx](https://dotenvx.com) to create and manage the encrypted file.

#### How can I avoid expanding pre-existing envs (already in my `process.env`, for example `pas$word`)?

As of `v12.0.0` dotenv-expand no longer expands `process.env`.

If you need this ability, use [dotenvx](https://github.com/dotenvx/dotenvx) by shipping an encrypted .env file with your code - allowing safe expansion at runtime.
If you need this ability, use [dotenvx](https://dotenvx.com) and ship an encrypted `.env` file with your code, allowing safe expansion at runtime.

### How can I override an existing environment variable?
#### How can I override an existing environment variable?

Use [dotenvx](https://github.com/dotenvx/dotenvx) as dotenv-expand does not support this.
Use [dotenvx](https://dotenvx.com), as dotenv-expand does not support this.

dotenv-expand is a separate module (without knowledge of the loading of `process.env` and the `.env` file) and so cannot reliably know what to override.

Expand Down
84 changes: 25 additions & 59 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,20 @@
'use strict'

const {
decrypt,
encrypted,
evaluate,
expand: expandValue
} = require('@dotenvx/primitives')

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

function expandValue (value, processEnv, runningParsed) {
const env = { ...runningParsed, ...processEnv } // process.env wins

const regex = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g

let result = value
let match
const seen = new Set() // self-referential checker

while ((match = regex.exec(result)) !== null) {
seen.add(result)

const [template, bracedExpression, unbracedExpression] = match
const expression = bracedExpression || unbracedExpression

// match the operators `:+`, `+`, `:-`, and `-`
const opRegex = /(:\+|\+|:-|-)/
// find first match
const opMatch = expression.match(opRegex)
const splitter = opMatch ? opMatch[0] : null

const r = expression.split(splitter)

let defaultValue
let value

const key = r.shift()

if ([':+', '+'].includes(splitter)) {
defaultValue = env[key] ? r.join(splitter) : ''
value = null
} else {
defaultValue = r.join(splitter)
value = env[key]
}

if (value) {
// self-referential check
if (seen.has(value)) {
result = result.replace(template, defaultValue)
} else {
result = result.replace(template, value)
}
} else {
result = result.replace(template, defaultValue)
}

// if the result equaled what was in process.env and runningParsed then stop expanding
if (result === runningParsed[key]) {
break
}

regex.lastIndex = 0 // reset regex search position to re-evaluate after each replacement
}

return result
}

function expand (options) {
// for use with progressive expansion
const runningParsed = {}
const literals = { ...options.parsed }

let processEnv = process.env
if (options && options.processEnv != null) {
Expand All @@ -79,7 +29,23 @@ function expand (options) {
if (processEnv[key] && processEnv[key] !== value) {
value = processEnv[key]
} else {
value = expandValue(value, processEnv, runningParsed)
const expansionProcessEnv = { ...processEnv }

// dotenv.config() may have copied literal, unexpanded values into processEnv.
// Prefer already-expanded values from earlier entries in that case.
for (const runningKey in runningParsed) {
if (expansionProcessEnv[runningKey] === literals[runningKey]) {
delete expansionProcessEnv[runningKey]
}
}

const privateKey = expansionProcessEnv.DOTENV_PRIVATE_KEY || runningParsed.DOTENV_PRIVATE_KEY
if (privateKey && encrypted(value)) {
value = decrypt(privateKey, value)
}

value = expandValue(value, { processEnv: expansionProcessEnv, runningParsed })
value = evaluate(value, { processEnv: expansionProcessEnv, runningParsed })
}

options.parsed[key] = _resolveEscapeSequences(value)
Expand Down
Loading
Loading