Skip to content

Commit

Permalink
Support output to file, including append
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed Jul 8, 2024
1 parent e6b9b80 commit 542b5f8
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 57 deletions.
29 changes: 13 additions & 16 deletions src/io/file_output.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
import { BufferedOutput } from "./buffered_output"
//import { IFileSystem } from "../file_system"
import { IFileSystem } from "../file_system"

export class FileOutput extends BufferedOutput {
//constructor(fs: IFileSystem, path: string, append: boolean) {
constructor(path: string, append: boolean) {
constructor(readonly fileSystem: IFileSystem, readonly path: string, readonly append: boolean) {
super()
//this.fs = fs
this.path = path
this.append = append
}

console.log(this.path)
override async flush(): Promise<void> {
const { FS } = this.fileSystem
let content = this.data.join("")

if (this.append) {
throw Error("FileOutput in append mode not implemented")
try {
const prevContent = FS.readFile(this.path, { "encoding": "utf8" })
content = prevContent + content
} catch (e) {
// If file does not exist, fallback to write (non-append) behaviour.
}
}
}

override async flush(): Promise<void> {
//const all_data = this.data.join()
//this.fs.write(this.path, all_data)
FS.writeFile(this.path, content)
this.clear()
}

//private readonly fs: IFileSystem
private readonly path: string
private readonly append: boolean // or replace
}
4 changes: 2 additions & 2 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ function _createCommandNode(tokens: Token[]) {
let args = tokens.slice(1)

// Handle redirects.
const index = args.findIndex((token) => token.value == ">")
const index = args.findIndex((token) => token.value.startsWith(">"))
if (index >= 0) {
// Must support multiple redirects for a single command.
if (args.length != index + 2) {
// Need better error handling here.
throw Error("Redirect should be followed by file to redirect to")
throw new Error("Redirect should be followed by file to redirect to")
}
const redirect = new RedirectNode(args[index], args[index+1])
args = args.slice(0, index)
Expand Down
31 changes: 24 additions & 7 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CommandRegistry } from "./command_registry"
import { Context } from "./context"
import { IFileSystem } from "./file_system"
import { TerminalOutput } from "./io"
import { FileOutput, Output, TerminalOutput } from "./io"
import { IOutputCallback } from "./output_callback"
import { CommandNode, parse } from "./parse"
import * as FsModule from './wasm/fs'
Expand Down Expand Up @@ -93,23 +93,40 @@ export class Shell {

// Keeping this public for tests.
async _runCommands(cmdText: string): Promise<void> {
const cmdNodes = parse(cmdText)
const ncmds = cmdNodes.length
const stdout = new TerminalOutput(this._outputCallback)
try {
const cmdNodes = parse(cmdText)
const ncmds = cmdNodes.length

for (let i = 0; i < ncmds; ++i) {
const command = cmdNodes[i] as CommandNode
const cmdName = command.name.value

const commands = CommandRegistry.instance().get(cmdName)
if (commands === null) {
const runner = CommandRegistry.instance().get(cmdName)
if (runner === null) {
// Give location of command in input?
throw new Error(`Unknown command: '${cmdName}'`)
}

let output: Output = stdout
if (command.redirects) {
// Support single redirect only, write or append to file.
if (command.redirects.length > 1) {
throw new Error("Only implemented a single redirect per command")
}
const redirect = command.redirects[0]
const redirectChars = redirect.token.value
if (!(redirectChars == ">" || redirectChars == ">>")) {
throw new Error("Only implemented redirect write to file, not " + redirectChars)
}

const path = redirect.target.value
output = new FileOutput(this._fileSystem!, path, redirectChars == ">>")
}

const args = command.suffix.map((token) => token.value)
const context = new Context(args, this._fileSystem!, this._mountpoint, stdout)
await commands.run(cmdName, context)
const context = new Context(args, this._fileSystem!, this._mountpoint, output)
await runner.run(cmdName, context)

await context.flush()
}
Expand Down
36 changes: 27 additions & 9 deletions src/tokenize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ export function tokenize(source: string): Token[] {
let offset: number = -1 // Offset of start of current token, -1 if not in token.
const n = source.length

let prevChar: string = ""
let prevCharType: CharType = CharType.None
for (let i = 0; i < n; i++) {
const char = source[i]
const charType = _getCharType(char)
if (offset >= 0) { // In token.
if (whitespace.includes(char)) {
if (charType == CharType.Whitespace) {
// Finish current token.
tokens.push({offset, value: source.slice(offset, i)})
offset = -1
} else if (delimiters.includes(char)) {
// Finish current token and create new one for delimiter.
} else if (charType != prevCharType || (charType == CharType.Delimiter && char != prevChar)) {
// Finish current token and start new one.
tokens.push({offset, value: source.slice(offset, i)})
tokens.push({offset: i, value: source.slice(i, i+1)})
offset = -1
offset = i
}
} else { // Not in token.
if (delimiters.includes(char)) {
// Single character delimiter.
tokens.push({offset: i, value: source.slice(i, i+1)})
} else if (!whitespace.includes(char)) {
if (charType != CharType.Whitespace) {
// Start new token.
offset = i
}
}
prevChar = char
prevCharType = charType
}

if (offset >= 0) {
Expand All @@ -44,3 +45,20 @@ export function tokenize(source: string): Token[] {

return tokens
}

enum CharType {
None,
Delimiter,
Whitespace,
Other,
}

function _getCharType(char: string): CharType {
if (whitespace.includes(char)) {
return CharType.Whitespace
} else if (delimiters.includes(char)) {
return CharType.Delimiter
} else {
return CharType.Other
}
}
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
27 changes: 27 additions & 0 deletions tests/commands/grep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { shell_setup_simple } from "../shell_setup"

describe("grep command", () => {
it("should write to stdout", async () => {
const { shell, output } = await shell_setup_simple()
await shell._runCommands("grep cond file2")
expect(output.text).toEqual("Second line\r\n")
})

it("should support ^ and $", async () => {
const { shell, output, FS } = await shell_setup_simple()
const line0 = " hello"
const line1 = "hello "
FS.writeFile("file3", line0 + "\n" + line1)

await shell._runCommands("grep hello file3")
expect(output.text).toEqual(line0 + "\r\n" + line1 + "\r\n")
output.clear()

await shell._runCommands("grep ^hello file3")
expect(output.text).toEqual(line1 + "\r\n")
output.clear()

await shell._runCommands("grep hello$ file3")
expect(output.text).toEqual(line0 + "\r\n")
})
})
15 changes: 0 additions & 15 deletions tests/file_system_setup.ts

This file was deleted.

5 changes: 5 additions & 0 deletions tests/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ describe("parse", () => {
[new RedirectNode({offset: 5, value: ">"}, {offset: 6, value: "file"})])
])
})

it("should raise on redirect without target file", () => {
expect(() => parse("ls >")).toThrow("file to redirect to")
expect(() => parse("ls >>")).toThrow("file to redirect to")
})
})
11 changes: 11 additions & 0 deletions tests/shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ describe("Shell", () => {
await shell._runCommands(" ls")
expect(output.text).toEqual("dirA file1 file2\r\n")
})

it("should output to file", async () => {
const { shell, output, FS } = await shell_setup_simple()
await shell._runCommands("echo Hello > out")
expect(output.text).toEqual("")
expect(FS.readFile("out", { "encoding": "utf8" })).toEqual("Hello\n")

await shell._runCommands("echo Goodbye >> out")
expect(output.text).toEqual("")
expect(FS.readFile("out", { "encoding": "utf8" })).toEqual("Hello\nGoodbye\n")
})
})

describe("input", () => {
Expand Down
23 changes: 16 additions & 7 deletions tests/tokenize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,22 @@ describe("Tokenize", () => {
expect(tokenize("ls;")).toEqual([{offset: 0, value: "ls"}, {offset: 2, value: ";"}])
expect(tokenize(";ls")).toEqual([{offset: 0, value: ";"}, {offset: 1, value: "ls"}])
expect(tokenize(";ls;;")).toEqual([
{offset: 0, value: ";"}, {offset: 1, value: "ls"}, {offset: 3, value: ";"},
{offset: 4, value: ";"},
{offset: 0, value: ";"}, {offset: 1, value: "ls"}, {offset: 3, value: ";;"},
])
expect(tokenize("ls ; ; pwd")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: ";"}, {offset: 5, value: ";"},
{offset: 7, value: "pwd"},
])
expect(tokenize("ls ;; pwd")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: ";"}, {offset: 4, value: ";"},
{offset: 6, value: "pwd"},
{offset: 0, value: "ls"}, {offset: 3, value: ";;"}, {offset: 6, value: "pwd"},
])
expect(tokenize("ls;pwd")).toEqual([
{offset: 0, value: "ls"}, {offset: 2, value: ";"}, {offset: 3, value: "pwd"},
])
expect(tokenize("ls;;pwd")).toEqual([
{offset: 0, value: "ls"}, {offset: 2, value: ";"}, {offset: 3, value: ";"},
{offset: 4, value: "pwd"},
{offset: 0, value: "ls"}, {offset: 2, value: ";;"}, {offset: 4, value: "pwd"},
])
expect(tokenize(" ;; ")).toEqual([{offset: 1, value: ";"}, {offset: 2, value: ";"}])
expect(tokenize(" ;; ")).toEqual([{offset: 1, value: ";;"}])
expect(tokenize(" ; ; ")).toEqual([{offset: 1, value: ";"}, {offset: 3, value: ";"}])
})

Expand All @@ -72,5 +69,17 @@ describe("Tokenize", () => {
{offset: 0, value: "ls"}, {offset: 3, value: "-l"}, {offset: 5, value: ">"},
{offset: 6, value: "somefile"},
])
expect(tokenize("ls >> somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: ">>"}, {offset: 6, value: "somefile"},
])
expect(tokenize("ls>>somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 2, value: ">>"}, {offset: 4, value: "somefile"},
])
expect(tokenize("ls >>somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: ">>"}, {offset: 5, value: "somefile"},
])
expect(tokenize("ls>> somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 2, value: ">>"}, {offset: 5, value: "somefile"},
])
})
})

0 comments on commit 542b5f8

Please sign in to comment.