From 6cfa4467be0f40f6c93e07d19833b50771b3fefd Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 8 Jul 2024 16:25:19 +0100 Subject: [PATCH] Support environment variables that persist beyond single commands --- src/commands/builtin_command_runner.ts | 8 +++-- src/commands/wasm_command_runner.ts | 20 +++++++---- src/context.ts | 8 ++++- src/environment.ts | 48 ++++++++++++++++++++++++++ src/shell.ts | 20 +++++++---- tests/commands/cat.test.ts | 2 +- tests/commands/cd.test.ts | 11 ++++++ tests/commands/env.test.ts | 13 +++++++ 8 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/environment.ts create mode 100644 tests/commands/cd.test.ts create mode 100644 tests/commands/env.test.ts diff --git a/src/commands/builtin_command_runner.ts b/src/commands/builtin_command_runner.ts index 4ec8acf..5ca69b6 100644 --- a/src/commands/builtin_command_runner.ts +++ b/src/commands/builtin_command_runner.ts @@ -21,8 +21,10 @@ export class BuiltinCommandRunner implements ICommandRunner { return; } const path = args[0] // Ignore other arguments? - // Need to handle path of "-". Maybe previous path is in an env var? - context.fileSystem.FS.chdir(path) - // Need to set PWD env var? + // Need to handle path of "-". Maybe previous path is in an env var? "OLDPWD" + + const { FS } = context.fileSystem + FS.chdir(path) + context.environment.set("PWD", FS.cwd()) } } diff --git a/src/commands/wasm_command_runner.ts b/src/commands/wasm_command_runner.ts index 0111cb1..7c180d1 100644 --- a/src/commands/wasm_command_runner.ts +++ b/src/commands/wasm_command_runner.ts @@ -17,20 +17,28 @@ export abstract class WasmCommandRunner implements ICommandRunner { noInitialRun: true, print: (text: string) => stdout.write(`${text}\n`), printErr: (text: string) => stdout.write(`${text}\n`), // Should be stderr + preRun: (module: any) => { + // Use PROXYFS so that command sees the shared FS. + const FS = module.FS + FS.mkdir(mountpoint, 0o777) + FS.mount(fileSystem.PROXYFS, { root: mountpoint, fs: fileSystem.FS }, mountpoint) + FS.chdir(fileSystem.FS.cwd()) + + // Copy environment variables into command. + context.environment.copyIntoCommand(module.ENV) + }, }) - - // Need to use PROXYFS so that command sees the shared FS. - const FS = wasm.FS - FS.mkdir(mountpoint, 0o777) - FS.mount(fileSystem.PROXYFS, { root: mountpoint, fs: fileSystem.FS }, mountpoint) - FS.chdir(fileSystem.FS.cwd()) const loaded = Date.now() wasm.callMain(args) + const FS = wasm.FS FS.close(FS.streams[1]) FS.close(FS.streams[2]) + // Copy environment variables back from command. + context.environment.copyFromCommand(wasm.getEnvStrings()) + const end = Date.now() console.log(`${cmdName} load time ${loaded-start} ms, run time ${end-loaded} ms`) } diff --git a/src/context.ts b/src/context.ts index 79397fb..8048357 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,3 +1,4 @@ +import { Environment } from "./environment" import { IFileSystem } from "./file_system" import { Output } from "./io" @@ -9,10 +10,15 @@ export class Context { readonly args: string[], readonly fileSystem: IFileSystem, readonly mountpoint: string, + environment: Environment, readonly stdout: Output, - ) {} + ) { + this.environment = environment + } async flush(): Promise { await this.stdout.flush() } + + environment: Environment } diff --git a/src/environment.ts b/src/environment.ts new file mode 100644 index 0000000..7cce710 --- /dev/null +++ b/src/environment.ts @@ -0,0 +1,48 @@ +/** + * Collection of environment variables that are known to a shell and are passed in and out of + * commands. + */ +export class Environment { + constructor() { + this._env.set("PS1", "\x1b[1;31mjs-shell:$\x1b[1;0m ") // red color + } + + /** + * Copy environment variables back from a command after it has run. + */ + copyFromCommand(source: string[]) { + for (const str of source) { + const split = str.split("=") + const key = split.shift() + if (key && !this._ignore.has(key)) { + this._env.set(key, split.join("=")) + } + } + } + + /** + * Copy environment variables into a command before it is run. + */ + copyIntoCommand(target: { [key: string]: string }) { + for (const [key, value] of this._env.entries()) { + target[key] = value + } + } + + get(key: string): string | null { + return this._env.get(key) ?? null + } + + getPrompt(): string { + return this._env.get("PS1") ?? "$ " + } + + set(key: string, value: string) { + this._env.set(key, value) + } + + private _env: Map = new Map() + + // Keys to ignore when copying back from a command's env vars. + private _ignore: Set = new Set(["USER", "LOGNAME", "HOME", "LANG", "_"]) +} diff --git a/src/shell.ts b/src/shell.ts index 0d5cf58..957de4b 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -1,5 +1,6 @@ import { CommandRegistry } from "./command_registry" import { Context } from "./context" +import { Environment } from "./environment" import { IFileSystem } from "./file_system" import { TerminalOutput } from "./io" import { IOutputCallback } from "./output_callback" @@ -14,7 +15,11 @@ export class Shell { this._outputCallback = outputCallback this._mountpoint = mountpoint; this._currentLine = "" - this._prompt = "\x1b[1;31mjs-shell:$\x1b[1;0m " // red color + this._environment = new Environment() + } + + get environment(): Environment { + return this._environment } async input(char: string): Promise { @@ -26,7 +31,7 @@ export class Shell { const cmdText = this._currentLine.trimStart() this._currentLine = "" await this._runCommands(cmdText) - await this.output(this._prompt) + await this.output(this._environment.getPrompt()) } else if (code == 127) { // Backspace if (this._currentLine.length > 0) { this._currentLine = this._currentLine.slice(0, -1) @@ -48,7 +53,7 @@ export class Shell { } else if (possibles.length > 1) { const line = possibles.join(" ") // Note keep leading whitespace on current line. - await this.output(`\r\n${line}\r\n${this._prompt}${this._currentLine}`) + await this.output(`\r\n${line}\r\n${this._environment.getPrompt()}${this._currentLine}`) } } else if (code == 27) { // Escape following by 1+ more characters const remainder = char.slice(1) @@ -68,6 +73,7 @@ export class Shell { const { FS, PATH, ERRNO_CODES, PROXYFS } = this._fsModule; FS.mkdir(this._mountpoint, 0o777) FS.chdir(this._mountpoint) + this._environment.set("PWD", FS.cwd()) this._fileSystem = { FS, PATH, ERRNO_CODES, PROXYFS } return this._fileSystem } @@ -88,7 +94,7 @@ export class Shell { } async start(): Promise { - await this.output(this._prompt) + await this.output(this._environment.getPrompt()) } // Keeping this public for tests. @@ -108,7 +114,9 @@ export class Shell { } const args = command.suffix.map((token) => token.value) - const context = new Context(args, this._fileSystem!, this._mountpoint, stdout) + const context = new Context( + args, this._fileSystem!, this._mountpoint, this._environment, stdout, + ) await commands.run(cmdName, context) await context.flush() @@ -127,7 +135,7 @@ export class Shell { private readonly _outputCallback: IOutputCallback private _currentLine: string - private _prompt: string // Should really obtain this from env + private _environment: Environment private _fsModule: any private _fileSystem?: IFileSystem diff --git a/tests/commands/cat.test.ts b/tests/commands/cat.test.ts index 00e7d02..8135db8 100644 --- a/tests/commands/cat.test.ts +++ b/tests/commands/cat.test.ts @@ -1,6 +1,6 @@ import { shell_setup_simple } from "../shell_setup" -describe("echo command", () => { +describe("cat command", () => { it("should write to stdout", async () => { const { shell, output } = await shell_setup_simple() await shell._runCommands("cat file1") diff --git a/tests/commands/cd.test.ts b/tests/commands/cd.test.ts new file mode 100644 index 0000000..3899e07 --- /dev/null +++ b/tests/commands/cd.test.ts @@ -0,0 +1,11 @@ +import { shell_setup_simple } from "../shell_setup" + +describe("cd command", () => { + it("should update PWD", async () => { + const { shell } = await shell_setup_simple() + const { environment } = shell + expect(environment.get("PWD")).toEqual("/drive") + await shell._runCommands("cd dirA") + expect(environment.get("PWD")).toEqual("/drive/dirA") + }) +}) diff --git a/tests/commands/env.test.ts b/tests/commands/env.test.ts new file mode 100644 index 0000000..bdae236 --- /dev/null +++ b/tests/commands/env.test.ts @@ -0,0 +1,13 @@ +import { shell_setup_simple } from "../shell_setup" + +describe("env command", () => { + it("should write to stdout", async () => { + const { shell, output } = await shell_setup_simple() + const { environment } = shell + expect(environment.get("MYENV")).toBeNull() + + await shell._runCommands("env MYENV=23") + expect(environment.get("MYENV")).toBeNull() + expect(output.text.trim().split("\r\n").at(-1)).toEqual("MYENV=23") + }) +})