Skip to content

Commit

Permalink
Merge pull request #21 from jupyterlite/aliases
Browse files Browse the repository at this point in the history
Add support for aliases
  • Loading branch information
ianthomas23 authored Jul 18, 2024
2 parents 179307a + d82b5dc commit 0b79075
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 73 deletions.
34 changes: 34 additions & 0 deletions src/aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Collection of aliases that are known to a shell.
*/
export class Aliases extends Map<string, string> {
constructor() {
super()
this.set("dir", "dir --color=auto")
this.set("grep", "grep --color=auto")
this.set("ls", "ls --color=auto")
this.set("ll", "ls -lF")
this.set("vdir", "vdir --color=auto")
}

getRecursive(key: string): string | undefined {
let alias = this.get(key)
while (alias !== undefined) {
const newKey = alias.split(" ")[0]
if (newKey == key) {
// Avoid infinite recursion.
break
}
const newAlias = this.get(newKey)
alias = (newAlias === undefined) ? newAlias : newAlias + alias!.slice(newKey.length)
key = newKey
}
return alias
}

match(start: string): string[] {
return [...this.keys()].filter((name) => {
return name.startsWith(start)
})
}
}
15 changes: 13 additions & 2 deletions src/commands/builtin_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { Context } from "../context"

export class BuiltinCommandRunner implements ICommandRunner {
names(): string[] {
return ["cd", "history"]
return ["alias", "cd", "history"]
}

async run(cmdName: string, context: Context): Promise<void> {
switch (cmdName) {
case "alias":
await this._alias(context)
break
case "cd":
this._cd(context)
break
Expand All @@ -17,6 +20,14 @@ export class BuiltinCommandRunner implements ICommandRunner {
}
}

private async _alias(context: Context) {
// TODO: support flags to clear, set, etc.
const { aliases, stdout } = context
for (const [key, value] of aliases.entries()) {
await stdout.write(`${key}='${value}'\n`)
}
}

private _cd(context: Context) {
const { args } = context
if (args.length < 1) {
Expand All @@ -29,7 +40,7 @@ export class BuiltinCommandRunner implements ICommandRunner {
let path = args[0]
if (path == "-") {
const oldPwd = context.environment.get("OLDPWD")
if (oldPwd === null) {
if (oldPwd === undefined) {
throw new Error("cd: OLDPWD not set")
}
path = oldPwd
Expand Down
8 changes: 4 additions & 4 deletions src/commands/coreutils_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { WasmCommandRunner } from "./wasm_command_runner"
export class CoreutilsCommandRunner extends WasmCommandRunner {
names(): string[] {
return [
"basename", "cat", "cp", "cut", "date", "dirname", "echo", "env", "expr", "head", "id",
"join", "ln", "logname", "ls", "md5sum", "mkdir", "mv", "nl", "pwd", "realpath", "rm",
"rmdir", "seq", "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum", "sleep",
"sort", "stat", "stty", "tail", "touch", "tr", "tty", "uname", "uniq", "wc",
"basename", "cat", "cp", "cut", "date", "dir", "dircolors", "dirname", "echo", "env", "expr",
"head", "id", "join", "ln", "logname", "ls", "md5sum", "mkdir", "mv", "nl", "pwd", "realpath",
"rm", "rmdir", "seq", "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum", "sleep",
"sort", "stat", "stty", "tail", "touch", "tr", "tty", "uname", "uniq", "vdir", "wc",
]
}

Expand Down
2 changes: 2 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Aliases } from "./aliases"
import { Environment } from "./environment"
import { History } from "./history"
import { IFileSystem } from "./file_system"
Expand All @@ -11,6 +12,7 @@ export class Context {
readonly args: string[],
readonly fileSystem: IFileSystem,
readonly mountpoint: string,
readonly aliases: Aliases,
readonly environment: Environment,
readonly history: History,
readonly stdin: IInput,
Expand Down
25 changes: 6 additions & 19 deletions src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
* Collection of environment variables that are known to a shell and are passed in and out of
* commands.
*/
export class Environment {
export class Environment extends Map<string, string> {
constructor() {
this._env.set("PS1", "\x1b[1;31mjs-shell:$\x1b[1;0m ") // red color
super()
this.set("PS1", "\x1b[1;31mjs-shell:$\x1b[1;0m ") // red color
}

/**
Expand All @@ -15,7 +16,7 @@ export class Environment {
const split = str.split("=")
const key = split.shift()
if (key && !this._ignore.has(key)) {
this._env.set(key, split.join("="))
this.set(key, split.join("="))
}
}
}
Expand All @@ -24,19 +25,11 @@ export class Environment {
* Copy environment variables into a command before it is run.
*/
copyIntoCommand(target: { [key: string]: string }) {
for (const [key, value] of this._env.entries()) {
for (const [key, value] of this.entries()) {
target[key] = value
}
}

delete(key: string) {
this._env.delete(key)
}

get(key: string): string | null {
return this._env.get(key) ?? null
}

getNumber(key: string): number | null {
const str = this.get(key)
if (str === null) {
Expand All @@ -47,15 +40,9 @@ export class Environment {
}

getPrompt(): string {
return this._env.get("PS1") ?? "$ "
return this.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", "_"])
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Aliases } from "./aliases"
export { Context } from "./context"
export { IFileSystem } from "./file_system"
export { IOutputCallback } from "./output_callback"
Expand Down
5 changes: 3 additions & 2 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Aliases } from "./aliases"
import { Token, tokenize } from "./tokenize"

const endOfCommand = ";&"
Expand Down Expand Up @@ -26,8 +27,8 @@ export class RedirectNode extends Node {
}


export function parse(source: string): Node[] {
const tokens = tokenize(source)
export function parse(source: string, aliases?: Aliases): Node[] {
const tokens = tokenize(source, aliases)

const ret: Node[] = []
const stack: CommandNode[] = []
Expand Down
18 changes: 15 additions & 3 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Aliases } from "./aliases"
import { CommandRegistry } from "./command_registry"
import { Context } from "./context"
import { Environment } from "./environment"
Expand All @@ -16,10 +17,15 @@ export class Shell {
this._outputCallback = outputCallback
this._mountpoint = mountpoint;
this._currentLine = ""
this._aliases = new Aliases()
this._environment = new Environment()
this._history = new History()
}

get aliases(): Aliases {
return this._aliases
}

get environment(): Environment {
return this._environment
}
Expand Down Expand Up @@ -138,7 +144,7 @@ export class Shell {
const stdin = new TerminalInput()
const stdout = new TerminalOutput(this._outputCallback)
try {
const nodes = parse(cmdText)
const nodes = parse(cmdText, this._aliases)

for (const node of nodes) {
if (node instanceof CommandNode) {
Expand Down Expand Up @@ -195,7 +201,8 @@ export class Shell {

const args = commandNode.suffix.map((token) => token.value)
const context = new Context(
args, this._fileSystem!, this._mountpoint, this._environment, this._history, input, output,
args, this._fileSystem!, this._mountpoint, this._aliases, this._environment, this._history,
input, output,
)
await runner.run(name, context)

Expand All @@ -204,11 +211,16 @@ export class Shell {

private async _tabComplete(text: string): Promise<[number, string[]]> {
// Assume tab completing command.
return [text.length, CommandRegistry.instance().match(text)]
const commandMatches = CommandRegistry.instance().match(text)
const aliasMatches = this._aliases.match(text)
// Combine, removing duplicates, and sort.
const matches = [...new Set([...commandMatches, ...aliasMatches])]
return [text.length, matches]
}

private readonly _outputCallback: IOutputCallback
private _currentLine: string
private _aliases: Aliases
private _environment: Environment
private _history: History

Expand Down
134 changes: 95 additions & 39 deletions src/tokenize.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Aliases } from "./aliases"

const delimiters = ";&|><"
const whitespace = " "

Expand All @@ -7,58 +9,112 @@ export type Token = {
value: string
}

export function tokenize(source: string): Token[] {
const tokens: Token[] = []
export function tokenize(source: string, aliases?: Aliases): Token[] {
const tokenizer = new Tokenizer(source, aliases)
tokenizer.run()
return tokenizer.tokens
}

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

let offset: number = -1 // Offset of start of current token, -1 if not in token.
const n = source.length
class State {
prevChar: string = ""
prevCharType: CharType = CharType.None
index: number = -1 // Index into source string.
offset: number = -1 // Offset of start of current token, -1 if not in token.
aliasOffset: number = -1
}

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.
class Tokenizer {
constructor(source: string, readonly aliases?: Aliases) {
this._source = source
this._tokens = []
this._state = new State()
}

run() {
while (this._state.index <= this._source.length) {
this._next()
}
}

get tokens(): Token[] {
return this._tokens
}

private _addToken(offset: number, value: string): boolean {
if (this.aliases !== undefined && offset != this._state.aliasOffset) {
const isCommand = (
this._tokens.length == 0 ||
";&|".includes(this._tokens.at(-1)!.value.at(-1)!)
)

if (isCommand) {
const alias = this.aliases.getRecursive(value)
if (alias !== undefined) {
// Replace token with its alias and set state to beginning of it to re-tokenize.
const n = value.length
this._state.offset = -1
this._state.index = offset - 1
this._state.aliasOffset = offset // Do not attempt to alias this token again.
this._source = this._source.slice(0, offset) + alias + this._source.slice(offset + n)
this._state.prevChar = ""
this._state.prevCharType = CharType.None
return false
}
}
}

this._tokens.push({ offset, value })
return true
}

private _getCharType(char: string): CharType {
if (whitespace.includes(char)) {
return CharType.Whitespace
} else if (delimiters.includes(char)) {
return CharType.Delimiter
} else {
return CharType.Other
}
}

private _next() {
const i = ++this._state.index

const char = i < this._source.length ? this._source[i] : " "
const charType = this._getCharType(char)
if (this._state.offset >= 0) { // In token.
if (charType == CharType.Whitespace) {
// Finish current token.
tokens.push({offset, value: source.slice(offset, i)})
offset = -1
} else if (charType != prevCharType || (charType == CharType.Delimiter && char != prevChar)) {
if (this._addToken(this._state.offset, this._source.slice(this._state.offset, i))) {
this._state.offset = -1
}
} else if (charType != this._state.prevCharType ||
(charType == CharType.Delimiter && char != this._state.prevChar)) {
// Finish current token and start new one.
tokens.push({offset, value: source.slice(offset, i)})
offset = i
if (this._addToken(this._state.offset, this._source.slice(this._state.offset, i))) {
this._state.offset = i
}
}
} else { // Not in token.
if (charType != CharType.Whitespace) {
// Start new token.
offset = i
this._state.offset = i
}
}
prevChar = char
prevCharType = charType
}

if (offset >= 0) {
// Finish last token.
tokens.push({offset, value: source.slice(offset, n)})
this._state.prevChar = char
this._state.prevCharType = charType
}

return tokens
private _source: string
private _tokens: Token[]
private _state: State
}

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
}
}
Loading

0 comments on commit 0b79075

Please sign in to comment.