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

656 lines
24 KiB
TypeScript
Raw Normal View History

2019-10-31 19:08:09 +01:00
import * as Cookies from "js-cookie";
import "./Extensions"
2019-11-04 18:19:24 +01:00
import {Directory, File, FileSystem, Path} from "./FileSystem"
import {IllegalStateError} from "./Shared";
import {Environment, InputArgs} from "./Shell";
import {EscapeCharacters} from "./Terminal";
2019-11-01 11:59:33 +01:00
import {UserSession} from "./UserSession";
2019-10-29 12:36:03 +01:00
/**
* A collection of commands executed within a particular user session.
*/
export class Commands {
/**
* 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.
*/
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 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
*/
constructor(environment: Environment, userSession: UserSession, fileSystem: FileSystem) {
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`,
`cat FILE ...`,
`Reads files sequentially, writing them to the standard output.`,
new InputValidator({minArgs: 1})
),
"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
),
"cd": new Command(
2019-10-21 02:25:42 +02:00
this.cd,
`change directory`,
`cd [DIRECTORY]`,
`Changes the current working directory to [DIRECTORY].
If [DIRECTORY] is empty, the current working directory is changed to the root.`.trimLines(),
2019-10-31 00:29:55 +01:00
new InputValidator({maxArgs: 1})
2019-10-21 02:25:42 +02:00
),
"cp": new Command(
2019-10-21 02:25:42 +02:00
this.cp,
`copy file`,
2019-10-31 14:15:27 +01:00
`cp [-R] SOURCE DESTINATION`,
2019-10-21 02:25:42 +02:00
`Copies SOURCE to DESTINATION.
2019-10-31 14:15:27 +01:00
SOURCE is what should be copied, and DESTINATION is where it should be copied to.
If DESTINATION is an existing directory, SOURCE is copied into that directory.
Unless -R is given, SOURCE must be a file.`.trimLines(),
2019-10-31 00:29:55 +01:00
new InputValidator({minArgs: 2, maxArgs: 2})
2019-10-21 02:25:42 +02:00
),
"echo": new Command(
2019-10-21 02:25:42 +02:00
this.echo,
`display text`,
`echo [-n] [TEXT]`,
`Displays [TEXT].
Unless the -n parameter is given, a newline is appended to the end.`.trimLines(),
new InputValidator()
2019-10-21 02:25:42 +02: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
),
"help": new Command(
2019-10-21 02:25:42 +02:00
this.help,
`display documentation`,
`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
),
"ls": new Command(
2019-10-21 02:25:42 +02:00
this.ls,
`list directory contents`,
2019-11-03 18:10:59 +01:00
`ls [-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.
2019-11-03 18:10:59 +01:00
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.`.trimLines(),
new InputValidator()
2019-10-21 02:25:42 +02:00
),
"man": new Command(
2019-10-21 02:25:42 +02:00
this.man,
`display manual documentation pages`,
`man PAGE...`,
`Displays the manual pages with names PAGE....`.trimLines(),
new InputValidator()
2019-10-21 02:25:42 +02:00
),
"mkdir": new Command(
2019-10-21 02:25:42 +02:00
this.mkdir,
`make directories`,
`mkdir [-p] DIRECTORY ...`,
2019-10-21 02:25:42 +02:00
`Creates the directories given by DIRECTORY.
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.`.trimLines(),
2019-10-31 00:29:55 +01:00
new InputValidator({minArgs: 1})
2019-10-21 02:25:42 +02: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
),
"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
),
"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
),
"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
),
"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
),
"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
),
"set": new Command(
this.set,
`set environment variable`,
`set key [value]`,
`Sets the environment variable with the given key to the given value.
If no value is given, the environment variable is cleared.`.trimLines(),
new InputValidator({minArgs: 1, maxArgs: 2})
),
"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
),
"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
* @return the output generated by that command
*/
2019-11-02 21:28:32 +01:00
execute(input: InputArgs): string {
if (input.command === "factory-reset") {
Cookies.remove("files");
Cookies.remove("cwd");
2019-10-31 14:04:42 +01:00
Cookies.remove("user");
Cookies.remove("env");
location.reload();
2019-10-31 22:46:42 +01:00
throw new Error("Goodbye");
}
2019-10-31 00:29:55 +01:00
if (input.command === "")
return "";
2019-10-31 00:29:55 +01:00
if (!this.commands.hasOwnProperty(input.command))
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 `.`
* @return an output action corresponding to an error message about invalid usage of a command
*/
private createUsageErrorOutput(commandName: string, errorMessage: string | undefined): string {
2019-10-31 00:29:55 +01:00
const command = this.commands[commandName];
if (command === undefined)
throw new Error(`Unknown command \`${commandName}\`.`);
2019-10-31 00:29:55 +01:00
return "" +
2019-10-31 00:29:55 +01:00
`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
private cat(input: InputArgs): string {
return input.args
.map(arg => Path.interpret(this.environment["cwd"].value, 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))
return `cat: ${it}: No such file`;
2019-10-31 01:34:36 +01:00
return node.contents;
})
.filter(it => it !== "")
.join("\n");
2019-10-31 01:34:36 +01:00
}
private cd(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
if (input.args[0] === "") {
this.environment["cwd"] = {value: "/", readonly: true};
2019-11-04 18:19:24 +01:00
return "";
}
const path = Path.interpret(this.environment["cwd"].value, input.args[0]);
2019-11-04 18:19:24 +01:00
if (!this.fileSystem.has(path))
return `The directory '${path}' does not exist.`;
this.environment["cwd"] = {value: path.toString(), readonly: true};
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
private cp(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
try {
this.fileSystem.copy(
Path.interpret(this.environment["cwd"].value, input.args[0]),
Path.interpret(this.environment["cwd"].value, input.args[1]),
2019-11-04 18:19:24 +01:00
input.hasAnyOption(["r", "R"])
);
return "";
} catch (error) {
return error.message;
}
2018-11-29 13:34:46 +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
private echo(input: InputArgs): string {
return input.args.join(" ").replace("hunter2", "*******")
+ (input.hasOption("n") ? "" : "\n");
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
private exit(): string {
2019-10-29 12:36:03 +01:00
this.userSession.logOut();
return "";
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +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
if (input.args.length > 0) {
return input.args
.map(it => {
if (!this.commands.hasOwnProperty(it))
return `Unknown command ${it}.`;
const commandName = it.toLowerCase();
const command = this.commands[commandName];
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
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
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
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
}
private ls(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
const lists = (input.args.length === 0 ? [""] : input.args)
.map(arg => Path.interpret(this.environment["cwd"].value, 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 = [
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
}
private man(input: InputArgs): string {
2019-10-31 00:29:55 +01:00
if (input.args.length === 0)
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)
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
}
private mkdir(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
return input.args
.map(arg => Path.interpret(this.environment["cwd"].value, 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;
}
})
.filter(it => it !== "")
2019-11-04 18:19:24 +01:00
.join("\n");
2018-11-28 23:22:17 +01:00
}
private mv(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
try {
this.fileSystem.move(
Path.interpret(this.environment["cwd"].value, input.args[0]),
Path.interpret(this.environment["cwd"].value, input.args[1])
);
2019-11-04 18:19:24 +01:00
return "";
} catch (error) {
return error.message;
}
2018-11-29 13:18:48 +01:00
}
private open(input: InputArgs): string {
const path = Path.interpret(this.environment["cwd"].value, 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);
return "";
2018-11-29 00:15:08 +01:00
}
private poweroff(): string {
const user = this.userSession.currentUser;
if (user === undefined)
2019-10-31 22:46:42 +01:00
throw new IllegalStateError("Cannot execute `poweroff` while not logged in.");
Cookies.set("poweroff", "true", {
"expires": new Date(new Date().setSeconds(new Date().getSeconds() + 30)),
"path": "/"
});
2018-11-28 23:22:17 +01:00
setTimeout(() => location.reload(), 2000);
return "" +
2018-11-28 23:22:17 +01:00
`Shutdown NOW!
*** FINAL System shutdown message from ${user.name}@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
}
private pwd(): string {
return this.environment["cwd"].value;
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01:00
private rm(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
return input.args
.map(arg => Path.interpret(this.environment["cwd"].value, 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")
);
return "";
2019-11-04 18:19:24 +01:00
} catch (error) {
return error.message;
}
})
.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
private rmdir(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
return input.args
.map(arg => Path.interpret(this.environment["cwd"].value, 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;
}
})
.filter(it => it !== "")
2019-11-04 18:19:24 +01:00
.join("\n");
2018-11-29 20:56:27 +01:00
}
private set(input: InputArgs): string {
const key = input.args[0];
if (!key.match(/^[0-9a-z_]+$/i))
return "Environment variable keys can only contain alphanumerical characters and underscores.";
if (this.environment[key] !== undefined && this.environment[key].readonly)
return "Cannot set read-only environment variable.";
if (input.args.length === 1)
delete this.environment[input.args[0]];
else
this.environment[input.args[0]] = {value: input.args[1], readonly: false};
return "";
}
private touch(input: InputArgs): string {
2019-11-04 18:19:24 +01:00
return input.args
.map(arg => Path.interpret(this.environment["cwd"].value, 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;
}
})
.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
private whoami(): string {
2019-10-29 12:36:03 +01:00
const user = this.userSession.currentUser;
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
return 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.
*/
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
*/
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
* @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)
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
* @return `"1 argument"` if the given amount is `1` and returns `"$n arguments"` otherwise.
*/
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
}