diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f6d49..c259df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ # Changelog -## 10.1.1 - In Development +## Landed in master -- **Upgrade**: Upgraded dependency `commander` to `5.x` -- **Upgrade**: Upgraded devDependencies `ts-standard`, `sinon` +- **Upgrade**: Upgraded dependency `commander` to `13.x` +- **Upgrade**: Upgraded dependency `cross-spawn` to `7.x` +- **Upgrade**: Upgraded all 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` diff --git a/README.md b/README.md index 52b148e..b7b3fef 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,16 @@ Usage: env-cmd [options] -- [...args] Options: -v, --version output the version number -e, --environments [envs...] The rc file environment(s) to use - -f, --file [path] Custom env file path or .rc file path if '-e' used (default path: ./.env or - ./.env-cmdrc.(js|cjs|mjs|json)) - -x, --expand-envs Replace $var in args and command with environment variables + -f, --file [path] Custom env file path or .rc file path if '-e' used (default path: ./.env or ./.env-cmdrc.(js|cjs|mjs|json)) + -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 --fallback Fallback to default env file path, if custom env file path not found --no-override Do not override existing environment variables --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 -h, --help display help for command + ``` ## 🔬 Advanced Usage @@ -129,14 +130,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 ``` diff --git a/dist/env-cmd.js b/dist/env-cmd.js index a99077e..e4d0569 100644 --- a/dist/env-cmd.js +++ b/dist/env-cmd.js @@ -29,6 +29,13 @@ 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)) { + if (env[key] !== undefined) { + env[key] = expandEnvs(env[key], env); + } + } + } if (options.expandEnvs === true) { command = expandEnvs(command, env); commandArgs = commandArgs.map(arg => expandEnvs(arg, env)); diff --git a/dist/expand-envs.d.ts b/dist/expand-envs.d.ts index 7706ca7..c77ac26 100644 --- a/dist/expand-envs.d.ts +++ b/dist/expand-envs.d.ts @@ -1,6 +1,6 @@ import type { Environment } from './types.ts'; /** - * expandEnvs Replaces $var in args and command with environment variables + * expandEnvs Replaces $var and ${var} in args and command with environment variables * if the environment variable doesn't exist, it leaves it as is. */ export declare function expandEnvs(str: string, envs: Environment): string; diff --git a/dist/expand-envs.js b/dist/expand-envs.js index 59346d2..7fb5fd5 100644 --- a/dist/expand-envs.js +++ b/dist/expand-envs.js @@ -1,11 +1,10 @@ /** - * expandEnvs Replaces $var in args and command with environment variables + * expandEnvs Replaces $var and ${var} in args and command with environment variables * if the environment variable doesn't exist, it leaves it as is. */ export function expandEnvs(str, envs) { - return str.replace(/(? { - const varValue = envs[varName.slice(1)]; - // const test = 42; + return str.replace(/(? { + const varValue = envs[varName.startsWith('${') ? varName.slice(2, varName.length - 1) : varName.slice(1)]; return varValue ?? varName; }); } diff --git a/dist/parse-args.js b/dist/parse-args.js index 792001f..58a5ed9 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 (parsedCmdOptions.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,7 +88,8 @@ 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('--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/dist/types.d.ts b/dist/types.d.ts index 797988d..fbdd6bb 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -4,6 +4,7 @@ export type RCEnvironment = Partial>; export type CommanderOptions = Command<[], { environments?: true | string[]; expandEnvs?: boolean; + recursive?: boolean; fallback?: boolean; file?: true | string; override?: boolean; @@ -29,6 +30,7 @@ export interface EnvCmdOptions extends GetEnvVarOptions { commandArgs: string[]; options?: { expandEnvs?: boolean; + recursive?: boolean; noOverride?: boolean; silent?: boolean; useShell?: boolean; diff --git a/package.json b/package.json index 27420e7..6b9c8aa 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,12 @@ ], "author": "Todd Bluhm", "contributors": [ + "Anton Versal ", "Eric Lanehart ", "Jon Scheiding ", - "serapath (Alexander Praetorius) ", "Kyle Hensel ", - "Anton Versal " + "Nicholas Krul ", + "serapath (Alexander Praetorius) " ], "license": "MIT", "bugs": { diff --git a/src/env-cmd.ts b/src/env-cmd.ts index fe24739..947a575 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -40,6 +40,14 @@ export async function EnvCmd( env = Object.assign({}, processLib.env, env) } + if (options.recursive === true) { + for (const key of Object.keys(env)) { + if (env[key] !== undefined) { + 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/expand-envs.ts b/src/expand-envs.ts index 60b66cf..958405d 100644 --- a/src/expand-envs.ts +++ b/src/expand-envs.ts @@ -1,13 +1,12 @@ import type { Environment } from './types.ts' /** - * expandEnvs Replaces $var in args and command with environment variables + * expandEnvs Replaces $var and ${var} in args and command with environment variables * if the environment variable doesn't exist, it leaves it as is. */ -export function expandEnvs(str: string, envs: Environment): string { - return str.replace(/(? { - const varValue = envs[varName.slice(1)] - // const test = 42; +export function expandEnvs (str: string, envs: Environment): string { + return str.replace(/(? { + const varValue = envs[varName.startsWith('${') ? varName.slice(2, varName.length - 1) : varName.slice(1)] return varValue ?? varName }) } diff --git a/src/parse-args.ts b/src/parse-args.ts index 74dcdd7..03bdb25 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 (parsedCmdOptions.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, @@ -95,7 +100,8 @@ export function parseArgsUsingCommander(args: string[]): CommanderOptions { .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('--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..e304330 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ export type RCEnvironment = Partial> export type CommanderOptions = Command<[], { environments?: true | string[] expandEnvs?: boolean // Default: false + recursive?: boolean // Default: false fallback?: boolean // Default false file?: true | string override?: boolean // Default: false @@ -37,6 +38,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..838bc65 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}' }) + 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/expand-envs.spec.ts b/test/expand-envs.spec.ts index fb9c605..ca348e8 100644 --- a/test/expand-envs.spec.ts +++ b/test/expand-envs.spec.ts @@ -8,11 +8,22 @@ describe('expandEnvs', (): void => { dollar: 'money', PING: 'PONG', IP1: '127.0.0.1', - THANKSFORALLTHEFISH: 42, - BRINGATOWEL: true, + THANKSFORALLTHEFISH: '42', + BRINGATOWEL: 'true', } - 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}', '\\${PING}', + '$PING}', '${PING2' + ] + const argsExpanded = [ + 'notvar', 'money', '\\$notvar', '-4', + 'PONG', '127.0.0.1', '\\$IP1', '$NONEXIST', + 'PONG', '${NONEXIST}', '\\${PING}', + 'PONG}', '${PING2' + ] it('should replace environment variables in args', (): void => { const res = args.map(arg => expandEnvs(arg, envs)) 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)