From 53f454900170d837840b58c323110a30498a3fca Mon Sep 17 00:00:00 2001 From: Nicholas Krul Date: Wed, 10 Mar 2021 14:12:48 +1100 Subject: [PATCH] feat: recursive embedding of env vars in env vars --- CHANGELOG.md | 1 + dist/env-cmd.js | 5 +++++ dist/parse-args.js | 9 ++++++++- dist/types.d.ts | 1 + src/env-cmd.ts | 6 ++++++ src/parse-args.ts | 6 ++++++ src/types.ts | 1 + test/env-cmd.spec.ts | 29 +++++++++++++++++++++++++++++ test/parse-args.spec.ts | 6 ++++++ 9 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f590f55..991a1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - **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 diff --git a/dist/env-cmd.js b/dist/env-cmd.js index a99077e..ab21cf1 100644 --- a/dist/env-cmd.js +++ b/dist/env-cmd.js @@ -29,6 +29,11 @@ export async function EnvCmd({ command, commandArgs, envFile, rc, options = {}, // Add in the system environment variables to our environment list env = Object.assign({}, processLib.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 = expandEnvs(command, env); commandArgs = commandArgs.map(arg => expandEnvs(arg, env)); diff --git a/dist/parse-args.js b/dist/parse-args.js index 792001f..5a569ef 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -28,6 +28,10 @@ export function parseArgs(args) { if (parsedCmdOptions.expandEnvs === true) { expandEnvs = true; } + let recursive = false; + if (program.recursive === true) { + recursive = true; + } let verbose = false; if (parsedCmdOptions.verbose === true) { verbose = true; @@ -65,6 +69,7 @@ export function parseArgs(args) { rc, options: { expandEnvs, + recursive, noOverride, silent, useShell, @@ -83,16 +88,18 @@ export function parseArgsUsingCommander(args) { .usage('[options] -- [...args]') .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList) .option('-f, --file [path]', 'Custom env file path or .rc file path if \'-e\' used (default path: ./.env or ./.env-cmdrc.(js|cjs|mjs|json))') - .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('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') .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('--recursive', 'Replace $var and $\\{var\\} in env file with the referenced environment variable') // TODO: Remove -r deprecation error on version >= v12 .addOption(new Option('-r, --rc-file [path]', 'Deprecated Option') .hideHelp() .argParser(() => { throw new CommanderError(1, 'deprecated-option', 'The -r flag has been deprecated, use the -f flag instead.'); })) + // ENDTODO .allowUnknownOption(true) .allowExcessArguments(true) .parse(['_', '_', ...args], { from: 'node' }); diff --git a/dist/types.d.ts b/dist/types.d.ts index 797988d..24a260b 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -29,6 +29,7 @@ export interface EnvCmdOptions extends GetEnvVarOptions { commandArgs: string[]; options?: { expandEnvs?: boolean; + recursive?: boolean; noOverride?: boolean; silent?: boolean; useShell?: boolean; diff --git a/src/env-cmd.ts b/src/env-cmd.ts index fe24739..4c6b8d5 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -40,6 +40,12 @@ export async function EnvCmd( env = Object.assign({}, processLib.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)) diff --git a/src/parse-args.ts b/src/parse-args.ts index 74dcdd7..7b59b93 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -33,6 +33,10 @@ export function parseArgs(args: string[]): EnvCmdOptions { if (parsedCmdOptions.expandEnvs === true) { expandEnvs = true } + let recursive = false + if (program.recursive === true) { + recursive = true + } let verbose = false if (parsedCmdOptions.verbose === true) { verbose = true @@ -75,6 +79,7 @@ export function parseArgs(args: string[]): EnvCmdOptions { rc, options: { expandEnvs, + recursive, noOverride, silent, useShell, @@ -96,6 +101,7 @@ export function parseArgsUsingCommander(args: string[]): CommanderOptions { .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList) .option('-f, --file [path]', 'Custom env file path or .rc file path if \'-e\' used (default path: ./.env or ./.env-cmdrc.(js|cjs|mjs|json))') .option('-x, --expand-envs', 'Replace $var in args and command with environment variables') + .option('--recursive', 'Replace $var and $\\{var\\} in env file with the referenced environment variable') .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('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.') diff --git a/src/types.ts b/src/types.ts index c9fc0c0..0d16455 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,7 @@ export interface EnvCmdOptions extends GetEnvVarOptions { commandArgs: string[] options?: { expandEnvs?: boolean + recursive?: boolean noOverride?: boolean silent?: boolean useShell?: boolean diff --git a/test/env-cmd.spec.ts b/test/env-cmd.spec.ts index 7372df9..9b14420 100644 --- a/test/env-cmd.spec.ts +++ b/test/env-cmd.spec.ts @@ -169,6 +169,35 @@ describe('EnvCmd', (): void => { }, ) + it('should spawn process with args expanded if recursive option is true', + async (): Promise => { + 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 => { delete process.env.BOB diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index 30e19ac..1e762b0 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -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)