diff --git a/dist/env-cmd.js b/dist/env-cmd.js index e02865f..a99077e 100644 --- a/dist/env-cmd.js +++ b/dist/env-cmd.js @@ -33,11 +33,15 @@ export async function EnvCmd({ command, commandArgs, envFile, rc, options = {}, command = expandEnvs(command, env); commandArgs = commandArgs.map(arg => expandEnvs(arg, env)); } + if (!command) { + throw new Error('env-cmd cannot be used as a standalone command. ' + + 'Refer to the documentation for usage examples: https://npm.im/env-cmd'); + } // Execute the command with the given environment variables const proc = spawn(command, commandArgs, { stdio: 'inherit', shell: options.useShell, - env: env, + env, }); // Handle any termination signals for parent and child proceses const signals = new TermSignals({ verbose: options.verbose }); diff --git a/dist/parse-args.js b/dist/parse-args.js index 4c7a632..1af336a 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -94,5 +94,6 @@ export function parseArgsUsingCommander(args) { .option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--verbose', 'Print helpful debugging information') .allowUnknownOption(true) + .allowExcessArguments(true) .parse(['_', '_', ...args], { from: 'node' }); } diff --git a/dist/parse-env-file.d.ts b/dist/parse-env-file.d.ts index 299c2f9..b5661c6 100644 --- a/dist/parse-env-file.d.ts +++ b/dist/parse-env-file.d.ts @@ -19,3 +19,12 @@ export declare function stripComments(envString: string): string; * Strips out newlines from env file string */ export declare function stripEmptyLines(envString: string): string; +/** + * If we load data from a file like .js, the user + * might export something which is not an object. + * + * This function ensures that the input is valid, + * and converts the object's values to strings, for + * consistincy. See issue #125 for details. + */ +export declare function normalizeEnvObject(input: unknown, absolutePath: string): Environment; diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index 00e3cc8..1d430ab 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -14,7 +14,7 @@ export async function getEnvFileVars(envFilePath) { } // Get the file extension const ext = extname(absolutePath).toLowerCase(); - let env = {}; + let env; if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them let attributeTypes = {}; @@ -22,7 +22,7 @@ export async function getEnvFileVars(envFilePath) { attributeTypes = { [importAttributesKeyword]: { type: 'json' } }; } const res = await import(pathToFileURL(absolutePath).href, attributeTypes); - if ('default' in res) { + if (typeof res === 'object' && res && 'default' in res) { env = res.default; } else { @@ -32,12 +32,14 @@ export async function getEnvFileVars(envFilePath) { if (isPromise(env)) { env = await env; } + return normalizeEnvObject(env, absolutePath); } - else { - const file = readFileSync(absolutePath, { encoding: 'utf8' }); - env = parseEnvString(file); + const file = readFileSync(absolutePath, { encoding: 'utf8' }); + switch (ext) { + // other loaders can be added here + default: + return parseEnvString(file); } - return env; } /** * Parse out all env vars from a given env file string and return an object @@ -61,23 +63,18 @@ export function parseEnvVars(envString) { // Note: match[1] is the full env=var line const key = match[2].trim(); let value = match[3].trim(); - // remove any surrounding quotes - value = value - .replace(/(^['"]|['"]$)/g, '') - .replace(/\\n/g, '\n'); - // Convert string to JS type if appropriate - if (value !== '' && !isNaN(+value)) { - matches[key] = +value; - } - else if (value === 'true') { - matches[key] = true; - } - else if (value === 'false') { - matches[key] = false; + // if the string is quoted, remove everything after the final + // quote. This implicitly removes inline comments. + if (value.startsWith("'") || value.startsWith('"')) { + value = value.slice(1, value.lastIndexOf(value[0])); } else { - matches[key] = value; + // if the string is not quoted, we need to explicitly remove + // inline comments. + value = value.split('#')[0].trim(); } + value = value.replace(/\\n/g, '\n'); + matches[key] = value; } return JSON.parse(JSON.stringify(matches)); } @@ -95,3 +92,25 @@ export function stripEmptyLines(envString) { const emptyLinesRegex = /(^\n)/gim; return envString.replace(emptyLinesRegex, ''); } +/** + * If we load data from a file like .js, the user + * might export something which is not an object. + * + * This function ensures that the input is valid, + * and converts the object's values to strings, for + * consistincy. See issue #125 for details. + */ +export function normalizeEnvObject(input, absolutePath) { + if (typeof input !== 'object' || !input) { + throw new Error(`env-cmd cannot load “${absolutePath}” because it does not export an object.`); + } + const env = {}; + for (const [key, value] of Object.entries(input)) { + // we're intentionally stringifying the value here, to + // match what `child_process.spawn` does when loading + // env variables. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + env[key] = `${value}`; + } + return env; +} diff --git a/dist/types.d.ts b/dist/types.d.ts index 92f1ba3..483442f 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,5 +1,5 @@ import type { Command } from '@commander-js/extra-typings'; -export type Environment = Partial>; +export type Environment = Partial>; export type RCEnvironment = Partial>; export type CommanderOptions = Command<[], { environments?: true | string[]; diff --git a/package.json b/package.json index 5a514bd..62db792 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ }, "homepage": "https://github.com/toddbluhm/env-cmd#readme", "dependencies": { - "@commander-js/extra-typings": "^12.1.0", - "commander": "^12.1.0", + "@commander-js/extra-typings": "^13.1.0", + "commander": "^13.1.0", "cross-spawn": "^7.0.6" }, "devDependencies": { diff --git a/src/parse-args.ts b/src/parse-args.ts index 8f0120c..03fbad4 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -108,5 +108,6 @@ export function parseArgsUsingCommander(args: string[]): CommanderOptions { .option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--verbose', 'Print helpful debugging information') .allowUnknownOption(true) + .allowExcessArguments(true) .parse(['_', '_', ...args], { from: 'node' }) }