import {Commands} from "./Commands"; import {Environment} from "./Environment"; import {Directory, File, FileSystem, Path} from "./FileSystem"; import {InputParser} from "./InputParser"; import {Persistence} from "./Persistence"; import {asciiHeaderHtml, IllegalStateError, stripHtmlTags} from "./Shared"; import {EscapeCharacters, InputHistory} from "./Terminal"; import {UserList} from "./UserList"; /** * A shell that interacts with the user session and file system to execute commands. */ export class Shell { /** * The environment in which commands are executed. */ private readonly environment: Environment; /** * The history of the user's inputs. */ private readonly inputHistory: InputHistory; /** * The user session describing the user that interacts with the shell. */ private readonly userList: UserList; /** * The file system. */ private readonly fileSystem: FileSystem; /** * The set of commands that can be executed. */ private readonly commands: Commands; /** * The name of the user that the user is currently trying to log in as, or `undefined` if the user is not currently * trying to log in. */ private attemptUser: string | undefined; /** * Constructs a new shell. * * @param inputHistory the history of inputs */ constructor(inputHistory: InputHistory) { this.inputHistory = inputHistory; this.userList = new UserList(); this.fileSystem = Persistence.getFileSystem(); this.environment = Persistence.getEnvironment(this.fileSystem, this.userList); this.commands = new Commands(this.environment, this.userList, this.fileSystem); this.saveState(); } /** * Returns the header that is displayed when a user logs in. */ generateHeader(): string { if (this.environment.get("user") === "") return ""; return `${asciiHeaderHtml} Student MSc Computer Science @ TU Delft, the Netherlands ${(new Date()).toISOString()} Type "help" for help. Welcome to josh v%%VERSION_NUMBER%%, the javascript online shell. `.trimLines(); } /** * Returns the prefix based on the current state of the terminal. */ generatePrefix(): string { const userName = this.environment.get("user"); if (userName === "") { return this.attemptUser === undefined ? "login as: " : `Password for ${this.attemptUser}@fwdekker.com: `; } const cwd = new Path(this.environment.get("cwd")); const home = new Path(this.environment.get("home")); let anchorPath: Path; let anchorSymbol: string; if (home.isAncestorOf(cwd) || home.toString() === cwd.toString()) { anchorPath = home; anchorSymbol = "~"; } else { anchorPath = new Path("/"); anchorSymbol = "/"; } const parts = cwd.getAncestorsUntil(anchorPath).reverse().concat(cwd).slice(1); const rootText = (new Directory().nameString(anchorSymbol, anchorPath)) + (parts.length !== 0 && !anchorSymbol.endsWith("/") ? "/" : ""); const partText = parts .map(part => new Directory().nameString(part.fileName, part)) .join("/"); return `${userName}@fwdekker.com ${rootText}${partText}> `; } /** * Processes a user's input. * * @param inputString the input to process */ execute(inputString: string): string { if (this.environment.get("user") === "") { if (this.attemptUser === undefined) { this.attemptUser = inputString.trim() ?? undefined; // Set to undefined if empty string this.saveState(); return EscapeCharacters.Escape + EscapeCharacters.HideInput; } else { const attemptUser = this.userList.get(this.attemptUser); let resultString: string; if (attemptUser !== undefined && attemptUser.password === inputString) { this.environment.set("user", attemptUser.name); this.environment.set("home", attemptUser.home); this.environment.set("cwd", attemptUser.home); resultString = this.generateHeader(); } else { resultString = "Access denied\n"; } this.attemptUser = undefined; this.saveState(); return EscapeCharacters.Escape + EscapeCharacters.ShowInput + resultString; } } this.inputHistory.addEntry(inputString.trim()); const parser = InputParser.create(this.environment, this.fileSystem); let input; try { input = parser.parse(stripHtmlTags(inputString)); } catch (error) { return error.message; } if (input.redirectTarget[0] === "write") { try { const path = Path.interpret(this.environment.get("cwd"), input.redirectTarget[1]); if (this.fileSystem.get(path) instanceof Directory) return `Error while redirecting: '${path}' is a directory.`; this.fileSystem.remove(path); } catch (error) { return error.message; } } let output = this.commands.execute(input); if (input.redirectTarget[0] !== "default") { const path = Path.interpret(this.environment.get("cwd"), input.redirectTarget[1]); output = this.writeToFile(path, output, input.redirectTarget[0] === "append"); } if (this.environment.get("user") === "") { this.inputHistory.clear(); this.environment.clear(); this.environment.set("user", ""); } this.saveState(); return output; } /** * Writes or appends `data` to `file`. * * @param path the path of the file to write or append to * @param data the data to write or append * @param append `true` if and only if the data should be appended */ private writeToFile(path: Path, data: string, append: boolean): string { try { if (!this.fileSystem.has(path)) this.fileSystem.add(path, new File(), true); } catch (error) { return error.message; } const target = this.fileSystem.get(path); if (!(target instanceof File)) throw new IllegalStateError("File unexpectedly disappeared since last check."); if (append) target.contents += data; else target.contents = data; return ""; } /** * Persists the shell's state. * * @see Persistence */ private saveState() { Persistence.setFileSystem(this.fileSystem); Persistence.setEnvironment(this.environment); } } export module InputArgs { /** * The options given to a command. */ export type Options = { [key: string]: string | null }; /** * The intended target of the output of a command. * *