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

646 lines
22 KiB
TypeScript
Raw Normal View History

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-29 12:36:03 +01:00
/**
* A collection of commands executed within a particular user session.
*/
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})
),
"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].
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
),
"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
),
"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(),
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`,
`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
),
"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 DIRECTORY...`,
`Creates the directories given by DIRECTORY.
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
),
"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
),
"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-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") {
// @ts-ignore
Cookies.remove("files");
// @ts-ignore
Cookies.remove("cwd");
location.reload();
throw "Goodbye";
}
2019-10-31 00:29:55 +01:00
const input = new InputArgs(stripHtmlTags(inputString));
if (input.command === "")
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")
]
}
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
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
}
private clear(): OutputAction {
return ["clear"];
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01: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
private exit(): OutputAction {
2019-10-29 12:36:03 +01:00
this.userSession.logOut();
return ["nothing"];
2018-11-28 22:07:51 +01:00
}
2018-11-28 19:51:48 +01: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
if (input.args.length > 0) {
return ["append",
input.args
.map(it => {
if (!this.commands.hasOwnProperty(it))
return `Unknown command ${it}.`;
2018-11-28 19:51:48 +01:00
const commandName = it.toLowerCase();
const command = this.commands[commandName];
2018-11-28 19:51:48 +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
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
}
private ls(input: InputArgs): OutputAction {
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)
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)
return ["append", `The file '${fileName}' does not exist`];
2019-10-21 02:25:42 +02:00
if (!(node instanceof File))
return ["append", `'${fileName}' is not a file`];
2018-11-29 00:15:08 +01:00
// @ts-ignore: False positive
2019-10-31 01:27:31 +01:00
window.open(node.contents, target);
return ["nothing"];
2018-11-29 00:15:08 +01:00
}
private poweroff(): OutputAction {
const user = this.userSession.currentUser;
if (user === undefined)
throw "Cannot execute `poweroff` while not logged in.";
// @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);
return ["append",
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(): OutputAction {
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 {
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")
)
];
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
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.";
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.
*/
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-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) {
const inputParts = (input.match(/("[^"]+"|[^"\s]+)/g) || [])
2018-11-29 10:08:44 +01:00
.map(it => it.replace(/^"/, "").replace(/"$/, ""));
this.command = (inputParts[0] || "").toLowerCase().trim();
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-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
}