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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ All notable changes to this project will be documented in this file. See [standa
### Added

* Add cli `dotenv run --` ([#1022](https://github.com/motdotla/dotenv/pull/1022))
* CLI supports `--debug`, `--override`, and the same `DOTENV_CONFIG_*` environment variables formerly used by preload (`PATH`, `ENCODING`, `QUIET`, `DEBUG`, `OVERRIDE`)
* CLI supports `--debug`, `--override`, `--secure`, `--fast`, and the same `DOTENV_CONFIG_*` environment variables formerly used by preload (`PATH`, `ENCODING`, `QUIET`, `DEBUG`, `OVERRIDE`, `SECURE`, `FAST`)
* `--secure` / `config({ secure: true })` / `DOTENV_CONFIG_SECURE=true` hands off to dotenvx for decryption
* CLI resolves dotenvx from local `@dotenvx/dotenvx` then `PATH`; `config({ secure: true })` requires local `@dotenvx/dotenvx`
* Warn when `encrypted:` values are present without `--secure` / `secure: true`
* `--fast` / `config({ fast: true })` / `parse(src, { fast: true })` / `DOTENV_CONFIG_FAST=true` opts into the ~2x character-scanner parser ([#1010](https://github.com/motdotla/dotenv/pull/1010)). Benchmark with `node scripts/parse-perf.js`.

### Changed

Expand Down
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Import with [ES6](#how-do-i-use-dotenv-with-import):
import 'dotenv/config'
```

`DOTENV_CONFIG_ENCODING`, `DOTENV_CONFIG_PATH`, `DOTENV_CONFIG_QUIET`, `DOTENV_CONFIG_DEBUG`, and `DOTENV_CONFIG_OVERRIDE` provide defaults for `config()` and `dotenv run`. Options/flags passed directly take precedence.
`DOTENV_CONFIG_ENCODING`, `DOTENV_CONFIG_PATH`, `DOTENV_CONFIG_QUIET`, `DOTENV_CONFIG_DEBUG`, `DOTENV_CONFIG_OVERRIDE`, `DOTENV_CONFIG_SECURE`, and `DOTENV_CONFIG_FAST` provide defaults for `config()` and `dotenv run`. Options/flags passed directly take precedence.

</details>
<details><summary>bun</summary><br>
Expand Down Expand Up @@ -169,13 +169,45 @@ Use `--override` to overwrite existing environment variables, and `--debug` for
$ dotenv run --override --debug -- node index.js
```

Use `--secure` or `config({ secure: true })` to decrypt via [dotenvx](https://dotenvx.com).

```bash
$ npm i @dotenvx/dotenvx
$ dotenv run --secure -- node index.js
```

```js
require('dotenv').config({ secure: true })
```

Or with an environment variable:

```bash
$ DOTENV_CONFIG_SECURE=true dotenv run -- node index.js
$ DOTENV_CONFIG_SECURE=true node -e "require('dotenv').config()"
```

`dotenv run --secure` resolves local `@dotenvx/dotenvx` then `dotenvx` on your `PATH`. `config({ secure: true })` requires a local `@dotenvx/dotenvx` install.

If your `.env` contains `encrypted:` values and you run without `--secure` / `secure: true`, dotenv warns and leaves them encrypted.

The same `DOTENV_CONFIG_*` environment variables formerly used by preload still work with the CLI. CLI flags take precedence.

```bash
$ DOTENV_CONFIG_PATH=./.env.local DOTENV_CONFIG_QUIET=true dotenv run -- node index.js
```

Supported: `DOTENV_CONFIG_PATH`, `DOTENV_CONFIG_ENCODING`, `DOTENV_CONFIG_QUIET`, `DOTENV_CONFIG_DEBUG`, `DOTENV_CONFIG_OVERRIDE`.
Use `--fast` (or `config({ fast: true })`) for the faster character-scanner parser (~2x). Default remains the classic regex parser.

```bash
$ dotenv run --fast -- node index.js
```

```js
require('dotenv').config({ fast: true })
```

Supported: `DOTENV_CONFIG_PATH`, `DOTENV_CONFIG_ENCODING`, `DOTENV_CONFIG_QUIET`, `DOTENV_CONFIG_DEBUG`, `DOTENV_CONFIG_OVERRIDE`, `DOTENV_CONFIG_SECURE`, `DOTENV_CONFIG_FAST`.

</details>
<details><summary>Variable Expansion</summary><br>
Expand Down Expand Up @@ -655,6 +687,26 @@ Override any environment variables that have already been set on your machine wi
require('dotenv').config({ override: true })
```

##### secure

Default: `false`

Decrypt via [dotenvx](https://dotenvx.com). Requires a local `@dotenvx/dotenvx` install.

```js
require('dotenv').config({ secure: true })
```

##### fast

Default: `false`

Use the faster character-scanner parser (~2x). Default remains the classic regex parser.

```js
require('dotenv').config({ fast: true })
```

##### processEnv

Default: `process.env`
Expand Down
132 changes: 128 additions & 4 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function parseBoolean (value) {

function printHelp () {
console.log([
'Usage: dotenv run [--help] [--quiet] [--debug] [--override] [-f <path>] -- <command>',
'Usage: dotenv run [--help] [--quiet] [--debug] [--override] [--secure] [--fast] [-f <path>] -- <command>',
'',
'Run a command with environment variables from a .env file.',
'',
Expand All @@ -25,10 +25,13 @@ function printHelp () {
' --quiet suppress the injected env message',
' --debug enable debug logging',
' --override override existing environment variables',
' --secure decrypt via dotenvx (requires dotenvx)',
' --fast use the faster character-scanner parser',
'',
'Environment variables (same as former preload):',
' DOTENV_CONFIG_PATH, DOTENV_CONFIG_ENCODING, DOTENV_CONFIG_QUIET,',
' DOTENV_CONFIG_DEBUG, DOTENV_CONFIG_OVERRIDE'
' DOTENV_CONFIG_DEBUG, DOTENV_CONFIG_OVERRIDE, DOTENV_CONFIG_SECURE,',
' DOTENV_CONFIG_FAST'
].join('\n'))
}

Expand All @@ -38,6 +41,8 @@ function parseRunArgs (args) {
let quiet
let debug
let override
let secure
let fast
let commandIndex = -1

for (let i = 0; i < args.length; i++) {
Expand Down Expand Up @@ -67,6 +72,16 @@ function parseRunArgs (args) {
continue
}

if (arg === '--secure') {
secure = true
continue
}

if (arg === '--fast') {
fast = true
continue
}

if (arg === '-f') {
const filepath = args[i + 1]
if (!filepath || filepath === '--') {
Expand Down Expand Up @@ -100,6 +115,8 @@ function parseRunArgs (args) {
quiet,
debug,
override,
secure,
fast,
command
}
}
Expand All @@ -126,6 +143,12 @@ function optionsFromEnv () {
if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
options.override = parseBoolean(process.env.DOTENV_CONFIG_OVERRIDE)
}
if (process.env.DOTENV_CONFIG_SECURE != null) {
options.secure = parseBoolean(process.env.DOTENV_CONFIG_SECURE)
}
if (process.env.DOTENV_CONFIG_FAST != null) {
options.fast = parseBoolean(process.env.DOTENV_CONFIG_FAST)
}

return options
}
Expand All @@ -137,6 +160,8 @@ function resolveRunOptions (parsed) {
quiet: envOptions.quiet === true,
debug: envOptions.debug === true,
override: envOptions.override === true,
secure: envOptions.secure === true,
fast: envOptions.fast === true,
paths: ['.env'],
defaultPath: true
}
Expand All @@ -153,10 +178,100 @@ function resolveRunOptions (parsed) {
if (parsed.quiet != null) options.quiet = parsed.quiet
if (parsed.debug != null) options.debug = parsed.debug
if (parsed.override != null) options.override = parsed.override
if (parsed.secure != null) options.secure = parsed.secure
if (parsed.fast != null) options.fast = parsed.fast

return options
}

function resolveDotenvx () {
try {
const pkgPath = require.resolve('@dotenvx/dotenvx/package.json', { paths: [process.cwd()] })
const pkg = JSON.parse(fs.readFileSync(pkgPath, { encoding: 'utf8' }))
const bin = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin && pkg.bin.dotenvx)
if (bin) {
return {
command: process.execPath,
args: [path.resolve(path.dirname(pkgPath), bin)]
}
}
} catch (_) {}

const which = process.platform === 'win32' ? 'where' : 'which'
const result = cp.spawnSync(which, ['dotenvx'], { encoding: 'utf8' })
if (result.status === 0) {
const binPath = result.stdout.split(/\r?\n/).filter(Boolean)[0]
if (binPath) {
return {
command: binPath,
args: []
}
}
}

return null
}

function buildDotenvxArgs (options, command) {
const args = ['run']

for (const filepath of options.paths) {
args.push('-f', filepath)
}
if (options.quiet) args.push('--quiet')
if (options.debug) args.push('--debug')
if (options.override) args.push('--overload')
args.push('--')
for (const part of command) {
args.push(part)
}

return args
}

function printSecureMissingError () {
console.error('dotenv: --secure requires dotenvx')
console.error(' npm i @dotenvx/dotenvx')
console.error(' # or: curl -sfS https://dotenvx.sh | sh')
}

function runSecure (options, command) {
const resolved = resolveDotenvx()
if (!resolved) {
printSecureMissingError()
process.exitCode = 1
return
}

const child = cp.spawn(resolved.command, resolved.args.concat(buildDotenvxArgs(options, command)), {
stdio: 'inherit',
shell: process.platform === 'win32'
})

child.on('error', function (e) {
console.error(`dotenv: ${e.message}`)
process.exitCode = 1
})

child.on('exit', function (exitCode, signal) {
if (typeof exitCode === 'number') {
process.exit(exitCode)
} else {
process.kill(process.pid, signal)
}
})
}

function hasEncryptedValues (parsed) {
for (const key of Object.keys(parsed)) {
const value = parsed[key]
if (typeof value === 'string' && value.indexOf('encrypted:') === 0) {
return true
}
}
return false
}

function loadEnvFiles (options) {
const parsedAll = {}
const loadedPaths = []
Expand All @@ -168,7 +283,7 @@ function loadEnvFiles (options) {
for (const filepath of options.paths) {
const resolvedPath = path.resolve(process.cwd(), resolveHome(filepath))
try {
const parsed = dotenv.parse(fs.readFileSync(resolvedPath, { encoding: options.encoding }))
const parsed = dotenv.parse(fs.readFileSync(resolvedPath, { encoding: options.encoding }), { fast: options.fast })
dotenv.populate(parsedAll, parsed, populateOptions)
loadedPaths.push(filepath)
} catch (e) {
Expand All @@ -181,8 +296,9 @@ function loadEnvFiles (options) {
}
}

const encrypted = hasEncryptedValues(parsedAll)
const injected = dotenv.populate(process.env, parsedAll, populateOptions)
return { injected, loadedPaths }
return { injected, loadedPaths, encrypted }
}

function run (argv) {
Expand Down Expand Up @@ -220,6 +336,11 @@ function run (argv) {

const options = resolveRunOptions(parsed)

if (options.secure) {
runSecure(options, parsed.command)
return
}

try {
const result = loadEnvFiles(options)
if (!options.quiet) {
Expand All @@ -229,6 +350,9 @@ function run (argv) {
}
console.error(message)
}
if (result.encrypted) {
console.error('┆ encrypted values detected — use: dotenv run --secure -- <command>')
}
} catch (e) {
console.error(`dotenv: ${e.message}`)
process.exitCode = 1
Expand Down
35 changes: 33 additions & 2 deletions lib/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,29 @@ export interface DotenvPopulateOutput {
[name: string]: string;
}

export interface DotenvParseOptions {
/**
* Default: `false`
*
* Use the faster character-scanner parser (from PR #1010).
*
* example: `require('dotenv').parse(src, { fast: true })`
*/
fast?: boolean;
}

/**
* Parses a string or buffer in the .env file format into an object.
*
* See https://dotenvx.com/docs
*
* @param src - contents to be parsed. example: `'DB_HOST=localhost'`
* @param options - parse options. example: `{ fast: true }`
* @returns an object with keys and values based on `src`. example: `{ DB_HOST : 'localhost' }`
*/
export function parse<T extends DotenvParseOutput = DotenvParseOutput>(
src: string | Buffer
src: string | Buffer,
options?: DotenvParseOptions
): T;

export interface DotenvConfigOptions {
Expand Down Expand Up @@ -70,6 +83,24 @@ export interface DotenvConfigOptions {
*/
override?: boolean;

/**
* Default: `false`
*
* Decrypt via dotenvx. Requires a local `@dotenvx/dotenvx` install.
*
* example: `require('dotenv').config({ secure: true })`
*/
secure?: boolean;

/**
* Default: `false`
*
* Use the faster character-scanner parser.
*
* example: `require('dotenv').config({ fast: true })`
*/
fast?: boolean;

/**
* Default: `process.env`
*
Expand All @@ -87,7 +118,7 @@ export interface DotenvConfigOutput {
}

type DotenvError = Error & {
code: 'OBJECT_REQUIRED';
code: 'OBJECT_REQUIRED' | 'SECURE_REQUIRES_DOTENVX';
}

export interface DotenvPopulateOptions {
Expand Down
Loading
Loading