forked from tools/josh
1
0
Fork 0
josh/src/js/commands.ts

399 lines
13 KiB
TypeScript
Raw Normal View History

import "./extensions.js"
2019-10-21 03:32:49 +02:00
import {File, FileSystem, UrlFile} from "./fs.js"
import {Terminal} from "./terminal.js";
export class Commands {
2019-10-21 02:25:42 +02:00
private readonly terminal: Terminal;
private readonly fileSystem: FileSystem;
2019-10-21 17:07:16 +02:00
private readonly commands: { [key: string]: Command };
2019-10-21 02:25:42 +02:00
constructor(terminal: Terminal, fileSystem: FileSystem) {
this.terminal = terminal;
this.fileSystem = fileSystem;
this.commands = {
clear: new Command(
this.clear,
`clear terminal output`,
`clear`,
`Clears all previous terminal output.`.trimLines()
),
cd: new Command(
this.cd,
`change directory`,
`cd [DIRECTORY]`,
`Changes the current working directory to [DIRECTORY].
If [DIRECTORY] is empty, nothing happens.`.trimLines()
),
cp: new Command(
this.cp,
`copy file`,
`cp SOURCE DESTINATION`,
`Copies SOURCE to DESTINATION.
SOURCE must be a file.
If DESTINATION exists and is a directory, SOURCE is copied into the directory.`.trimLines()
),
echo: new Command(
this.echo,
`display text`,
`echo [TEXT]`,
`Displays [TEXT].`.trimLines()
),
exit: new Command(
this.exit,
`close session`,
`exit`,
`Closes the terminal session.`.trimLines()
),
help: new Command(
this.help,
`display documentation`,
`help [COMMAND]`,
`Displays help documentation for [COMMAND].
If [COMMAND] is empty, a list of all commands is shown.`.trimLines()
),
ls: new Command(
this.ls,
`list directory contents`,
`ls [DIRECTORY]`,
`Displays the files and directories in [DIRECTORY].
If [DIRECTORY] is empty, the files and directories in the current working directory are shown.`.trimLines()
),
man: new Command(
this.man,
`display manual documentation pages`,
`man PAGE`,
`Displays the manual page with the name PAGE.`.trimLines()
),
mkdir: new Command(
this.mkdir,
`make directories`,
`mkdir DIRECTORY...`,
`Creates the directories given by DIRECTORY.
2019-10-21 02:25:42 +02:00
If more than one directory is given, the directories are created in the order they are given in`.trimLines()
),
mv: new Command(
this.mv,
`move file`,
`mv SOURCE DESTINATION`,
`Renames SOURCE to DESTINATION.`.trimLines()
),
open: new Command(
this.open,
`open web page`,
`open [-b | --blank] FILE`,
`Opens the web page linked to by FILE in this browser window.
2018-11-29 00:15:08 +01:00
2019-10-21 02:25:42 +02:00
If -b or --blank is set, the web page is opened in a new tab.`.trimLines()
),
poweroff: new Command(
this.poweroff,
`close down the system`,
`poweroff`,
`Automated shutdown procedure to nicely notify users when the system is shutting down.`.trimLines()
),
pwd: new Command(
this.pwd,
`print working directory`,
`pwd`,
`Displays the current working directory.`.trimLines()
),
rm: new Command(
this.rm,
`remove file`,
`rm [-f | --force] [-r | -R | --recursive] [--no-preserve-root] FILE...`,
`Removes the files given by FILE.
If more than one file is given, the files are removed in the order they are given in.
If -f or --force is set, no warning is given if a file could not be removed.
If -r, -R, or --recursive is set, files and directories are removed recursively.
Unless --no-preserve-root is set, the root directory cannot be removed.`.trimLines()
),
rmdir: new Command(
this.rmdir,
`remove directories`,
`rmdir DIRECTORY...`,
`Removes the directories given by DIRECTORY.
2018-11-29 09:20:23 +01:00
2019-10-21 02:25:42 +02:00
If more than one directory is given, the directories are removed in the order they are given in.`.trimLines()
),
touch: new Command(
this.touch,
`change file timestamps`,
`touch FILE...`,
`Update the access and modification times of each FILE to the current time.
2018-11-29 09:37:42 +01:00
2019-10-21 02:25:42 +02:00
If a file does not exist, it is created.`.trimLines()
2019-10-26 16:08:46 +02:00
),
whoami: new Command(
this.whoami,
`print short description of user`,
`whoami`,
`Print a description of the user associated with the current effective user ID.`
2019-10-21 02:25:42 +02:00
)
2018-11-28 22:07:51 +01:00
};
}
2018-11-28 19:51:48 +01:00
2019-10-21 17:07:16 +02:00
execute(input: string): string {
2018-11-29 08:58:53 +01:00
const args = new InputArgs(input);
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
if (args.command.trim() === "")
2018-11-28 22:41:59 +01:00
return "";
2019-10-21 02:25:42 +02:00
else if (this.commands.hasOwnProperty(args.command))
return this.commands[args.command].fun.bind(this)(args);
2019-06-10 15:31:46 +02:00
else
2019-10-21 02:25:42 +02:00
return `Unknown command '${args.command}'`;
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private cd(input: InputArgs): string {
return this.fileSystem.cd(input.getArg(0));
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private cp(input: InputArgs): string {
return this.fileSystem.cp(input.getArg(0), input.getArg(1));
2018-11-29 13:34:46 +01:00
}
2019-10-21 02:25:42 +02:00
private clear(): string {
this.terminal.clear();
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
private echo(input: InputArgs): string {
2019-10-21 02:25:42 +02:00
return input.args
2018-11-29 08:58:53 +01:00
.join(" ")
2018-11-28 22:41:59 +01:00
.replace("hunter2", "*******");
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private exit(): string {
this.terminal.logOut();
2018-11-28 22:41:59 +01:00
return "";
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private help(input: InputArgs): string {
const command = input.getArg(0, "").toLowerCase();
const commandNames = Object.keys(this.commands);
2018-11-28 19:51:48 +01:00
2018-11-28 22:07:51 +01:00
if (commandNames.indexOf(command) >= 0) {
2019-10-21 02:25:42 +02:00
const info = this.commands[command];
2018-11-28 19:51:48 +01:00
2018-11-28 22:07:51 +01:00
return "" +
`${command} - ${info.summary}
2018-11-28 19:51:48 +01:00
2018-11-29 01:08:58 +01:00
<b>Usage</b>
${info.usage}
2018-11-28 19:51:48 +01:00
2018-11-29 01:08:58 +01:00
<b>Description</b>
${info.desc}`.trimLines();
2018-11-28 22:07:51 +01:00
} else {
const commandWidth = Math.max.apply(null, commandNames.map(it => it.length)) + 4;
const commandEntries = commandNames.map(
2019-10-21 02:25:42 +02:00
it => `${it.padEnd(commandWidth, ' ')}${this.commands[it].summary}`
2018-11-28 22:07:51 +01:00
);
2018-11-28 19:51:48 +01:00
2018-11-28 22:07:51 +01:00
return "" +
2019-06-10 15:36:03 +02:00
`The source code of this website is <a href="https://git.fwdekker.com/FWDekker/fwdekker.com">available on git</a>.
2019-06-08 04:20:25 +02:00
<b>List of commands</b>
2018-11-29 01:08:58 +01:00
${commandEntries.join("\n")}
2018-11-28 19:51:48 +01:00
2018-11-29 01:08:58 +01:00
Write "help [COMMAND]" for more information on a command.`.trimLines();
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
}
2019-10-21 02:25:42 +02:00
private ls(input: InputArgs): string {
return this.fileSystem.ls(input.getArg(0));
2018-11-28 19:51:48 +01:00
}
2019-10-21 02:25:42 +02:00
private man(args: InputArgs): string {
if (args.args.length === 0)
2018-12-02 17:01:11 +01:00
return "What manual page do you want?";
2019-10-21 02:25:42 +02:00
else if (Object.keys(this.commands).indexOf(args.getArg(0)) < 0)
2018-12-02 17:01:11 +01:00
return `No manual entry for ${args.getArg(0)}`;
2019-06-10 15:31:46 +02:00
else
2018-12-02 17:01:11 +01:00
return this.help(args);
}
2019-10-21 02:25:42 +02:00
private mkdir(args: InputArgs): string {
return this.fileSystem.mkdirs(args.args);
2018-11-28 23:22:17 +01:00
}
2019-10-21 02:25:42 +02:00
private mv(args: InputArgs): string {
return this.fileSystem.mv(args.getArg(0), args.getArg(1));
2018-11-29 13:18:48 +01:00
}
2019-10-21 02:25:42 +02:00
private open(args: InputArgs): string {
2018-11-29 08:58:53 +01:00
const fileName = args.getArg(0);
const target = args.hasAnyOption(["b", "blank"]) ? "_blank" : "_self";
2018-11-29 00:15:08 +01:00
2019-10-21 02:25:42 +02:00
const node = this.fileSystem.getNode(fileName);
if (node === undefined)
2018-11-29 00:15:08 +01:00
return `The file '${fileName}' does not exist`;
2019-10-21 02:25:42 +02:00
if (!(node instanceof File))
2018-11-29 13:50:36 +01:00
return `'${fileName}' is not a file`;
2019-10-21 02:25:42 +02:00
if (!(node instanceof UrlFile))
2018-11-29 00:15:08 +01:00
return `Could not open '${fileName}'`;
// @ts-ignore: False positive
2019-10-21 02:25:42 +02:00
window.open(node.url, target);
2018-11-29 00:15:08 +01:00
return "";
}
2019-10-21 02:25:42 +02:00
private poweroff(): string {
2018-11-28 23:22:17 +01:00
const date = new Date();
date.setSeconds(date.getSeconds() + 30);
document.cookie = `poweroff=true; expires=${date.toUTCString()}; path=/`;
setTimeout(() => location.reload(), 2000);
return "" +
`Shutdown NOW!
*** FINAL System shutdown message from ${this.terminal.currentUser}@fwdekker.com ***
2018-11-28 23:22:17 +01:00
System going down IMMEDIATELY
System shutdown time has arrived`.trimLines();
2018-11-28 19:51:48 +01:00
}
2019-10-21 02:25:42 +02:00
private pwd(): string {
return this.fileSystem.pwd;
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private rm(args: InputArgs): string {
return this.fileSystem.rms(
args.args,
args.hasAnyOption(["f", "force"]),
args.hasAnyOption(["r", "R", "recursive"]),
args.hasOption("no-preserve-root")
2018-11-29 13:50:36 +01:00
);
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
2019-10-21 02:25:42 +02:00
private rmdir(args: InputArgs): string {
return this.fileSystem.rmdirs(args.args);
2018-11-29 20:56:27 +01:00
}
2019-10-21 02:25:42 +02:00
private touch(args: InputArgs): string {
return this.fileSystem.createFiles(args.args);
}
2019-10-26 16:08:46 +02:00
private whoami(): string {
const user = this.terminal.currentUser;
if (user === undefined)
throw "Cannot execute `whoami` while not logged in.";
return user.description;
}
2019-10-21 02:25:42 +02:00
}
class Command {
readonly fun: (args: InputArgs) => string;
readonly summary: string;
readonly usage: string;
readonly desc: string;
constructor(fun: (args: InputArgs) => string, summary: string, usage: string, desc: string) {
this.fun = fun;
this.summary = summary;
this.usage = usage;
this.desc = desc;
2018-11-28 19:51:48 +01:00
}
2018-11-28 22:07:51 +01:00
}
2018-11-29 08:58:53 +01:00
class InputArgs {
2019-10-21 02:25:42 +02:00
readonly command: string;
2019-10-21 17:07:16 +02:00
private readonly _options: { [key: string]: string };
2019-10-21 02:25:42 +02:00
private readonly _args: string[];
2019-10-21 02:25:42 +02:00
constructor(input: string) {
const inputParts = (input.match(/("[^"]+"|[^"\s]+)/g) || [])
2018-11-29 10:08:44 +01:00
.map(it => it.replace(/^"/, "").replace(/"$/, ""));
2019-10-21 02:25:42 +02:00
this.command = (inputParts[0] || "").toLowerCase();
this._options = {};
2018-11-29 08:58:53 +01:00
let i;
for (i = 1; i < inputParts.length; i++) {
const arg = inputParts[i];
const argParts = arg.split("=");
if (arg.startsWith("--")) {
// --option, --option=value
const argName = argParts[0].substr(2);
this._options[argName] = (argParts[1] || "");
} else if (arg.startsWith("-")) {
// -o, -o=value, -opq
if (argParts[0].length === 2) {
// -o, -o=value
const argName = argParts[0].substr(1);
this._options[argName] = (argParts[1] || "");
} else if (argParts.length === 1) {
// -opq
const argNames = argParts[0].substr(1).split("");
2019-10-21 02:25:42 +02:00
argNames.forEach(argName => this._options[argName] = "");
} else {
// Invalid
throw "Cannot assign value to multiple options!";
}
} else {
// Not an option
2018-11-29 08:58:53 +01:00
break;
}
this._options[argParts[0]] = (argParts[1] || "");
}
this._args = inputParts.slice(i);
2018-11-29 08:58:53 +01:00
}
2019-10-21 02:25:42 +02:00
get options(): object {
return Object.assign({}, this._options);
}
get args(): string[] {
2018-11-29 08:58:53 +01:00
return this._args.slice();
}
2019-10-21 02:25:42 +02:00
2019-10-21 17:07:16 +02:00
getArg(index: number, def: string | undefined = undefined): string {
2018-11-29 08:58:53 +01:00
return (def === undefined)
? this._args[index]
2019-10-21 02:25:42 +02:00
: (this.hasArg(index) ? this._args[index] : def);
2018-11-29 08:58:53 +01:00
}
2019-10-21 02:25:42 +02:00
hasArg(index: number): boolean {
return index >= 0 && index < this._args.length;
2018-11-29 08:58:53 +01:00
}
2019-10-21 17:07:16 +02:00
getOption(key: string, def: string | undefined = undefined) {
2018-11-29 08:58:53 +01:00
return (def === undefined)
? this._options[key]
2019-10-21 02:25:42 +02:00
: (this.hasOption(key) ? this._options[key] : def);
2018-11-29 08:58:53 +01:00
}
2019-10-21 02:25:42 +02:00
hasOption(key: string): boolean {
return this._options.hasOwnProperty(key);
2018-11-29 08:58:53 +01:00
}
2019-10-21 02:25:42 +02:00
hasAnyOption(keys: string[]): boolean {
2019-06-10 15:31:46 +02:00
for (let i = 0; i < keys.length; i++)
if (this.hasOption(keys[i]))
2018-11-29 08:58:53 +01:00
return true;
return false;
}
}