2019-10-20 23:55:04 +02:00
|
|
|
import "./extensions.js"
|
2019-10-31 01:27:31 +01:00
|
|
|
import {File, FileSystem, Path} from "./fs.js"
|
2019-10-29 12:36:03 +01:00
|
|
|
import {OutputAction} from "./terminal.js";
|
2019-10-31 01:34:36 +01:00
|
|
|
import {stripHtmlTags} from "./shared.js";
|
2019-10-29 12:36:03 +01:00
|
|
|
import {UserSession} from "./user-session.js";
|
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-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The user session describing the user that executes commands.
|
|
|
|
*/
|
|
|
|
private readonly userSession: UserSession;
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @param userSession the user session describing the user that executes commands
|
|
|
|
* @param fileSystem the file system to interact with
|
|
|
|
*/
|
|
|
|
constructor(userSession: UserSession, fileSystem: FileSystem) {
|
|
|
|
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`,
|
|
|
|
`cat FILE ...`,
|
|
|
|
`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-10-31 00:29:55 +01:00
|
|
|
`Clears all previous terminal output.`.trimLines(),
|
|
|
|
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]`,
|
|
|
|
`Changes the current working directory to [DIRECTORY].
|
2019-10-31 00:29:55 +01:00
|
|
|
If [DIRECTORY] is empty, nothing happens.`.trimLines(),
|
|
|
|
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,
|
|
|
|
`copy file`,
|
|
|
|
`cp SOURCE DESTINATION`,
|
|
|
|
`Copies SOURCE to DESTINATION.
|
|
|
|
SOURCE must be a file.
|
2019-10-31 00:29:55 +01:00
|
|
|
If DESTINATION exists and is a directory, SOURCE is copied into the directory.`.trimLines(),
|
|
|
|
new InputValidator({minArgs: 2, maxArgs: 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`,
|
|
|
|
`echo [TEXT]`,
|
2019-10-31 00:29:55 +01:00
|
|
|
`Displays [TEXT].`.trimLines(),
|
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-10-31 00:29:55 +01:00
|
|
|
`Closes the terminal session.`.trimLines(),
|
|
|
|
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...].
|
|
|
|
If no commands are given, a list of all commands is shown.`.trimLines(),
|
|
|
|
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-10-31 01:07:27 +01:00
|
|
|
`ls [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.`.trimLines(),
|
|
|
|
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...`,
|
|
|
|
`Displays the manual pages with names PAGE....`.trimLines(),
|
|
|
|
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`,
|
|
|
|
`mkdir DIRECTORY...`,
|
|
|
|
`Creates the directories given by DIRECTORY.
|
2018-11-29 09:08:24 +01:00
|
|
|
|
2019-10-31 00:48:40 +01:00
|
|
|
If more than one directory is given, the directories are created in the order they are given in.`.trimLines(),
|
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,
|
|
|
|
`move file`,
|
|
|
|
`mv SOURCE DESTINATION`,
|
2019-10-31 00:29:55 +01:00
|
|
|
`Renames SOURCE to DESTINATION.`.trimLines(),
|
|
|
|
new InputValidator({minArgs: 2, maxArgs: 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.
|
2018-11-29 00:15:08 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
If -b or --blank is set, the web page is opened in a new tab.`.trimLines(),
|
|
|
|
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-10-31 00:29:55 +01:00
|
|
|
`Automated shutdown procedure to nicely notify users when the system is shutting down.`.trimLines(),
|
|
|
|
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-10-31 00:29:55 +01:00
|
|
|
`Displays the current working directory.`.trimLines(),
|
|
|
|
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...`,
|
|
|
|
`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.
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
Unless --no-preserve-root is set, the root directory cannot be removed.`.trimLines(),
|
|
|
|
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...`,
|
|
|
|
`Removes the directories given by DIRECTORY.
|
2018-11-29 09:20:23 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
If more than one directory is given, the directories are removed in the order they are given in.`.trimLines(),
|
|
|
|
new InputValidator({minArgs: 1})
|
2019-10-21 02:25:42 +02:00
|
|
|
),
|
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...`,
|
|
|
|
`Update the access and modification times of each FILE to the current time.
|
2018-11-29 09:37:42 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
If a file does not exist, it is created.`.trimLines(),
|
|
|
|
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-10-31 00:29:55 +01:00
|
|
|
* @param inputString the input string to parse and execute
|
2019-10-29 12:36:03 +01:00
|
|
|
* @return the output generated by that command
|
|
|
|
*/
|
2019-10-31 00:29:55 +01:00
|
|
|
execute(inputString: string): OutputAction {
|
|
|
|
if (inputString === "factory-reset") {
|
2019-10-30 22:19:07 +01:00
|
|
|
// @ts-ignore
|
|
|
|
Cookies.remove("files");
|
2019-10-30 22:58:00 +01:00
|
|
|
// @ts-ignore
|
|
|
|
Cookies.remove("cwd");
|
2019-10-30 22:19:07 +01:00
|
|
|
location.reload();
|
|
|
|
throw "Goodbye";
|
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
const input = new InputArgs(stripHtmlTags(inputString));
|
|
|
|
if (input.command === "")
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["nothing"];
|
2019-10-31 00:29:55 +01:00
|
|
|
if (!this.commands.hasOwnProperty(input.command))
|
|
|
|
return ["append", `Unknown command '${input.command}'`];
|
|
|
|
|
|
|
|
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 `.`
|
|
|
|
* @return an output action corresponding to an error message about invalid usage of a command
|
|
|
|
*/
|
|
|
|
private createUsageErrorOutput(commandName: string, errorMessage: string | undefined): OutputAction {
|
|
|
|
const command = this.commands[commandName];
|
|
|
|
if (command === undefined)
|
|
|
|
throw `Unknown command \`${commandName}\`.`;
|
|
|
|
|
|
|
|
return ["append",
|
|
|
|
`Invalid usage of ${commandName}.${errorMessage === undefined ? "" : ` ${errorMessage}`}
|
|
|
|
|
|
|
|
<b>Usage</b>
|
|
|
|
${command.usage}`.trimLines()
|
|
|
|
];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
|
|
|
|
2019-10-31 01:34:36 +01:00
|
|
|
private cat(input: InputArgs): OutputAction {
|
|
|
|
return ["append",
|
|
|
|
input.args
|
|
|
|
.map(it => {
|
|
|
|
const node = this.fileSystem.getNode(it);
|
|
|
|
if (node === undefined || !(node instanceof File))
|
|
|
|
return `cat: ${it}: No such file`;
|
|
|
|
|
|
|
|
return node.contents;
|
|
|
|
})
|
|
|
|
.join("\n")
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private cd(input: InputArgs): OutputAction {
|
2019-10-31 00:29:55 +01:00
|
|
|
return ["append", this.fileSystem.cd(input.args[0])];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private cp(input: InputArgs): OutputAction {
|
2019-10-31 00:29:55 +01:00
|
|
|
return ["append", this.fileSystem.cp(input.args[0], input.args[1])];
|
2018-11-29 13:34:46 +01:00
|
|
|
}
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private clear(): OutputAction {
|
|
|
|
return ["clear"];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private echo(input: InputArgs): OutputAction {
|
2019-10-31 00:29:55 +01:00
|
|
|
return ["append", input.args.join(" ").replace("hunter2", "*******") + "\n"];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private exit(): OutputAction {
|
2019-10-29 12:36:03 +01:00
|
|
|
this.userSession.logOut();
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["nothing"];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private help(input: InputArgs): OutputAction {
|
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-10-27 01:15:56 +02:00
|
|
|
return ["append",
|
2019-10-31 01:07:27 +01:00
|
|
|
input.args
|
|
|
|
.map(it => {
|
|
|
|
if (!this.commands.hasOwnProperty(it))
|
|
|
|
return `Unknown command ${it}.`;
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-31 01:07:27 +01:00
|
|
|
const commandName = it.toLowerCase();
|
|
|
|
const command = this.commands[commandName];
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-31 01:07:27 +01:00
|
|
|
return "" +
|
|
|
|
`<b>Name</b>
|
|
|
|
${commandName}
|
|
|
|
|
|
|
|
<b>Summary</b>
|
|
|
|
${command.summary}
|
|
|
|
|
|
|
|
<b>Usage</b>
|
|
|
|
${command.usage}
|
|
|
|
|
|
|
|
<b>Description</b>
|
|
|
|
${command.desc}`.trimLines();
|
|
|
|
})
|
|
|
|
.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
|
|
|
|
.map(it => `<a href="#" onclick="run('help ${it}')">${it}</a>`)
|
|
|
|
.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-10-27 01:15:56 +02:00
|
|
|
return ["append",
|
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
|
|
|
|
2019-10-31 00:48:40 +01:00
|
|
|
Write "help [COMMAND]" or click a command in the list above 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-27 01:15:56 +02:00
|
|
|
private ls(input: InputArgs): OutputAction {
|
2019-10-31 01:07:27 +01:00
|
|
|
if (input.args.length <= 1)
|
|
|
|
return ["append", this.fileSystem.ls(input.args[0] || "")];
|
|
|
|
|
|
|
|
return ["append", input.args
|
|
|
|
.map(arg => "" +
|
|
|
|
`<b>${new Path(this.fileSystem.cwd, arg).path}</b>
|
|
|
|
${this.fileSystem.ls(arg)}`.trimLines())
|
|
|
|
.join("\n\n")];
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private man(input: InputArgs): OutputAction {
|
|
|
|
if (input.args.length === 0)
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["append", "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)
|
|
|
|
return ["append", `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-10-31 00:29:55 +01:00
|
|
|
private mkdir(input: InputArgs): OutputAction {
|
|
|
|
return ["append", this.fileSystem.mkdirs(input.args)];
|
2018-11-28 23:22:17 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private mv(input: InputArgs): OutputAction {
|
|
|
|
return ["append", this.fileSystem.mv(input.args[0], input.args[1])];
|
2018-11-29 13:18:48 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private open(input: InputArgs): OutputAction {
|
|
|
|
const fileName = input.args[0];
|
|
|
|
const target = input.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)
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["append", `The file '${fileName}' does not exist`];
|
2019-10-21 02:25:42 +02:00
|
|
|
if (!(node instanceof File))
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["append", `'${fileName}' is not a file`];
|
2018-11-29 00:15:08 +01:00
|
|
|
|
2019-10-21 03:13:14 +02:00
|
|
|
// @ts-ignore: False positive
|
2019-10-31 01:27:31 +01:00
|
|
|
window.open(node.contents, target);
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["nothing"];
|
2018-11-29 00:15:08 +01:00
|
|
|
}
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private poweroff(): OutputAction {
|
2019-10-30 22:19:07 +01:00
|
|
|
const user = this.userSession.currentUser;
|
|
|
|
if (user === undefined)
|
|
|
|
throw "Cannot execute `poweroff` while not logged in.";
|
|
|
|
|
2019-10-30 22:13:28 +01:00
|
|
|
// @ts-ignore
|
|
|
|
Cookies.set("poweroff", "true", {
|
|
|
|
"expires": new Date().setSeconds(new Date().getSeconds() + 30),
|
|
|
|
"path": "/"
|
|
|
|
});
|
2018-11-28 23:22:17 +01:00
|
|
|
|
|
|
|
setTimeout(() => location.reload(), 2000);
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["append",
|
2018-11-28 23:22:17 +01:00
|
|
|
`Shutdown NOW!
|
|
|
|
|
2019-10-30 22:19:07 +01:00
|
|
|
*** FINAL System shutdown message from ${user.name}@fwdekker.com ***
|
2018-11-28 23:22:17 +01:00
|
|
|
|
|
|
|
System going down IMMEDIATELY
|
|
|
|
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
System shutdown time has arrived`.trimLines()];
|
2018-11-28 19:51:48 +01:00
|
|
|
}
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private pwd(): OutputAction {
|
2019-10-30 22:58:00 +01:00
|
|
|
return ["append", this.fileSystem.cwd];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private rm(input: InputArgs): OutputAction {
|
2019-10-27 01:15:56 +02:00
|
|
|
return [
|
|
|
|
"append",
|
|
|
|
this.fileSystem.rms(
|
2019-10-31 00:29:55 +01:00
|
|
|
input.args,
|
|
|
|
input.hasAnyOption(["f", "force"]),
|
|
|
|
input.hasAnyOption(["r", "R", "recursive"]),
|
|
|
|
input.hasOption("no-preserve-root")
|
2019-10-27 01:15:56 +02:00
|
|
|
)
|
|
|
|
];
|
2018-11-28 22:07:51 +01:00
|
|
|
}
|
2018-11-28 19:51:48 +01:00
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private rmdir(input: InputArgs): OutputAction {
|
|
|
|
return ["append", this.fileSystem.rmdirs(input.args)];
|
2018-11-29 20:56:27 +01:00
|
|
|
}
|
|
|
|
|
2019-10-31 00:29:55 +01:00
|
|
|
private touch(input: InputArgs): OutputAction {
|
|
|
|
return ["append", this.fileSystem.createFiles(input.args)];
|
2019-10-21 02:25:42 +02:00
|
|
|
}
|
2019-10-26 16:08:46 +02:00
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
private whoami(): OutputAction {
|
2019-10-29 12:36:03 +01:00
|
|
|
const user = this.userSession.currentUser;
|
2019-10-26 16:08:46 +02:00
|
|
|
if (user === undefined)
|
|
|
|
throw "Cannot execute `whoami` while not logged in.";
|
|
|
|
|
2019-10-27 01:15:56 +02:00
|
|
|
return ["append", user.description];
|
2019-10-26 16:08:46 +02:00
|
|
|
}
|
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-10-27 01:15:56 +02:00
|
|
|
readonly fun: (args: InputArgs) => OutputAction;
|
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-10-31 00:29:55 +01:00
|
|
|
constructor(fun: (args: InputArgs) => OutputAction, summary: string, usage: string, desc: string,
|
|
|
|
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-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* A set of parsed command-line arguments.
|
|
|
|
*/
|
2018-11-29 08:58:53 +01:00
|
|
|
class InputArgs {
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The name of the command, i.e. the first word in the input string.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
readonly command: string;
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The set of options and the corresponding values that the user has given.
|
|
|
|
*/
|
2019-10-21 17:07:16 +02:00
|
|
|
private readonly _options: { [key: string]: string };
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* The remaining non-option arguments that the user has given.
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
private readonly _args: string[];
|
2019-10-20 23:55:04 +02:00
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Parses an input string into a set of command-line arguments.
|
|
|
|
*
|
|
|
|
* @param input the input string to parse
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
constructor(input: string) {
|
2018-12-02 17:17:39 +01:00
|
|
|
const inputParts = (input.match(/("[^"]+"|[^"\s]+)/g) || [])
|
2018-11-29 10:08:44 +01:00
|
|
|
.map(it => it.replace(/^"/, "").replace(/"$/, ""));
|
2018-12-02 17:17:39 +01:00
|
|
|
|
2019-10-26 23:41:02 +02:00
|
|
|
this.command = (inputParts[0] || "").toLowerCase().trim();
|
2018-12-02 17:17:39 +01:00
|
|
|
|
|
|
|
this._options = {};
|
2018-11-29 08:58:53 +01:00
|
|
|
let i;
|
2018-12-02 17:17:39 +01:00
|
|
|
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] = "");
|
2018-12-02 17:17:39 +01:00
|
|
|
} 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] || "");
|
|
|
|
}
|
|
|
|
|
2018-12-02 17:17:39 +01:00
|
|
|
this._args = inputParts.slice(i);
|
2018-11-29 08:58:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Returns a copy of the options the user has given.
|
|
|
|
*
|
|
|
|
* @return a copy of the options the user has given
|
|
|
|
*/
|
|
|
|
get options(): { [key: string]: string } {
|
2019-10-21 02:25:42 +02:00
|
|
|
return Object.assign({}, this._options);
|
|
|
|
}
|
|
|
|
|
2019-10-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Returns a copy of the arguments the user has given.
|
|
|
|
*
|
|
|
|
* @return a copy of the arguments the user has given
|
|
|
|
*/
|
2019-10-21 02:25:42 +02:00
|
|
|
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-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Returns `true` if and only if the option with the given key has been set.
|
|
|
|
*
|
|
|
|
* @param key the key to check
|
|
|
|
* @return `true` if and only if the option with the given key has been set
|
|
|
|
*/
|
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-29 12:36:03 +01:00
|
|
|
/**
|
|
|
|
* Returns `true` if and only if at least one of the options with the given keys has been set.
|
|
|
|
*
|
|
|
|
* @param keys the keys to check
|
|
|
|
* @return `true` if and only if at least one of the options with the given keys has been set
|
|
|
|
*/
|
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;
|
|
|
|
}
|
2019-10-29 12:36:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
2019-10-31 00:29:55 +01:00
|
|
|
* Returns `true` if and only if there is an argument at the given index.
|
2019-10-29 12:36:03 +01:00
|
|
|
*
|
2019-10-31 00:29:55 +01:00
|
|
|
* @param index the index to check
|
|
|
|
* @return `true` if and only if there is an argument at the given index
|
2019-10-29 12:36:03 +01:00
|
|
|
*/
|
2019-10-31 00:29:55 +01:00
|
|
|
hasArg(index: number): boolean {
|
|
|
|
return this._args[index] !== undefined;
|
2019-10-29 12:36:03 +01:00
|
|
|
}
|
2019-10-31 00:29:55 +01:00
|
|
|
}
|
2019-10-29 12:36:03 +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)
|
|
|
|
throw "`minArgs` must be less than or equal to `maxArgs`.";
|
|
|
|
|
|
|
|
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
|
|
|
|
* @return `[true]` if the input is valid, or `[false, string]` where the string is a reason if the input is not
|
|
|
|
* valid
|
|
|
|
*/
|
|
|
|
validate(input: InputArgs): [true] | [false, string] {
|
|
|
|
if (this.minArgs === this.maxArgs && input.args.length !== this.minArgs)
|
|
|
|
return [false, `Expected ${this.arguments(this.minArgs)} but got ${input.args.length}.`];
|
|
|
|
if (input.args.length < this.minArgs)
|
|
|
|
return [false, `Expected at least ${this.arguments(this.minArgs)} but got ${input.args.length}.`];
|
|
|
|
if (input.args.length > this.maxArgs)
|
|
|
|
return [false, `Expected at most ${this.arguments(this.maxArgs)} but got ${input.args.length}.`];
|
|
|
|
|
|
|
|
return [true];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns `"1 argument"` if the given amount is `1` and returns `"$n arguments"` otherwise.
|
|
|
|
*
|
|
|
|
* @param amount the amount to check
|
|
|
|
* @return `"1 argument"` if the given amount is `1` and returns `"$n arguments"` otherwise.
|
|
|
|
*/
|
|
|
|
private arguments(amount: number): string {
|
|
|
|
return amount === 1 ? `1 argument` : `${amount} arguments`;
|
2019-10-29 12:36:03 +01:00
|
|
|
}
|
2018-11-29 08:58:53 +01:00
|
|
|
}
|