Skip to content

Commit

Permalink
Parse pipes and redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed May 22, 2024
1 parent 52325ae commit 82f67fb
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 7 deletions.
57 changes: 53 additions & 4 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@ const endOfCommand = ";&"
export abstract class Node {}

export class CommandNode extends Node {
constructor(readonly name: Token, readonly suffix: Token[]) {
constructor(readonly name: Token, readonly suffix: Token[], readonly redirects?: RedirectNode[]) {
super()
}
}

export class PipeNode extends Node {
// Must be at least 2 commands
constructor(readonly commands: CommandNode[]) {
super()
}
}

export class RedirectNode extends Node {
constructor(readonly token: Token, readonly target: Token) {
super()
}
}
Expand All @@ -17,29 +30,65 @@ export function parse(source: string): Node[] {
const tokens = tokenize(source)

const ret: Node[] = []
const stack: CommandNode[] = []
let offset: number = -1 // Offset of start of current command, -1 if not in command.
const n = tokens.length

function clearStack() {
if (stack.length == 1) {
ret.push(stack[0])
} else if (stack.length > 1) {
ret.push(new PipeNode([...stack]))
}
stack.length = 0
}

for (let i = 0; i < n; i++) {
const token = tokens[i]
if (offset >= 0) { // In command
if (endOfCommand.includes(token.value)) {
// Finish current command, ignore endOfCommand token.
ret.push(new CommandNode(tokens[offset], tokens.slice(offset+1, i)))
stack.push(_createCommandNode(tokens.slice(offset, i)))
clearStack()
offset = -1
} else if (token.value == "|") {
// Finish current command which is in a pipe.
stack.push(_createCommandNode(tokens.slice(offset, i)))
offset = -1
}
} else { // Not in command
if (!endOfCommand.includes(token.value)) {
// Start new token.
// Start new command.
offset = i
}
}
}

if (offset >= 0) {
// Finish last command.
ret.push(new CommandNode(tokens[offset], tokens.slice(offset+1, n)))
stack.push(_createCommandNode(tokens.slice(offset, n)))
}

clearStack()
return ret
}


function _createCommandNode(tokens: Token[]) {
let args = tokens.slice(1)

// Handle redirects.
const index = args.findIndex((token) => token.value == ">")
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")
}
const redirect = new RedirectNode(args[index], args[index+1])
args = args.slice(0, index)
return new CommandNode(tokens[0], args, [redirect])
}

return new CommandNode(tokens[0], args)
}
2 changes: 1 addition & 1 deletion src/tokenize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const delimiters = ";&"
const delimiters = ";&|>"
const whitespace = " "

export type Token = {
Expand Down
41 changes: 40 additions & 1 deletion tests/parse.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommandNode, parse } from "../src/parse"
import { CommandNode, PipeNode, RedirectNode, parse } from "../src/parse"

describe("parse", () => {
it("should support no commands", () => {
Expand Down Expand Up @@ -30,4 +30,43 @@ describe("parse", () => {
new CommandNode({offset: 13, value: "ls"}, [{offset: 16, value: "-al"}]),
])
})

it("should support pipe", () => {
expect(parse("ls | sort")).toEqual([
new PipeNode([
new CommandNode({offset: 0, value: "ls"}, []),
new CommandNode({offset: 5, value: "sort"}, []),
]),
])
expect(parse("ls | sort|uniq")).toEqual([
new PipeNode([
new CommandNode({offset: 0, value: "ls"}, []),
new CommandNode({offset: 5, value: "sort"}, []),
new CommandNode({offset: 10, value: "uniq"}, []),
]),
])

expect(parse("ls | sort; cat")).toEqual([
new PipeNode([
new CommandNode({offset: 0, value: "ls"}, []),
new CommandNode({offset: 5, value: "sort"}, []),
]),
new CommandNode({offset: 11, value: "cat"}, []),
])
})

it("should support redirect", () => {
expect(parse("ls -l > file")).toEqual([
new CommandNode(
{offset: 0, value: "ls"},
[{offset: 3, value: "-l"}],
[new RedirectNode({offset: 6, value: ">"}, {offset: 8, value: "file"})])
])
expect(parse("ls -l>file")).toEqual([
new CommandNode(
{offset: 0, value: "ls"},
[{offset: 3, value: "-l"}],
[new RedirectNode({offset: 5, value: ">"}, {offset: 6, value: "file"})])
])
})
})
23 changes: 22 additions & 1 deletion tests/tokenize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ describe("Tokenize", () => {
expect(tokenize(" ls ")).toEqual([{offset: 1, value: "ls"}])
})


it("should support multiple tokens", () => {
expect(tokenize("ls -al; pwd")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: "-al"}, {offset: 6, value: ";"},
Expand Down Expand Up @@ -52,4 +51,26 @@ describe("Tokenize", () => {
expect(tokenize(" ;; ")).toEqual([{offset: 1, value: ";"}, {offset: 2, value: ";"}])
expect(tokenize(" ; ; ")).toEqual([{offset: 1, value: ";"}, {offset: 3, value: ";"}])
})

it("should support pipe", () => {
expect(tokenize("ls -l | sort")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: "-l"}, {offset: 6, value: "|"},
{offset: 8, value: "sort"},
])
expect(tokenize("ls -l|sort")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: "-l"}, {offset: 5, value: "|"},
{offset: 6, value: "sort"},
])
})

it("should support redirection of stdout", () => {
expect(tokenize("ls -l > somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: "-l"}, {offset: 6, value: ">"},
{offset: 8, value: "somefile"},
])
expect(tokenize("ls -l>somefile")).toEqual([
{offset: 0, value: "ls"}, {offset: 3, value: "-l"}, {offset: 5, value: ">"},
{offset: 6, value: "somefile"},
])
})
})

0 comments on commit 82f67fb

Please sign in to comment.