Skip to content

Commit

Permalink
Support environment variables that persist beyond single commands
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed Jul 8, 2024
1 parent e6b9b80 commit 6cfa446
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 17 deletions.
8 changes: 5 additions & 3 deletions src/commands/builtin_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
20 changes: 14 additions & 6 deletions src/commands/wasm_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
Expand Down
8 changes: 7 additions & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Environment } from "./environment"
import { IFileSystem } from "./file_system"
import { Output } from "./io"

Expand All @@ -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<void> {
await this.stdout.flush()
}

environment: Environment
}
48 changes: 48 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = new Map()

// Keys to ignore when copying back from a command's env vars.
private _ignore: Set<string> = new Set(["USER", "LOGNAME", "HOME", "LANG", "_"])
}
20 changes: 14 additions & 6 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<void> {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -88,7 +94,7 @@ export class Shell {
}

async start(): Promise<void> {
await this.output(this._prompt)
await this.output(this._environment.getPrompt())
}

// Keeping this public for tests.
Expand All @@ -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()
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/cat.test.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
11 changes: 11 additions & 0 deletions tests/commands/cd.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
13 changes: 13 additions & 0 deletions tests/commands/env.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})

0 comments on commit 6cfa446

Please sign in to comment.