Skip to content

Commit

Permalink
Merge 5917041 into 8f35b71
Browse files Browse the repository at this point in the history
  • Loading branch information
nkrul committed Mar 10, 2021
2 parents 8f35b71 + 5917041 commit 1684270
Show file tree
Hide file tree
Showing 24 changed files with 106 additions and 23 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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
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
@@ -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
Expand Up @@ -39,7 +39,8 @@
"Eric Lanehart <eric@pushred.co>",
"Jon Scheiding <jonscheiding@gmail.com>",
"serapath (Alexander Praetorius) <dev@serapath.de>",
"Anton Versal <ant.ver@gmail.com>"
"Anton Versal <ant.ver@gmail.com>",
"Nicholas Krul <nicholas.krul@gmail.com>"
],
"license": "MIT",
"bugs": {
Expand Down
6 changes: 6 additions & 0 deletions src/env-cmd.ts
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
@@ -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
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
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
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
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
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
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

0 comments on commit 1684270

Please sign in to comment.