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

feat: Add --recursive flag to enable env-var nesting #253

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

- **Upgrade**: Upgraded dependency `commander` to `5.x`
- **Upgrade**: Upgraded devDependencies `ts-standard`, `sinon`
- **Feature**: support both `$var` and `${var}` when expanding vars
- **Feature**: Added support for nested env variables with the `--recursive` flag

## 10.1.0

- **Feature**: Added support for expanding vars using the `-x` flag.
Note: only supports `$var` syntax
- **Feature**: Added support for `--silent` flag that ignores env-cmd errors and missing files and
- **Feature**: Added support for `--silent` flag that ignores env-cmd errors and missing files and
only terminates on caught signals
- **Feature**: Added a new `--verbose` flag that prints additional debugging info to `console.info`
- **Upgrade**: Upgraded dependency `commander` to `4.x`
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ Options:
--silent Ignore any env-cmd errors and only fail on executed program failure.
--use-shell Execute the command in a new shell with the given environment
--verbose Print helpful debugging information
-x, --expand-envs Replace $var in args and command with environment variables
-x, --expand-envs Replace $var and ${var} in args and command with environment variables
--recursive Replace $var and ${var} in env file with the referenced environment variable
-h, --help output usage information
```

Expand Down Expand Up @@ -130,14 +131,14 @@ commands together that share the same environment variables.
```

### Asynchronous env file support

EnvCmd supports reading from asynchronous `.env` files. Instead of using a `.env` file, pass in a `.js`
file that exports either an object or a `Promise` resolving to an object (`{ ENV_VAR_NAME: value, ... }`). Asynchronous `.rc`
files are also supported using `.js` file extension and resolving to an object with top level environment
names (`{ production: { ENV_VAR_NAME: value, ... } }`).

**Terminal**

