2019-10-31 19:08:09 +01:00
|
|
|
import * as Cookies from "js-cookie";
|
2019-11-02 13:10:49 +01:00
|
|
|
import "./Extensions"
|
2019-11-06 14:02:18 +01:00
|
|
|
import {Environment} from "./Environment";
|
2019-11-04 18:19:24 +01:00
|
|
|
import {Directory, File, FileSystem, Path} from "./FileSystem"
|
2019-11-06 14:47:14 +01:00
|
|
|
import {IllegalArgumentError, IllegalStateError} from "./Shared";
|
2019-11-06 14:02:18 +01:00
|
|
|
import {InputArgs} from "./Shell";
|
2019-11-02 15:16:50 +01:00
|
|
|
import {EscapeCharacters} from "./Terminal";
|
2019-11-06 12:53:48 +01:00
|
|
|
import {UserList} from "./UserList";
|
2019-10-20 23:55:04 +02:00
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A collection of commands executed within a particular user session.
|
|
|
|
*/
|
2019-10-20 23:55:04 +02:00
|
|
|
export class Commands {
|
2019-11-04 14:47:59 +01:00
|
|
|
/**
|
|
|
|
* The environment in which commands are executed.
|
|
|
|
*/
|
|
|
|
private readonly environment: Environment;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The user session describing the user that executes commands.
|
|
|
|
*/
|
2019-11-06 12:53:48 +01:00
|
|
|
private readonly userSession: UserList;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The file system to interact with.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
private readonly fileSystem: FileSystem;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The list of all available commands.
|
|
|
|
*/
|
2019-10-21 17:07:16 +02:00
|
|
|
private readonly commands: { [key: string]: Command };
|
2019-10-21 02:25:42 +02:00
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Constructs a new collection of commands executed within the given user session.
|
|
|
|
*
|
2019-11-04 14:47:59 +01:00
|
|
|
* @param environment the environment in which commands are executed
|
2019-10-29 12:36:03 +01:00
|
|
|
* @param userSession the user session describing the user that executes commands
|
|
|
|
* @param fileSystem the file system to interact with
|
|
|
|
*/
|
2019-11-06 12:53:48 +01:00
|
|
|
constructor(environment: Environment, userSession: UserList, fileSystem: FileSystem) {
|
2019-11-04 14:47:59 +01:00
|
|
|
this.environment = environment;
|
2019-10-29 12:36:03 +01:00
|
|
|
this.userSession = userSession;
|
2019-10-21 02:25:42 +02:00
|
|
|
this.fileSystem = fileSystem;
|
|
|
|
this.commands = {
|
2019-10-31 01:34:36 +01:00
|
|
|
"cat": new Command(
|
|
|
|
this.cat,
|
|
|
|
`concatenate and print files`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`cat FILE...`,
|
2019-10-31 01:34:36 +01:00
|
|
|
`Reads files sequentially, writing them to the standard output.`,
|
|
|
|
new InputValidator({minArgs: 1})
|
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"clear": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.clear,
|
|
|
|
`clear terminal output`,
|
|
|
|
`clear`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Clears all previous terminal output.`,
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({maxArgs: 0})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"cd": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.cd,
|
|
|
|
`change directory`,
|
|
|
|
`cd [DIRECTORY]`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Changes the current working directory to [DIRECTORY]. If [DIRECTORY] is empty, the current working \\\
|
|
|
|
directory is changed to the root.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({maxArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"cp": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.cp,
|
2019-11-06 00:58:16 +01:00
|
|
|
`copy files`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`cp [-r | -R | --recursive] SOURCE DESTINATION
|
|
|
|
cp [-r | -R | --recursive] SOURCES... DESTINATION`,
|
|
|
|
`In its first form, the file or directory at SOURCE is copied to DESTINATION. If DESTINATION is an \\\
|
|
|
|
existing directory, SOURCE is copied into that directory, retaining the file name from SOURCE. If \\\
|
|
|
|
DESTINATION does not exist, SOURCE is copied to the exact location of DESTINATION.
|
|
|
|
|
|
|
|
In its second form, all files and directories at SOURCES are copied to DESTINATION. DESTINATION must \\\
|
|
|
|
be a pre-existing directory, and all SOURCES are copied into DESTINATION retaining the file names \\\
|
|
|
|
from SOURCES.
|
|
|
|
|
|
|
|
In both forms, sources are not copied if they are directories unless the -R options is given.\\\
|
|
|
|
`.trimMultiLines(),
|
2019-11-06 00:58:16 +01:00
|
|
|
new InputValidator({minArgs: 2})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"echo": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.echo,
|
|
|
|
`display text`,
|
2019-11-03 18:21:25 +01:00
|
|
|
`echo [-n] [TEXT]`,
|
|
|
|
`Displays [TEXT].
|
2019-11-06 15:07:56 +01:00
|
|
|
|
|
|
|
Unless the -n parameter is given, a newline is appended to the end.`.trimMultiLines(),
|
2019-10-31 01:07:27 +01:00
|
|
|
new InputValidator()
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"exit": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.exit,
|
|
|
|
`close session`,
|
|
|
|
`exit`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Closes the terminal session.`,
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({maxArgs: 0})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"help": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.help,
|
|
|
|
`display documentation`,
|
2019-10-31 01:07:27 +01:00
|
|
|
`help [COMMAND...]`,
|
|
|
|
`Displays help documentation for each command in [COMMAND...].
|
2019-11-06 15:07:56 +01:00
|
|
|
|
|
|
|
If no commands are given, a list of all commands is shown.`.trimMultiLines(),
|
2019-10-31 01:07:27 +01:00
|
|
|
new InputValidator()
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"ls": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.ls,
|
|
|
|
`list directory contents`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`ls [-a | -A] [DIRECTORY...]`,
|
|
|
|
`Displays the files and directories in [DIRECTORY...]. If no directory is given, the files and \\\
|
|
|
|
directories in the current working directory are shown. If more than one directory is given, the \\\
|
|
|
|
files and directories are shown for each given directory in order.
|
|
|
|
|
|
|
|
Files starting with a . are only shown if the -a option is given, with the exception of . and .., \\\
|
|
|
|
which are always shown.`.trimMultiLines(),
|
2019-10-31 01:07:27 +01:00
|
|
|
new InputValidator()
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-30 22:19:07 +01:00
|
|
|
"man": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.man,
|
|
|
|
`display manual documentation pages`,
|
2019-10-31 01:07:27 +01:00
|
|
|
`man PAGE...`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Displays the manual pages with names PAGE....`,
|
2019-10-31 01:07:27 +01:00
|
|
|
new InputValidator()
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"mkdir": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.mkdir,
|
|
|
|
`make directories`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`mkdir [-p] DIRECTORY...`,
|
2019-10-21 02:25:42 +02:00
|
|
|
`Creates the directories given by DIRECTORY.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
If more than one directory is given, the directories are created in the order they are given in. If \\\
|
|
|
|
the -p option is given, parent directories that do not exist are created as well.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({minArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"mv": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.mv,
|
2019-11-06 00:58:16 +01:00
|
|
|
`move files`,
|
|
|
|
`mv SOURCE DESTINATION
|
|
|
|
mv SOURCES... DESTINATION`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`In its first form, the file or directory at SOURCE is moved to DESTINATION. If DESTINATION is an \\\
|
|
|
|
existing directory, SOURCE is moved into that directory, retaining the file name from SOURCE. If \\\
|
|
|
|
DESTINATION does not exist, SOURCE is moved to the exact location of DESTINATION.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
In its second form, all files and directories at SOURCES are moved to DESTINATION. DESTINATION must \\\
|
|
|
|
be a pre-existing directory, and all SOURCES are moved into DESTINATION retaining the file names \\\
|
|
|
|
from SOURCES.`.trimMultiLines(),
|
2019-11-06 00:58:16 +01:00
|
|
|
new InputValidator({minArgs: 2})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"open": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.open,
|
|
|
|
`open web page`,
|
|
|
|
`open [-b | --blank] FILE`,
|
|
|
|
`Opens the web page linked to by FILE in this browser window.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
If -b or --blank is set, the web page is opened in a new tab.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({minArgs: 1, maxArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"poweroff": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.poweroff,
|
|
|
|
`close down the system`,
|
|
|
|
`poweroff`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Automated shutdown procedure to nicely notify users when the system is shutting down.`,
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({maxArgs: 0})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"pwd": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.pwd,
|
|
|
|
`print working directory`,
|
|
|
|
`pwd`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Displays the current working directory.`,
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({maxArgs: 0})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"rm": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.rm,
|
|
|
|
`remove file`,
|
|
|
|
`rm [-f | --force] [-r | -R | --recursive] [--no-preserve-root] FILE...`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Removes the files given by FILE. If more than one file is given, the files are removed in the order \\\
|
|
|
|
they are given in.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-10-21 02:25:42 +02:00
|
|
|
If -f or --force is set, no warning is given if a file could not be removed.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-10-21 02:25:42 +02:00
|
|
|
If -r, -R, or --recursive is set, files and directories are removed recursively.
|
2019-11-06 14:47:14 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
Unless --no-preserve-root is set, the root directory cannot be removed.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({minArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"rmdir": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.rmdir,
|
|
|
|
`remove directories`,
|
|
|
|
`rmdir DIRECTORY...`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Removes the directories given by DIRECTORY. If more than one directory is given, the directories \\\
|
|
|
|
are removed in the order they are given in.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({minArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
2019-11-04 14:47:59 +01:00
|
|
|
"set": new Command(
|
|
|
|
this.set,
|
|
|
|
`set environment variable`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`set KEY [VALUE]`,
|
|
|
|
`Sets the environment variable KEY to VALUE. If no value is given, the environment variable is \\\
|
|
|
|
cleared. Read-only variables cannot be set.`.trimMultiLines(),
|
2019-11-04 14:47:59 +01:00
|
|
|
new InputValidator({minArgs: 1, maxArgs: 2})
|
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"touch": new Command(
|
2019-10-21 02:25:42 +02:00
|
|
|
this.touch,
|
|
|
|
`change file timestamps`,
|
|
|
|
`touch FILE...`,
|
2019-11-06 15:07:56 +01:00
|
|
|
`Update the access and modification times of each FILE to the current time. If a file does not \\\
|
|
|
|
exist, it is created.`.trimMultiLines(),
|
2019-10-31 00:29:55 +01:00
|
|
|
new InputValidator({minArgs: 1})
|
2019-10-26 16:08:46 +02:00
|
|
|
),
|
2019-10-31 01:07:27 +01:00
|
|
|
"whoami": new Command(
|
2019-10-26 16:08:46 +02:00
|
|
|
this.whoami,
|
|
|
|
`print short description of user`,
|
|
|
|
`whoami`,
|
2019-10-31 00:29:55 +01:00
|
|
|
`Print a description of the user associated with the current effective user ID.`,
|
|
|
|
new InputValidator({maxArgs: 0})
|
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-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Parses and executes the given input string and returns the output generated by that command.
|
|
|
|
*
|
2019-11-02 21:28:32 +01:00
|
|
|
* @param input the input string to parse and execute
|
2019-10-29 12:36:03 +01:00
|
|
|
*/
|
2019-11-02 21:28:32 +01:00
|
|
|
execute(input: InputArgs): string {
|
|
|
|
if (input.command === "factory-reset") {
|
2019-10-30 22:19:07 +01:00
|
|
|
Cookies.remove("files");
|
2019-11-04 14:47:59 +01:00
|
|
|
Cookies.remove("env");
|
2019-10-30 22:19:07 +01:00
|
|
|
location.reload();
|
2019-10-31 22:46:42 +01:00
|
|
|
throw new Error("Goodbye");
|
2019-10-30 22:19:07 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
if (input.command === "")
|
2019-11-02 15:16:50 +01:00
|
|
|
return "";
|
2019-10-31 00:29:55 +01:00
|
|
|
if (!this.commands.hasOwnProperty(input.command))
|
2019-11-02 15:16:50 +01:00
|
|
|
return `Unknown command '${input.command}'`;
|
2019-10-31 00:29:55 +01:00
|
|
|
|
|
|
|
const command = this.commands[input.command];
|
|
|
|
const validation = command.validator.validate(input);
|
|
|
|
if (!validation[0])
|
|
|
|
return this.createUsageErrorOutput(input.command, validation[1]);
|
|
|
|
|
|
|
|
return command.fun.bind(this)(input);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an output action corresponding to an error message about invalid usage of a command.
|
|
|
|
*
|
|
|
|
* @param commandName the name of the command that was used incorrectly
|
|
|
|
* @param errorMessage the message describing how the command was used incorrectly; preferably ended with a `.`
|
|
|
|
*/
|
2019-11-02 15:16:50 +01:00
|
|
|
private createUsageErrorOutput(commandName: string, errorMessage: string | undefined): string {
|
2019-10-31 00:29:55 +01:00
|
|
|
const command = this.commands[commandName];
|
|
|
|
if (command === undefined)
|
2019-11-06 14:47:14 +01:00
|
|
|
throw new IllegalArgumentError(`Unknown command \`${commandName}\`.`);
|
2019-10-31 00:29:55 +01:00
|
|
|
|
2019-11-06 14:47:14 +01:00
|
|
|
return `Invalid usage of ${commandName}. ${errorMessage ?? ""}
|
|
|
|
|
|
|
|
<b>Usage</b>
|
|
|
|
${command.usage}`.trimLines();
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private cat(input: InputArgs): string {
|
|
|
|
return input.args
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
if (!this.fileSystem.has(path))
|
|
|
|
return `cat: ${it}: No such file`;
|
|
|
|
|
|
|
|
const node = this.fileSystem.get(path);
|
|
|
|
if (!(node instanceof File))
|
2019-11-02 15:16:50 +01:00
|
|
|
return `cat: ${it}: No such file`;
|
2019-10-31 01:34:36 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
return node.contents;
|
|
|
|
})
|
2019-11-04 19:57:13 +01:00
|
|
|
.filter(it => it !== "")
|
2019-11-02 15:16:50 +01:00
|
|
|
.join("\n");
|
2019-10-31 01:34:36 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private cd(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
if (input.args[0] === "") {
|
2019-11-06 14:02:18 +01:00
|
|
|
this.environment.set("cwd", "/");
|
2019-11-04 18:19:24 +01:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2019-11-06 14:02:18 +01:00
|
|
|
const path = Path.interpret(this.environment.get("cwd"), input.args[0]);
|
2019-11-04 18:19:24 +01:00
|
|
|
if (!this.fileSystem.has(path))
|
|
|
|
return `The directory '${path}' does not exist.`;
|
|
|
|
|
2019-11-06 14:02:18 +01:00
|
|
|
this.environment.set("cwd", path.toString());
|
2019-11-04 18:19:24 +01:00
|
|
|
return "";
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private cp(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
try {
|
2019-11-06 00:58:16 +01:00
|
|
|
return this.moveCopyMappings(input)
|
|
|
|
.map(([source, destination]) => {
|
|
|
|
try {
|
2019-11-06 15:07:56 +01:00
|
|
|
this.fileSystem.copy(source, destination, input.hasAnyOption(["r", "R", "recursive"]));
|
2019-11-06 00:58:16 +01:00
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter(it => it !== "")
|
|
|
|
.join("\n");
|
2019-11-04 18:19:24 +01:00
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
2018-11-29 13:34:46 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private clear(): string {
|
|
|
|
return EscapeCharacters.Escape + EscapeCharacters.Clear;
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private echo(input: InputArgs): string {
|
2019-11-03 18:21:25 +01:00
|
|
|
return input.args.join(" ").replace("hunter2", "*******")
|
2019-11-04 14:47:59 +01:00
|
|
|
+ (input.hasOption("n") ? "" : "\n");
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private exit(): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
this.environment.set("user", "");
|
2019-11-02 15:16:50 +01:00
|
|
|
return "";
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private help(input: InputArgs): string {
|
2019-10-21 02:25:42 +02:00
|
|
|
const commandNames = Object.keys(this.commands);
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-31 01:07:27 +01:00
|
|
|
if (input.args.length > 0) {
|
2019-11-02 15:16:50 +01:00
|
|
|
return input.args
|
|
|
|
.map(it => {
|
|
|
|
if (!this.commands.hasOwnProperty(it))
|
|
|
|
return `Unknown command ${it}.`;
|
|
|
|
|
|
|
|
const commandName = it.toLowerCase();
|
|
|
|
const command = this.commands[commandName];
|
|
|
|
|
2019-11-06 14:47:14 +01:00
|
|
|
return `<b>Name</b>
|
|
|
|
${commandName}
|
|
|
|
|
|
|
|
<b>Summary</b>
|
|
|
|
${command.summary}
|
2019-11-02 15:16:50 +01:00
|
|
|
|
2019-11-06 14:47:14 +01:00
|
|
|
<b>Usage</b>
|
|
|
|
${command.usage}
|
2019-11-02 15:16:50 +01:00
|
|
|
|
2019-11-06 14:47:14 +01:00
|
|
|
<b>Description</b>
|
|
|
|
${command.desc}`.trimLines();
|
2019-11-02 15:16:50 +01:00
|
|
|
})
|
|
|
|
.join("\n\n\n");
|
2018-11-28 22:07:51 +01:00
|
|
|
} else {
|
|
|
|
const commandWidth = Math.max.apply(null, commandNames.map(it => it.length)) + 4;
|
2019-10-31 00:48:40 +01:00
|
|
|
const commandPaddings = commandNames.map(it => commandWidth - it.length);
|
|
|
|
const commandLinks = commandNames
|
2019-10-31 23:22:37 +01:00
|
|
|
.map(it => `<a href="#" onclick="execute('help ${it}')">${it}</a>`)
|
2019-10-31 00:48:40 +01:00
|
|
|
.map((it, i) => `${it.padEnd(it.length + commandPaddings[i], ' ')}`);
|
|
|
|
const commandEntries = commandNames
|
|
|
|
.map((it, i) => `${commandLinks[i]}${this.commands[it].summary}`);
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
return `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
|
|
|
|
2019-11-06 14:47:14 +01:00
|
|
|
<b>List of commands</b>
|
|
|
|
${commandEntries.join("\n")}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-06 15:07:56 +01:00
|
|
|
Write "help [COMMAND]" or click a command in the list above for more information on a command.\\\
|
|
|
|
`.trimMultiLines();
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private ls(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
const lists = (input.args.length === 0 ? [""] : input.args)
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
const node = this.fileSystem.get(path);
|
|
|
|
if (node === undefined)
|
|
|
|
return [path, `The directory '${path}' does not exist.`];
|
|
|
|
if (!(node instanceof Directory))
|
|
|
|
return [path, `'${path}' is not a directory.`];
|
|
|
|
|
|
|
|
const dirList = [
|
2019-11-04 19:57:13 +01:00
|
|
|
new Directory().nameString("./", path),
|
|
|
|
new Directory().nameString("../", path.parent)
|
2019-11-04 18:19:24 +01:00
|
|
|
];
|
|
|
|
const fileList: string[] = [];
|
|
|
|
|
|
|
|
const nodes = node.nodes;
|
|
|
|
Object.keys(nodes)
|
|
|
|
.sortAlphabetically((x) => x, false)
|
|
|
|
.forEach(name => {
|
|
|
|
const node = nodes[name];
|
|
|
|
if (!input.hasAnyOption(["a", "A"]) && name.startsWith("."))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (node instanceof Directory)
|
|
|
|
dirList.push(node.nameString(`${name}/`, path.getChild(name)));
|
|
|
|
else if (node instanceof File)
|
|
|
|
fileList.push(node.nameString(name, path.getChild(name)));
|
|
|
|
else
|
|
|
|
throw new IllegalStateError(`'${path.getChild(name)}' is neither a file nor a directory.`);
|
|
|
|
});
|
|
|
|
|
|
|
|
return [path, dirList.concat(fileList).join("\n")];
|
|
|
|
});
|
|
|
|
|
|
|
|
if (lists.length === 1)
|
|
|
|
return lists.map(([_, list]) => list).join("");
|
|
|
|
else
|
|
|
|
return lists.map(([path, list]) => `<b>${path}</b>\n${list}`).join("\n\n");
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private man(input: InputArgs): string {
|
2019-10-31 00:29:55 +01:00
|
|
|
if (input.args.length === 0)
|
2019-11-02 15:16:50 +01:00
|
|
|
return "What manual page do you want?";
|
2019-10-31 00:29:55 +01:00
|
|
|
else if (Object.keys(this.commands).indexOf(input.args[0]) < 0)
|
2019-11-02 15:16:50 +01:00
|
|
|
return `No manual entry for ${input.args[0]}`;
|
2019-06-10 15:31:46 +02:00
|
|
|
else
|
2019-10-31 00:29:55 +01:00
|
|
|
return this.help(input);
|
2018-12-02 17:01:11 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private mkdir(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
return input.args
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
try {
|
|
|
|
this.fileSystem.add(path, new Directory(), input.hasOption("p"));
|
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
2019-11-04 19:57:13 +01:00
|
|
|
.filter(it => it !== "")
|
2019-11-04 18:19:24 +01:00
|
|
|
.join("\n");
|
2018-11-28 23:22:17 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private mv(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
try {
|
2019-11-06 00:58:16 +01:00
|
|
|
return this.moveCopyMappings(input)
|
|
|
|
.map(([source, destination]) => {
|
|
|
|
try {
|
|
|
|
this.fileSystem.move(source, destination);
|
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter(it => it !== "")
|
|
|
|
.join("\n");
|
2019-11-04 18:19:24 +01:00
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
2018-11-29 13:18:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private open(input: InputArgs): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
const path = Path.interpret(this.environment.get("cwd"), input.args[0]);
|
2019-10-31 00:29:55 +01:00
|
|
|
const target = input.hasAnyOption(["b", "blank"]) ? "_blank" : "_self";
|
2018-11-29 00:15:08 +01:00
|
|
|
|
2019-11-04 18:19:24 +01:00
|
|
|
const node = this.fileSystem.get(path);
|
2019-10-21 02:25:42 +02:00
|
|
|
if (node === undefined)
|
2019-11-04 18:19:24 +01:00
|
|
|
return `The file '${path}' does not exist`;
|
2019-10-21 02:25:42 +02:00
|
|
|
if (!(node instanceof File))
|
2019-11-04 18:19:24 +01:00
|
|
|
return `'${path}' is not a file`;
|
2018-11-29 00:15:08 +01:00
|
|
|
|
2019-10-31 01:27:31 +01:00
|
|
|
window.open(node.contents, target);
|
2019-11-02 15:16:50 +01:00
|
|
|
return "";
|
2018-11-29 00:15:08 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private poweroff(): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
const userName = this.environment.get("user");
|
2019-11-06 12:53:48 +01:00
|
|
|
if (userName === "")
|
2019-10-31 22:46:42 +01:00
|
|
|
throw new IllegalStateError("Cannot execute `poweroff` while not logged in.");
|
2019-10-30 22:19:07 +01:00
|
|
|
|
2019-10-30 22:13:28 +01:00
|
|
|
Cookies.set("poweroff", "true", {
|
2019-10-31 08:51:09 +01:00
|
|
|
"expires": new Date(new Date().setSeconds(new Date().getSeconds() + 30)),
|
2019-10-30 22:13:28 +01:00
|
|
|
"path": "/"
|
|
|
|
});
|
2018-11-28 23:22:17 +01:00
|
|
|
|
|
|
|
setTimeout(() => location.reload(), 2000);
|
2019-11-06 14:47:14 +01:00
|
|
|
return `Shutdown NOW!
|
|
|
|
|
|
|
|
*** FINAL System shutdown message from ${userName}@fwdekker.com ***
|
|
|
|
|
|
|
|
System going down IMMEDIATELY
|
|
|
|
|
|
|
|
|
|
|
|
System shutdown time has arrived`.trimLines();
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private pwd(): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
return this.environment.get("cwd");
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private rm(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
return input.args
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
try {
|
|
|
|
this.fileSystem.remove(
|
|
|
|
path,
|
|
|
|
input.hasAnyOption(["f", "force"]),
|
|
|
|
input.hasAnyOption(["r", "R", "recursive"]),
|
|
|
|
input.hasOption("no-preserve-root")
|
2019-11-04 19:57:13 +01:00
|
|
|
);
|
|
|
|
return "";
|
2019-11-04 18:19:24 +01:00
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
2019-11-04 19:57:13 +01:00
|
|
|
.filter(it => it !== "")
|
2019-11-04 18:19:24 +01:00
|
|
|
.join("\n");
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private rmdir(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
return input.args
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
try {
|
|
|
|
this.fileSystem.remove(path, false, false, false);
|
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
2019-11-04 19:57:13 +01:00
|
|
|
.filter(it => it !== "")
|
2019-11-04 18:19:24 +01:00
|
|
|
.join("\n");
|
2018-11-29 20:56:27 +01:00
|
|
|
}
|
|
|
|
|
2019-11-04 14:47:59 +01:00
|
|
|
private set(input: InputArgs): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
try {
|
|
|
|
if (input.args.length === 1)
|
|
|
|
this.environment.safeDelete(input.args[0]);
|
|
|
|
else
|
|
|
|
this.environment.safeSet(input.args[0], input.args[1]);
|
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
2019-11-04 14:47:59 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private touch(input: InputArgs): string {
|
2019-11-04 18:19:24 +01:00
|
|
|
return input.args
|
2019-11-06 14:02:18 +01:00
|
|
|
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
2019-11-04 18:19:24 +01:00
|
|
|
.map(path => {
|
|
|
|
try {
|
|
|
|
this.fileSystem.add(path, new File(), false);
|
|
|
|
return "";
|
|
|
|
} catch (error) {
|
|
|
|
return error.message;
|
|
|
|
}
|
|
|
|
})
|
2019-11-04 19:57:13 +01:00
|
|
|
.filter(it => it !== "")
|
2019-11-04 18:19:24 +01:00
|
|
|
.join("\n");
|
2019-10-21 02:25:42 +02:00
|
|
|
}
|
2019-10-26 16:08:46 +02:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
private whoami(): string {
|
2019-11-06 14:02:18 +01:00
|
|
|
const user = this.userSession.get(this.environment.get("user"));
|
2019-10-26 16:08:46 +02:00
|
|
|
if (user === undefined)
|
2019-10-31 22:46:42 +01:00
|
|
|
throw new IllegalStateError("Cannot execute `whoami` while not logged in.");
|
2019-10-26 16:08:46 +02:00
|
|
|
|
2019-11-02 15:16:50 +01:00
|
|
|
return user.description;
|
2019-10-26 16:08:46 +02:00
|
|
|
}
|
2019-11-06 00:58:16 +01:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps sources to inputs for the `move` and `copy` commands.
|
|
|
|
*
|
|
|
|
* @param input the input to extract mappings from
|
|
|
|
*/
|
|
|
|
private moveCopyMappings(input: InputArgs): [Path, Path][] {
|
2019-11-06 14:02:18 +01:00
|
|
|
const sources = input.args.slice(0, -1).map(arg => Path.interpret(this.environment.get("cwd"), arg));
|
|
|
|
const destination = Path.interpret(this.environment.get("cwd"), input.args.slice(-1)[0]);
|
2019-11-06 00:58:16 +01:00
|
|
|
|
|
|
|
let mappings: [Path, Path][];
|
|
|
|
if (this.fileSystem.has(destination)) {
|
|
|
|
// Move into directory
|
|
|
|
if (!(this.fileSystem.get(destination) instanceof Directory)) {
|
|
|
|
if (sources.length === 1)
|
2019-11-06 14:47:14 +01:00
|
|
|
throw new IllegalArgumentError(`'${destination}' already exists.`);
|
2019-11-06 00:58:16 +01:00
|
|
|
else
|
2019-11-06 14:47:14 +01:00
|
|
|
throw new IllegalArgumentError(`'${destination}' is not a directory.`);
|
2019-11-06 00:58:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
mappings = sources.map(source => [source, destination.getChild(source.fileName)]);
|
|
|
|
} else {
|
|
|
|
// Move to exact location
|
|
|
|
if (sources.length !== 1)
|
2019-11-06 14:47:14 +01:00
|
|
|
throw new IllegalArgumentError(`'${destination}' is not a directory.`);
|
2019-11-06 00:58:16 +01:00
|
|
|
|
|
|
|
if (!(this.fileSystem.get(destination.parent) instanceof Directory))
|
2019-11-06 14:47:14 +01:00
|
|
|
throw new IllegalArgumentError(`'${destination.parent}' is not a directory.`);
|
2019-11-06 00:58:16 +01:00
|
|
|
|
|
|
|
mappings = sources.map(path => [path, destination]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return mappings;
|
|
|
|
}
|
2019-10-21 02:25:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A command that can be executed.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
class Command {
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The function to execute with the command is executed.
|
|
|
|
*/
|
2019-11-02 15:16:50 +01:00
|
|
|
readonly fun: (args: InputArgs) => string;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A short summary of what the command does.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
readonly summary: string;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A string describing how the command is to be used.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
readonly usage: string;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A longer description of what the command does and how its parameters work.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
readonly desc: string;
|
2019-10-31 00:29:55 +01:00
|
|
|
/**
|
|
|
|
* A function that validates input for this command.
|
|
|
|
*/
|
|
|
|
readonly validator: InputValidator;
|
2019-10-21 02:25:42 +02:00
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Constructs a new command.
|
|
|
|
*
|
|
|
|
* @param fun the function to execute with the command is executed
|
|
|
|
* @param summary a short summary of what the command does
|
|
|
|
* @param usage a string describing how the command is to be used
|
|
|
|
* @param desc a longer description of what the command does and how its parameters work
|
2019-10-31 00:29:55 +01:00
|
|
|
* @param validator a function that validates input for this command
|
2019-10-29 12:36:03 +01:00
|
|
|
*/
|
2019-11-02 15:16:50 +01:00
|
|
|
constructor(fun: (args: InputArgs) => string, summary: string, usage: string, desc: string,
|
2019-10-31 00:29:55 +01:00
|
|
|
validator: InputValidator) {
|
2019-10-21 02:25:42 +02:00
|
|
|
this.fun = fun;
|
|
|
|
this.summary = summary;
|
|
|
|
this.usage = usage;
|
|
|
|
this.desc = desc;
|
2019-10-31 00:29:55 +01:00
|
|
|
this.validator = validator;
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-29 08:58:53 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
/**
|
|
|
|
* Validates the input of a command.
|
|
|
|
*/
|
|
|
|
class InputValidator {
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
2019-10-31 00:29:55 +01:00
|
|
|
* The minimum number of arguments allowed.
|
|
|
|
*/
|
|
|
|
readonly minArgs: number;
|
|
|
|
/**
|
|
|
|
* The maximum number of arguments allowed.
|
|
|
|
*/
|
|
|
|
readonly maxArgs: number;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a new input validator.
|
2019-10-29 12:36:03 +01:00
|
|
|
*
|
2019-10-31 00:29:55 +01:00
|
|
|
* @param minArgs the minimum number of arguments allowed
|
|
|
|
* @param maxArgs the maximum number of arguments allowed
|
2019-10-29 12:36:03 +01:00
|
|
|
*/
|
2019-10-31 00:29:55 +01:00
|
|
|
constructor({minArgs = 0, maxArgs = Number.MAX_SAFE_INTEGER}: { minArgs?: number, maxArgs?: number } = {}) {
|
|
|
|
if (minArgs > maxArgs)
|
2019-10-31 22:46:42 +01:00
|
|
|
throw new IllegalStateError("`minArgs` must be less than or equal to `maxArgs`.");
|
2019-10-31 00:29:55 +01:00
|
|
|
|
|
|
|
this.minArgs = minArgs;
|
|
|
|
this.maxArgs = maxArgs;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns `[true]` if the input is valid, or `[false, string]` where the string is a reason if the input is not
|
|
|
|
* valid.
|
|
|
|
*
|
|
|
|
* @param input the input to validate
|
|
|
|
*/
|
|
|
|
validate(input: InputArgs): [true] | [false, string] {
|
|
|
|
if (this.minArgs === this.maxArgs && input.args.length !== this.minArgs)
|
2019-10-31 23:22:37 +01:00
|
|
|
return [false, `Expected ${this.args(this.minArgs)} but got ${input.args.length}.`];
|
2019-10-31 00:29:55 +01:00
|
|
|
if (input.args.length < this.minArgs)
|
2019-10-31 23:22:37 +01:00
|
|
|
return [false, `Expected at least ${this.args(this.minArgs)} but got ${input.args.length}.`];
|
2019-10-31 00:29:55 +01:00
|
|
|
if (input.args.length > this.maxArgs)
|
2019-10-31 23:22:37 +01:00
|
|
|
return [false, `Expected at most ${this.args(this.maxArgs)} but got ${input.args.length}.`];
|
2019-10-31 00:29:55 +01:00
|
|
|
|
|
|
|
return [true];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns `"1 argument"` if the given amount is `1` and returns `"$n arguments"` otherwise.
|
|
|
|
*
|
|
|
|
* @param amount the amount to check
|
|
|
|
*/
|
2019-10-31 23:22:37 +01:00
|
|
|
private args(amount: number): string {
|
2019-10-31 00:29:55 +01:00
|
|
|
return amount === 1 ? `1 argument` : `${amount} arguments`;
|
2019-10-29 12:36:03 +01:00
|
|
|
}
|
2018-11-29 08:58:53 +01:00
|
|
|
}
|