feat: improve bash-like command parsing
This commit is contained in:
21
src/shell/wush/ShellEnvironment.ts
Normal file
21
src/shell/wush/ShellEnvironment.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Wush environment manager
|
||||
* Manages environment variables, aliases etc.
|
||||
*/
|
||||
export class ShellEnvironment {
|
||||
private variables: Record<string, string> = {}
|
||||
|
||||
SetVariable(name: string, value: string | null): void {
|
||||
// remove the variable if value is null
|
||||
if (value === null) {
|
||||
delete this.variables[name]
|
||||
return
|
||||
}
|
||||
|
||||
this.variables[name] = value
|
||||
}
|
||||
|
||||
GetVariable(name: string): string | undefined {
|
||||
return this.variables[name]
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import { Cat } from '../../program/Cat'
|
||||
import { Echo } from '../../program/Echo'
|
||||
import { Mkdir } from '../../program/Mkdir'
|
||||
import { Cd } from '../../program/Cd'
|
||||
import { Printf } from '../../program/Printf'
|
||||
import { Pwd } from '../../program/Pwd'
|
||||
// import { Pwd } from '../program/Pwd'
|
||||
// import { Cd } from '../program/Cd'
|
||||
|
||||
@@ -83,8 +85,9 @@ export class Wush extends Shell {
|
||||
this.programs['cat'] = new Cat()
|
||||
this.programs['echo'] = new Echo()
|
||||
this.programs['mkdir'] = new Mkdir()
|
||||
// this.programs['pwd'] = new Pwd()
|
||||
this.programs['pwd'] = new Pwd()
|
||||
this.programs['cd'] = new Cd(this)
|
||||
this.programs['printf'] = new Printf()
|
||||
|
||||
this.stdout.on(data => this.WriteEscapedString(data))
|
||||
this.Prompt()
|
||||
@@ -102,28 +105,32 @@ export class Wush extends Shell {
|
||||
delete this.programs[name]
|
||||
}
|
||||
|
||||
ExecuteProgram(name: string, args: string[]) {
|
||||
this.programs[name].Exec(this.stdin, this.stdout, this.workingDirectory, args)
|
||||
.then(code => {
|
||||
this.execExitCode = code != -1 ? code : -2
|
||||
})
|
||||
.catch((e) => {
|
||||
this.WriteEscapedString(`wush: command ${name} exited with the following exception\n`)
|
||||
this.WriteEscapedString(` | ${String(e)}\n`)
|
||||
})
|
||||
.finally(() => {
|
||||
// check if the exec actually exited with an exit code
|
||||
// and if not, set it to -2 to indicate that it didn't exit with a valid code
|
||||
if (this.execExitCode === -1)
|
||||
this.execExitCode = -2
|
||||
async ExecuteProgram(name: string, args: string[]): Promise<void> {
|
||||
return new Promise<void>(resolve => {
|
||||
this.programs[name].Exec(this.stdin, this.stdout, this.workingDirectory, args)
|
||||
.then(code => {
|
||||
this.execExitCode = code != -1 ? code : -2
|
||||
resolve()
|
||||
})
|
||||
.catch((e) => {
|
||||
this.WriteEscapedString(`wush: command ${name} exited with the following exception\n`)
|
||||
this.WriteEscapedString(` | ${String(e)}\n`)
|
||||
resolve()
|
||||
})
|
||||
.finally(() => {
|
||||
// check if the exec actually exited with an exit code
|
||||
// and if not, set it to -2 to indicate that it didn't exit with a valid code
|
||||
if (this.execExitCode === -1)
|
||||
this.execExitCode = -2
|
||||
|
||||
// this.terminal.Write(`The program exited with exit code ${this.execExitCode}.`)
|
||||
this.Prompt()
|
||||
})
|
||||
// this.terminal.Write(`The program exited with exit code ${this.execExitCode}.`)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Prompt() {
|
||||
this.terminal.Write(`boykisser in ${this.workingDirectory.GetPath()} -> `)
|
||||
this.terminal.Write(`user in ${this.workingDirectory.GetPath()} -> `)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,79 +182,267 @@ export class Wush extends Shell {
|
||||
this.bufferPos = 0
|
||||
}
|
||||
|
||||
ExecuteBuffer() {
|
||||
// parse the buffer
|
||||
const state: {
|
||||
inString: boolean
|
||||
inHardString: boolean
|
||||
inAndOperator: boolean
|
||||
} = {
|
||||
inString: false,
|
||||
inHardString: false,
|
||||
inAndOperator: false,
|
||||
}
|
||||
|
||||
const commands: {
|
||||
async ExecuteBuffer() {
|
||||
type OperatorToken = '&&' | '||' | '|' | ';'
|
||||
type Token = { type: 'word'; value: string } | { type: 'operator'; value: OperatorToken }
|
||||
type ParsedCommand = {
|
||||
args: string[]
|
||||
stdin: null | SimpleStream<string>
|
||||
stdout: null | SimpleStream<string>
|
||||
}[] = []
|
||||
}
|
||||
type ParsedPipeline = { commands: ParsedCommand[] }
|
||||
type ParsedListItem = { pipeline: ParsedPipeline; operator: '&&' | '||' | ';' | null }
|
||||
|
||||
const textBuf =
|
||||
this.buffer
|
||||
.join('')
|
||||
.split('')
|
||||
const tokenize = (text: string): { tokens: Token[]; error: string | null } => {
|
||||
const tokens: Token[] = []
|
||||
let current = ''
|
||||
let wordStarted = false
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let escapeNext = false
|
||||
|
||||
textBuf.forEach((char, i) => {
|
||||
// spacing
|
||||
if (char === ' ' && !(state.inString || state.inHardString )) {
|
||||
if (state.args[state.args.length - 1] !== '')
|
||||
state.args.push("")
|
||||
// string ("") handling
|
||||
} else if (char === '"' && !state.inHardString) {
|
||||
state.inString = !state.inString
|
||||
// hard string ('') handling
|
||||
} else if (char === "'" && !state.inString) {
|
||||
state.inHardString = !state.inHardString
|
||||
// && operator
|
||||
} else if (char === '&' || char === '|' && !(state.inString || state.inHardString )) {
|
||||
if (state.inAndOperator) {
|
||||
if (state.args[state.args.length - 1] === '')
|
||||
state.args[state.args.length - 1] = `${char}${char}`
|
||||
else state.args.push(`${char}${char}`)
|
||||
const pushWord = () => {
|
||||
if (!wordStarted)
|
||||
return
|
||||
|
||||
state.args.push("")
|
||||
state.inAndOperator = false
|
||||
} else if (this.buffer[i + 1] !== char) {
|
||||
state.args[state.args.length - 1] += char
|
||||
} else state.inAndOperator = true
|
||||
// adding the char to the current argument
|
||||
} else {
|
||||
state.args[state.args.length - 1] += char
|
||||
}
|
||||
})
|
||||
|
||||
this.FlushBuffer()
|
||||
|
||||
console.log(`Executing ${state.args[0]} with args '${state.args}'`)
|
||||
|
||||
this.execExitCode = -1
|
||||
|
||||
if (this.programs[state.args[0]] instanceof Program) {
|
||||
this.ExecuteProgram(state.args[0], state.args)
|
||||
} else {
|
||||
this.execExitCode = 0
|
||||
|
||||
// don't print anything if there are no arguments
|
||||
if (state.args[0].length !== 0) {
|
||||
this.terminal.Write(`wush: unknown command: ${state.args[0]}.`)
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
|
||||
this.execExitCode = 127
|
||||
tokens.push({ type: 'word', value: current })
|
||||
current = ''
|
||||
wordStarted = false
|
||||
}
|
||||
|
||||
this.Prompt()
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i]
|
||||
|
||||
if (escapeNext) {
|
||||
current += char
|
||||
escapeNext = false
|
||||
wordStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (inSingleQuote) {
|
||||
if (char === "'") {
|
||||
inSingleQuote = false
|
||||
wordStarted = true
|
||||
} else {
|
||||
current += char
|
||||
wordStarted = true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (inDoubleQuote) {
|
||||
if (char === '"') {
|
||||
inDoubleQuote = false
|
||||
wordStarted = true
|
||||
} else if (char === '\\') {
|
||||
if (i + 1 >= text.length)
|
||||
return { tokens: [], error: 'unterminated escape sequence' }
|
||||
|
||||
current += text[i + 1]
|
||||
i++
|
||||
wordStarted = true
|
||||
} else {
|
||||
current += char
|
||||
wordStarted = true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true
|
||||
wordStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === "'") {
|
||||
inSingleQuote = true
|
||||
wordStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inDoubleQuote = true
|
||||
wordStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === ' ' || char === '\t') {
|
||||
pushWord()
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '#') {
|
||||
if (!wordStarted)
|
||||
break
|
||||
|
||||
current += char
|
||||
wordStarted = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === ';') {
|
||||
pushWord()
|
||||
tokens.push({ type: 'operator', value: ';' })
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '|') {
|
||||
pushWord()
|
||||
if (text[i + 1] === '|') {
|
||||
tokens.push({ type: 'operator', value: '||' })
|
||||
i++
|
||||
} else {
|
||||
tokens.push({ type: 'operator', value: '|' })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '&') {
|
||||
if (text[i + 1] === '&') {
|
||||
pushWord()
|
||||
tokens.push({ type: 'operator', value: '&&' })
|
||||
i++
|
||||
} else {
|
||||
current += char
|
||||
wordStarted = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
current += char
|
||||
wordStarted = true
|
||||
}
|
||||
|
||||
if (escapeNext)
|
||||
return { tokens: [], error: 'unterminated escape sequence' }
|
||||
if (inSingleQuote)
|
||||
return { tokens: [], error: 'unterminated single quote' }
|
||||
if (inDoubleQuote)
|
||||
return { tokens: [], error: 'unterminated double quote' }
|
||||
|
||||
pushWord()
|
||||
return { tokens, error: null }
|
||||
}
|
||||
|
||||
const parseTokens = (tokens: Token[]): { list: ParsedListItem[]; error: string | null } => {
|
||||
const list: ParsedListItem[] = []
|
||||
let currentArgs: string[] = []
|
||||
let currentPipeline: ParsedCommand[] = []
|
||||
let expectCommand = false
|
||||
|
||||
const pushCommand = () => {
|
||||
if (currentArgs.length === 0)
|
||||
return false
|
||||
|
||||
currentPipeline.push({
|
||||
args: currentArgs,
|
||||
stdin: null,
|
||||
stdout: null,
|
||||
})
|
||||
currentArgs = []
|
||||
return true
|
||||
}
|
||||
|
||||
const pushPipeline = (operator: '&&' | '||' | ';' | null) => {
|
||||
if (currentPipeline.length === 0)
|
||||
return false
|
||||
|
||||
list.push({ pipeline: { commands: currentPipeline }, operator })
|
||||
currentPipeline = []
|
||||
return true
|
||||
}
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'word') {
|
||||
currentArgs.push(token.value)
|
||||
expectCommand = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (token.value === '|') {
|
||||
if (!pushCommand())
|
||||
return { list: [], error: 'unexpected "|" operator' }
|
||||
|
||||
expectCommand = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (!pushCommand() || !pushPipeline(token.value))
|
||||
return { list: [], error: `unexpected "${token.value}" operator` }
|
||||
|
||||
expectCommand = true
|
||||
}
|
||||
|
||||
if (expectCommand)
|
||||
return { list: [], error: 'unexpected end of input' }
|
||||
|
||||
if (currentArgs.length > 0)
|
||||
pushCommand()
|
||||
|
||||
if (currentPipeline.length > 0)
|
||||
pushPipeline(null)
|
||||
|
||||
return { list, error: null }
|
||||
}
|
||||
|
||||
const line = this.buffer.join('')
|
||||
this.FlushBuffer()
|
||||
|
||||
const { tokens, error } = tokenize(line)
|
||||
if (error) {
|
||||
this.terminal.Write(`wush: error: ${error}`)
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
|
||||
this.Prompt()
|
||||
return
|
||||
}
|
||||
|
||||
const { list, error: parseError } = parseTokens(tokens)
|
||||
if (parseError) {
|
||||
this.terminal.Write(`wush: error: ${parseError}`)
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
|
||||
this.Prompt()
|
||||
return
|
||||
}
|
||||
|
||||
if (list.length === 0) {
|
||||
this.Prompt()
|
||||
return
|
||||
}
|
||||
|
||||
if (list.some(item => item.pipeline.commands.length > 1)) {
|
||||
this.terminal.Write(`wush: error: pipes are not supported yet`)
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
|
||||
this.Prompt()
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
const command = item.pipeline.commands[0]
|
||||
|
||||
this.execExitCode = -1
|
||||
|
||||
if (this.programs[command.args[0]] instanceof Program) {
|
||||
await this.ExecuteProgram(command.args[0], command.args)
|
||||
} else {
|
||||
this.execExitCode = 0
|
||||
|
||||
// don't print anything if there are no arguments
|
||||
if (command.args[0].length !== 0) {
|
||||
this.terminal.Write(`wush: error: unknown command: ${command.args[0]}.`)
|
||||
this.terminal.MoveCursor(0, 1, { x: true, y: false })
|
||||
|
||||
this.execExitCode = 127
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.Prompt()
|
||||
}
|
||||
|
||||
ProcessSimpleControlCode(code: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user