```sh
./node_modules/.bin/env-cmd -f ./async-file.js node index.js
```
Expand Down
6 changes: 6 additions & 0 deletions dist/env-cmd.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnvCmd = exports.CLI = void 0;
const spawn_1 = require("./spawn");
const signal_termination_1 = require("./signal-termination");
const parse_args_1 = require("./parse-args");
Expand Down Expand Up @@ -51,6 +52,11 @@ async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) {
// Add in the system environment variables to our environment list
env = Object.assign({}, process.env, env);
}
if (options.recursive === true) {
for (const key of Object.keys(env)) {
env[key] = expand_envs_1.expandEnvs(env[key], env);
}
}
if (options.expandEnvs === true) {
command = expand_envs_1.expandEnvs(command, env);
commandArgs = commandArgs.map(arg => expand_envs_1.expandEnvs(arg, env));
Expand Down
2 changes: 1 addition & 1 deletion dist/expand-envs.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* expandEnvs Replaces $var in args and command with environment variables
* expandEnvs Replaces $var and ${var} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
export declare function expandEnvs(str: string, envs: {
Expand Down
7 changes: 4 additions & 3 deletions dist/expand-envs.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.expandEnvs = void 0;
/**
* expandEnvs Replaces $var in args and command with environment variables
* expandEnvs Replaces $var and ${var} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
function expandEnvs(str, envs) {
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => {
const varValue = envs[varName.slice(1)];
return str.replace(/(?<!\\)\$\{?[a-zA-Z0-9_]+\}?/g, varName => {
const varValue = envs[varName.startsWith('${') ? varName.substr(2, varName.length - 3) : varName.substr(1)];
return varValue === undefined ? varName : varValue;
});
}
Expand Down
1 change: 1 addition & 0 deletions dist/get-env-vars.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRCFile = exports.getEnvFile = exports.getEnvVars = void 0;
const parse_rc_file_1 = require("./parse-rc-file");
const parse_env_file_1 = require("./parse-env-file");
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'];
Expand Down
18 changes: 14 additions & 4 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetEnvVars = void 0;
const get_env_vars_1 = require("./get-env-vars");
__export(require("./env-cmd"));
// Export the core env-cmd API
__exportStar(require("./types"), exports);
__exportStar(require("./env-cmd"), exports);
exports.GetEnvVars = get_env_vars_1.getEnvVars;
9 changes: 8 additions & 1 deletion dist/parse-args.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseArgsUsingCommander = exports.parseArgs = void 0;
const commander = require("commander");
const utils_1 = require("./utils");
// Use commonjs require to prevent a weird folder hierarchy in dist
Expand Down Expand Up @@ -30,6 +31,10 @@ function parseArgs(args) {
if (program.expandEnvs === true) {
expandEnvs = true;
}
let recursive = false;
if (program.recursive === true) {
recursive = true;
}
let verbose = false;
if (program.verbose === true) {
verbose = true;
Expand Down Expand Up @@ -59,6 +64,7 @@ function parseArgs(args) {
rc,
options: {
expandEnvs,
recursive,
noOverride,
silent,
useShell,
Expand All @@ -84,7 +90,8 @@ function parseArgsUsingCommander(args) {
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('--use-shell', 'Execute the command in a new shell with the given environment')
.option('--verbose', 'Print helpful debugging information')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.option('-x, --expand-envs', 'Replace $var and $\\{var\\} in args and command with environment variables')
.option('--recursive', 'Replace $var and $\\{var\\} in env file with the referenced environment variable')
.allowUnknownOption(true)
.parse(['_', '_', ...args]);
}
Expand Down
1 change: 1 addition & 0 deletions dist/parse-env-file.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripEmptyLines = exports.stripComments = exports.parseEnvVars = exports.parseEnvString = exports.getEnvFileVars = void 0;
const fs = require("fs");
const path = require("path");
const utils_1 = require("./utils");
Expand Down
1 change: 1 addition & 0 deletions dist/parse-rc-file.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRCFileVars = void 0;
const fs_1 = require("fs");
const util_1 = require("util");
const path_1 = require("path");
Expand Down
2 changes: 1 addition & 1 deletion dist/signal-termination.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export declare class TermSignals {
/**
* Terminate parent process helper
*/
_terminateProcess(code?: number, signal?: NodeJS.Signals): void;
_terminateProcess(code?: number, signal?: NodeJS.Signals): boolean | void;
/**
* Exit event listener clean up helper
*/
Expand Down
1 change: 1 addition & 0 deletions dist/signal-termination.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TermSignals = void 0;
const SIGNALS_TO_HANDLE = [
'SIGINT', 'SIGTERM', 'SIGHUP'
];
Expand Down
1 change: 1 addition & 0 deletions dist/spawn.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawn = void 0;
const spawn = require("cross-spawn");
exports.spawn = spawn;
1 change: 1 addition & 0 deletions dist/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'>
commandArgs: string[];
options?: {
expandEnvs?: boolean;
recursive?: boolean;
noOverride?: boolean;
silent?: boolean;
useShell?: boolean;
Expand Down
1 change: 1 addition & 0 deletions dist/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isPromise = exports.parseArgList = exports.resolveEnvFilePath = void 0;
const path = require("path");
const os = require("os");
/**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"Eric Lanehart <[email protected]>",
"Jon Scheiding <[email protected]>",
"serapath (Alexander Praetorius) <[email protected]>",
"Anton Versal <[email protected]>"
"Anton Versal <[email protected]>",
"Nicholas Krul <[email protected]>"
],
"license": "MIT",
"bugs": {
Expand Down
6 changes: 6 additions & 0 deletions src/env-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export async function EnvCmd (
env = Object.assign({}, process.env, env)
}

if (options.recursive === true) {
for (const key of Object.keys(env)) {
env[key] = expandEnvs(env[key], env)
}
}

if (options.expandEnvs === true) {
command = expandEnvs(command, env)
commandArgs = commandArgs.map(arg => expandEnvs(arg, env))
Expand Down
6 changes: 3 additions & 3 deletions src/expand-envs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

/**
* expandEnvs Replaces $var in args and command with environment variables
* expandEnvs Replaces $var and ${var} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
export function expandEnvs (str: string, envs: { [key: string]: any }): string {
return str.replace(/(?<!\\)\$[a-zA-Z0-9_]+/g, varName => {
const varValue = envs[varName.slice(1)]
return str.replace(/(?<!\\)\$\{?[a-zA-Z0-9_]+\}?/g, varName => {
const varValue = envs[varName.startsWith('${') ? varName.substr(2, varName.length - 3) : varName.substr(1)]
return varValue === undefined ? varName : varValue
})
}
8 changes: 7 additions & 1 deletion src/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export function parseArgs (args: string[]): EnvCmdOptions {
if (program.expandEnvs === true) {
expandEnvs = true
}
let recursive = false
if (program.recursive === true) {
recursive = true
}
let verbose = false
if (program.verbose === true) {
verbose = true
Expand Down Expand Up @@ -65,6 +69,7 @@ export function parseArgs (args: string[]): EnvCmdOptions {
rc,
options: {
expandEnvs,
recursive,
noOverride,
silent,
useShell,
Expand All @@ -90,7 +95,8 @@ export function parseArgsUsingCommander (args: string[]): commander.Command {
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('--use-shell', 'Execute the command in a new shell with the given environment')
.option('--verbose', 'Print helpful debugging information')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.option('-x, --expand-envs', 'Replace $var and $\\{var\\} in args and command with environment variables')
.option('--recursive', 'Replace $var and $\\{var\\} in env file with the referenced environment variable')
.allowUnknownOption(true)
.parse(['_', '_', ...args])
}
2 changes: 1 addition & 1 deletion src/signal-termination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class TermSignals {
/**
* Terminate parent process helper
*/
public _terminateProcess (code?: number, signal?: NodeJS.Signals): void {
public _terminateProcess (code?: number, signal?: NodeJS.Signals): boolean | void {
if (signal !== undefined) {
return process.kill(process.pid, signal)
}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'>
commandArgs: string[]
options?: {
expandEnvs?: boolean
recursive?: boolean
noOverride?: boolean
silent?: boolean
useShell?: boolean
Expand Down
29 changes: 29 additions & 0 deletions test/env-cmd.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,35 @@ describe('EnvCmd', (): void => {
}
)

it('should spawn process with args expanded if recursive option is true',
async (): Promise<void> => {
getEnvVarsStub.returns({ PING: 'PONG', recursive: 'PING ${PING}' }) /* eslint-disable-line */
await envCmdLib.EnvCmd({
command: 'node',
commandArgs: [],
envFile: {
filePath: './.env',
fallback: true
},
rc: {
environments: ['dev'],
filePath: './.rc'
},
options: {
recursive: true
}
})

const spawnArgs = spawnStub.args[0]

assert.equal(getEnvVarsStub.callCount, 1, 'getEnvVars must be called once')
assert.equal(spawnStub.callCount, 1)
assert.isAtLeast(expandEnvsSpy.callCount, 3, 'total number of env args')
assert.equal(spawnArgs[0], 'node')
assert.equal(spawnArgs[2].env.recursive, 'PING PONG')
}
)

it('should ignore errors if silent flag provided',
async (): Promise<void> => {
delete process.env.BOB
Expand Down
4 changes: 2 additions & 2 deletions test/expand-envs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('expandEnvs', (): void => {
PING: 'PONG',
IP1: '127.0.0.1'
}
const args = ['notvar', '$dollar', '\\$notvar', '-4', '$PING', '$IP1', '\\$IP1', '$NONEXIST']
const argsExpanded = ['notvar', 'money', '\\$notvar', '-4', 'PONG', '127.0.0.1', '\\$IP1', '$NONEXIST']
const args = ['notvar', '$dollar', '\\$notvar', '-4', '$PING', '$IP1', '\\$IP1', '$NONEXIST', '${PING}', '${NONEXIST}'] /* eslint-disable-line */
const argsExpanded = ['notvar', 'money', '\\$notvar', '-4', 'PONG', '127.0.0.1', '\\$IP1', '$NONEXIST', 'PONG', '${NONEXIST}'] /* eslint-disable-line */

it('should replace environment variables in args', (): void => {
const res = args.map(arg => expandEnvs(arg, envs))
Expand Down
6 changes: 6 additions & 0 deletions test/parse-args.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ describe('parseArgs', (): void => {
assert.isTrue(res.options!.expandEnvs)
})

it('should parse recursive option', (): void => {
const res = parseArgs(['-f', envFilePath, '--recursive', command, ...commandArgs])
assert.exists(res.envFile)
assert.isTrue(res.options!.recursive)
})

it('should parse silent option', (): void => {
const res = parseArgs(['-f', envFilePath, '--silent', command, ...commandArgs])
assert.exists(res.envFile)
Expand Down