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: adds "moustage" syntax {{VAR}} variable interpolation / expand support #344

Closed
wants to merge 2 commits into from
Closed
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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ Options:
-e, --environments [env1,env2,...] The rc file environment(s) to use
-f, --file [path] Custom env file path (default path: ./.env)
--fallback Fallback to default env file path, if custom env file path not found
--no-override Do not override existing environment variables
-n, --no-override Do not override existing environment variables
-r, --rc-file [path] Custom rc file path (default path: ./.env-cmdrc(|.js|.json)
--silent Ignore any env-cmd errors and only fail on executed program failure.
-s, --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
Expand Down Expand Up @@ -165,6 +165,30 @@ or in `package.json` (use `\\` to insert a literal backslash)
}
```

### `-i` interpolate vars in arguments

EnvCmd supports interpolation `{{var}}` values passed in as arguments to the command. The allows a user
to provide arguments using "moustache" syntax to a command that are based on environment variable values at runtime.

**NOTE:** Main difference between `-i` and `-x` is that you do not need to escape the `{{` & `}}` characters with `\`
unlike using `$` in order to avoid terminal trying to auto expand it before passing it to `env-cmd`.

**Terminal**

```sh
# $VAR will be expanded into the env value it contains at runtime
./node_modules/.bin/env-cmd -x node index.js --arg={{VAR}}
```

or in `package.json`
```json
{
"script": {
"start": "env-cmd -x node index.js --arg={{VAR}}"
}
}
```


### `--silent` suppresses env-cmd errors

Expand Down Expand Up @@ -222,6 +246,7 @@ A function that executes a given command in a new child process with the given e
- **`filePath`** { `string` }: Custom path to the `.rc` file (defaults to: `./.env-cmdrc(|.js|.json)`)
- **`options`** { `object` }
- **`expandEnvs`** { `boolean` }: Expand `$var` values passed to `commandArgs` (default: `false`)
- **`interpolateEnvs`** { `boolean` }: Interpolates `{{var}}` values passed to `commandArgs` (default: `false`)
- **`noOverride`** { `boolean` }: Prevent `.env` file vars from overriding existing `process.env` vars (default: `false`)
- **`silent`** { `boolean` }: Ignore any errors thrown by env-cmd, used to ignore missing file errors (default: `false`)
- **`useShell`** { `boolean` }: Runs command inside a new shell instance (default: `false`)
Expand Down
6 changes: 6 additions & 0 deletions dist/env-cmd.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"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");
const get_env_vars_1 = require("./get-env-vars");
const expand_envs_1 = require("./expand-envs");
const interpolate_envs_1 = require("./interpolate-envs");
/**
* Executes env - cmd using command line arguments
* @export
Expand Down Expand Up @@ -55,6 +57,10 @@ async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) {
command = expand_envs_1.expandEnvs(command, env);
commandArgs = commandArgs.map(arg => expand_envs_1.expandEnvs(arg, env));
}
if (options.interpolateEnvs === true) {
command = expand_envs_1.expandEnvs(command, env);
commandArgs = commandArgs.map(arg => interpolate_envs_1.interpolateEnvs(arg, env));
}
// Execute the command with the given environment variables
const proc = spawn_1.spawn(command, commandArgs, {
stdio: 'inherit',
Expand Down
1 change: 1 addition & 0 deletions dist/expand-envs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.expandEnvs = void 0;
/**
* expandEnvs Replaces $var in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
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;
7 changes: 7 additions & 0 deletions dist/interpolate-envs.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* interpolateEnvs Replaces {{var}} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
export declare function interpolateEnvs(str: string, envs: {
[key: string]: any;
}): string;
14 changes: 14 additions & 0 deletions dist/interpolate-envs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.interpolateEnvs = void 0;
/**
* interpolateEnvs Replaces {{var}} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
function interpolateEnvs(str, envs) {
return str.replace(/(?<!\\){{[a-zA-Z0-9_]+}}/g, varName => {
const varValue = envs[varName.replace(/{|}/g, '')];
return varValue !== undefined ? varValue : varName;
});
}
exports.interpolateEnvs = interpolateEnvs;
13 changes: 10 additions & 3 deletions 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 interpolate = false;
if (program.interpolate === true) {
interpolate = true;
}
let verbose = false;
if (program.verbose === true) {
verbose = true;
Expand Down Expand Up @@ -62,7 +67,8 @@ function parseArgs(args) {
noOverride,
silent,
useShell,
verbose
verbose,
interpolate,
}
};
if (verbose) {
Expand All @@ -79,12 +85,13 @@ function parseArgsUsingCommander(args) {
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', utils_1.parseArgList)
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
.option('--fallback', 'Fallback to default env file path, if custom env file path not found')
.option('--no-override', 'Do not override existing environment variables')
.option('-n, --no-override', 'Do not override existing environment variables')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)')
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('-s, --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('-i, --interpolate', 'Interpolates {{var}} in args and command with environment variables')
.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
4 changes: 3 additions & 1 deletion 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 Expand Up @@ -82,7 +83,8 @@ class TermSignals {
*/
_terminateProcess(code, signal) {
if (signal !== undefined) {
return process.kill(process.pid, signal);
process.kill(process.pid, signal);
return;
}
if (code !== undefined) {
return process.exit(code);
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 @@ -18,5 +18,6 @@ export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'>
silent?: boolean;
useShell?: boolean;
verbose?: boolean;
interpolateEnvs?: boolean;
};
}
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "env-cmd",
"version": "10.1.0",
"version": "10.2.0",
"description": "Executes a command using the environment variables in an env file",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
5 changes: 5 additions & 0 deletions src/env-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TermSignals } from './signal-termination'
import { parseArgs } from './parse-args'
import { getEnvVars } from './get-env-vars'
import { expandEnvs } from './expand-envs'
import { interpolateEnvs } from './interpolate-envs';

/**
* Executes env - cmd using command line arguments
Expand Down Expand Up @@ -61,6 +62,10 @@ export async function EnvCmd (
command = expandEnvs(command, env)
commandArgs = commandArgs.map(arg => expandEnvs(arg, env))
}
if (options.interpolateEnvs === true) {
command = interpolateEnvs(command, env)
commandArgs = commandArgs.map(arg => interpolateEnvs(arg, env))
}

// Execute the command with the given environment variables
const proc = spawn(command, commandArgs, {
Expand Down
11 changes: 11 additions & 0 deletions src/interpolate-envs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

/**
* interpolateEnvs Replaces {{var}} in args and command with environment variables
* the environment variable doesn't exist, it leaves it as is.
*/
export function interpolateEnvs (str: string, envs: { [key: string]: any }): string {
return str.replace(/(?<!\\){{[a-zA-Z0-9_]+}}/g, varName => {
const varValue = envs[varName.replace(/{|}/g, '')]
return varValue !== undefined ? varValue : varName
})
}
13 changes: 10 additions & 3 deletions src/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export function parseArgs (args: string[]): EnvCmdOptions {
if (program.expandEnvs === true) {
expandEnvs = true
}
let interpolateEnvs = false
if (program.interpolate === true) {
interpolateEnvs = true
}

let verbose = false
if (program.verbose === true) {
verbose = true
Expand Down Expand Up @@ -68,7 +73,8 @@ export function parseArgs (args: string[]): EnvCmdOptions {
noOverride,
silent,
useShell,
verbose
verbose,
interpolateEnvs,
}
}
if (verbose) {
Expand All @@ -85,12 +91,13 @@ export function parseArgsUsingCommander (args: string[]): commander.Command {
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList)
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
.option('--fallback', 'Fallback to default env file path, if custom env file path not found')
.option('--no-override', 'Do not override existing environment variables')
.option('-n, --no-override', 'Do not override existing environment variables')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)')
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('-s, --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('-i, --interpolate', 'Interpolates {{var}} in args and command with environment variables')
.allowUnknownOption(true)
.parse(['_', '_', ...args])
}
3 changes: 2 additions & 1 deletion src/signal-termination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export class TermSignals {
*/
public _terminateProcess (code?: number, signal?: NodeJS.Signals): void {
if (signal !== undefined) {
return process.kill(process.pid, signal)
process.kill(process.pid, signal)
return
}
if (code !== undefined) {
return process.exit(code)
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface EnvCmdOptions extends Pick<GetEnvVarOptions, 'envFile' | 'rc'>
noOverride?: boolean
silent?: boolean
useShell?: boolean
verbose?: boolean
verbose?: boolean,
interpolateEnvs?: boolean,
}
}
33 changes: 33 additions & 0 deletions test/env-cmd.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as signalTermLib from '../src/signal-termination'
import * as parseArgsLib from '../src/parse-args'
import * as getEnvVarsLib from '../src/get-env-vars'
import * as expandEnvsLib from '../src/expand-envs'
import * as interpolateEnvsLib from '../src/interpolate-envs';
import * as spawnLib from '../src/spawn'
import * as envCmdLib from '../src/env-cmd'

Expand Down Expand Up @@ -52,6 +53,7 @@ describe('EnvCmd', (): void => {
let getEnvVarsStub: sinon.SinonStub<any, any>
let spawnStub: sinon.SinonStub<any, any>
let expandEnvsSpy: sinon.SinonSpy<any, any>
let interpolateEnvsSpy: sinon.SinonSpy<any, any>
before((): void => {
sandbox = sinon.createSandbox()
getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars')
Expand All @@ -61,6 +63,7 @@ describe('EnvCmd', (): void => {
kill: (): void => { /* Fake the kill method */ }
})
expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs')
interpolateEnvsSpy = sandbox.spy(interpolateEnvsLib, 'interpolateEnvs')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleUncaughtExceptions')
})
Expand Down Expand Up @@ -193,6 +196,36 @@ describe('EnvCmd', (): void => {
}
)

it('should spawn process with command and args interpolated if interpolation option is true',
async (): Promise<void> => {
getEnvVarsStub.returns({ PING: 'PONG', CMD: 'node' })
await envCmdLib.EnvCmd({
command: '{{CMD}}',
commandArgs: ['{{PING}}', '\\{{IP}}'],
envFile: {
filePath: './.env',
fallback: true
},
rc: {
environments: ['dev'],
filePath: './.rc'
},
options: {
interpolateEnvs: true
}
})

const spawnArgs = spawnStub.args[0]

assert.equal(getEnvVarsStub.callCount, 1, 'getEnvVars must be called once')
assert.equal(spawnStub.callCount, 1)
assert.equal(interpolateEnvsSpy.callCount, 3, 'command + number of args')
assert.equal(spawnArgs[0], 'node')
assert.sameOrderedMembers(spawnArgs[1], ['PONG', '\\{{IP}}'])
assert.equal(spawnArgs[2].env.PING, 'PONG')
}
)

it('should ignore errors if silent flag provided',
async (): Promise<void> => {
delete process.env.BOB
Expand Down
Loading