Skip to content

Commit

Permalink
feat: clearEnv on CommandBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
Yohe-Am committed Apr 10, 2024
1 parent 81f2ca1 commit dc1405b
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 4 deletions.
40 changes: 40 additions & 0 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,46 @@ 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, "");
}
{
const text = await $`deno eval 'console.log(JSON.stringify(Deno.env.toObject()))'`.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 text1 = await $`deno eval 'console.log(JSON.stringify(Deno.env.toObject()))'`
.clearEnv()
.exportEnv()
.text();
assertEquals(text1, "{}");
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: ");
Expand Down
23 changes: 20 additions & 3 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ interface CommandBuilderState {
env: Record<string, string | undefined>;
commands: Record<string, CommandHandler>;
cwd: string | undefined;
clearEnv: boolean;
exportEnv: boolean;
printCommand: boolean;
printCommandLogger: LoggerTreeBox;
Expand Down Expand Up @@ -147,6 +148,7 @@ export class CommandBuilder implements PromiseLike<CommandResult> {
env: {},
cwd: undefined,
commands: { ...builtInCommands },
clearEnv: false,
exportEnv: false,
printCommand: false,
printCommandLogger: new LoggerTreeBox(
Expand Down Expand Up @@ -176,6 +178,7 @@ export class CommandBuilder implements PromiseLike<CommandResult> {
env: { ...state.env },
cwd: state.cwd,
commands: { ...state.commands },
clearEnv: state.clearEnv,
exportEnv: state.exportEnv,
printCommand: state.printCommand,
printCommandLogger: state.printCommandLogger.createChild(),
Expand Down Expand Up @@ -454,6 +457,18 @@ export class CommandBuilder implements PromiseLike<CommandResult> {
});
}

/**
* 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.
*
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -1083,15 +1099,16 @@ export class CommandResult {
}
}

function buildEnv(env: Record<string, string | undefined>) {
const result = Deno.env.toObject();
function buildEnv(env: Record<string, string | undefined>, clearEnv: boolean) {
const result = clearEnv ? {} : Deno.env.toObject();
for (const [key, value] of Object.entries(env)) {
if (value == null) {
delete result[key];
} else {
result[key] = value;
}
}
console.log({ result });
return result;
}

Expand Down
38 changes: 37 additions & 1 deletion src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -481,12 +516,13 @@ export interface SpawnOpts {
commands: Record<string, CommandHandler>;
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,
Expand Down

0 comments on commit dc1405b

Please sign in to comment.