diff --git a/mod.test.ts b/mod.test.ts index 2bde4f6..598930b 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -691,6 +691,37 @@ Deno.test("exporting env should modify real environment when something changed v } }); +Deno.test("env should be clean slate when clearEnv is set", async () => { + { + const text = await $`printenv`.clearEnv().text(); + assertEquals(text, ""); + } + Deno.env.set("DAX_TVAR", "123"); + try { + const text = await $`deno eval 'console.log("DAX_TVAR: " + Deno.env.get("DAX_TVAR"))'`.clearEnv().text(); + assertEquals(text, "DAX_TVAR: undefined"); + } finally { + Deno.env.delete("DAX_TVAR"); + } +}); + +Deno.test("clearEnv + exportEnv should not clear out real environment", async () => { + Deno.env.set("DAX_TVAR", "123"); + try { + const text = + await $`deno eval 'console.log("VAR: " + Deno.env.get("DAX_TVAR") + " VAR2: " + Deno.env.get("DAX_TVAR2"))'` + .env("DAX_TVAR2", "shake it shake") + .clearEnv() + .exportEnv() + .text(); + assertEquals(text, "VAR: undefined VAR2: shake it shake"); + assertEquals(Deno.env.get("DAX_TVAR2"), "shake it shake"); + } finally { + Deno.env.delete("DAX_TVAR"); + Deno.env.delete("DAX_TVAR2"); + } +}); + Deno.test("setting an empty env var", async () => { const text = await $`VAR= deno eval 'console.log("VAR: " + Deno.env.get("VAR"))'`.text(); assertEquals(text, "VAR: "); diff --git a/src/command.ts b/src/command.ts index 7b2314e..9969d20 100644 --- a/src/command.ts +++ b/src/command.ts @@ -79,6 +79,7 @@ interface CommandBuilderState { env: Record; commands: Record; cwd: string | undefined; + clearEnv: boolean; exportEnv: boolean; printCommand: boolean; printCommandLogger: LoggerTreeBox; @@ -147,6 +148,7 @@ export class CommandBuilder implements PromiseLike { env: {}, cwd: undefined, commands: { ...builtInCommands }, + clearEnv: false, exportEnv: false, printCommand: false, printCommandLogger: new LoggerTreeBox( @@ -176,6 +178,7 @@ export class CommandBuilder implements PromiseLike { env: { ...state.env }, cwd: state.cwd, commands: { ...state.commands }, + clearEnv: state.clearEnv, exportEnv: state.exportEnv, printCommand: state.printCommand, printCommandLogger: state.printCommandLogger.createChild(), @@ -454,6 +457,18 @@ export class CommandBuilder implements PromiseLike { }); } + /** + * Clear environmental variables from parent process. + * + * Doesn't guarantee that only `env` variables are present, as the OS may + * set environmental variables for processes. + */ + clearEnv(value = true): CommandBuilder { + return this.#newWithState((state) => { + state.clearEnv = value; + }); + } + /** * Prints the command text before executing the command. * @@ -742,10 +757,11 @@ export function parseAndSpawnCommand(state: CommandBuilderState) { stdin: stdin instanceof ReadableStream ? readerFromStreamReader(stdin.getReader()) : stdin, stdout, stderr, - env: buildEnv(state.env), + env: buildEnv(state.env, state.clearEnv), commands: state.commands, cwd: state.cwd ?? Deno.cwd(), exportEnv: state.exportEnv, + clearedEnv: state.clearEnv, signal, fds, }); @@ -1083,8 +1099,8 @@ export class CommandResult { } } -function buildEnv(env: Record) { - const result = Deno.env.toObject(); +function buildEnv(env: Record, clearEnv: boolean) { + const result = clearEnv ? {} : Deno.env.toObject(); for (const [key, value] of Object.entries(env)) { if (value == null) { delete result[key]; diff --git a/src/shell.ts b/src/shell.ts index 1f36812..4d57dce 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -237,6 +237,41 @@ class ShellEnv implements Env { } } +/** + * Like {@link RealEnv} but any read actions will access values that were only set through it + * or return undefined. + */ +class RealEnvWriteOnly implements Env { + real = new RealEnv(); + shell = new ShellEnv(); + + setCwd(cwd: string) { + this.real.setCwd(cwd); + this.shell.setCwd(cwd); + } + + getCwd() { + return this.shell.getCwd(); + } + + setEnvVar(key: string, value: string | undefined) { + this.real.setEnvVar(key, value); + this.shell.setEnvVar(key, value); + } + + getEnvVar(key: string) { + return this.shell.getEnvVar(key); + } + + getEnvVars() { + return this.shell.getEnvVars(); + } + + clone(): Env { + return cloneEnv(this); + } +} + function initializeEnv(env: Env, opts: ShellEnvOpts) { env.setCwd(opts.cwd); for (const [key, value] of Object.entries(opts.env)) { @@ -481,12 +516,13 @@ export interface SpawnOpts { commands: Record; cwd: string; exportEnv: boolean; + clearedEnv: boolean; signal: KillSignal; fds: StreamFds | undefined; } export async function spawn(list: SequentialList, opts: SpawnOpts) { - const env = opts.exportEnv ? new RealEnv() : new ShellEnv(); + const env = opts.exportEnv ? opts.clearedEnv ? new RealEnvWriteOnly() : new RealEnv() : new ShellEnv(); initializeEnv(env, opts); const context = new Context({ env,