forked from tools/josh
1
0
Fork 0

Use standardised error codes

Fixes #124.
This commit is contained in:
Florine W. Dekker 2020-06-30 18:49:19 +02:00
parent 17d8f2dbba
commit d078deb545
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
3 changed files with 242 additions and 175 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "fwdekker.com", "name": "fwdekker.com",
"version": "0.34.0", "version": "0.35.0",
"description": "The source code of [my personal website](https://fwdekker.com/).", "description": "The source code of [my personal website](https://fwdekker.com/).",
"author": "Felix W. Dekker", "author": "Felix W. Dekker",
"browser": "dist/bundle.js", "browser": "dist/bundle.js",

View File

@ -50,26 +50,26 @@ export class Commands {
*/ */
execute(input: InputArgs, streams: StreamSet): number { execute(input: InputArgs, streams: StreamSet): number {
if (input.command === "") if (input.command === "")
return 0; return ExitCode.OK;
const command = this.resolve(input.command); const command = this.resolve(input.command);
if (command === undefined) { if (command === undefined) {
streams.err.writeLine(`Unknown command '${input.command}'.`); streams.err.writeLine(`Unknown command '${input.command}'.`);
return -1; return ExitCode.COMMAND_NOT_FOUND;
} }
if (command instanceof Error) { if (command instanceof Error) {
streams.err.writeLine(`Could not parse command '${input.command}': ${command}.`); streams.err.writeLine(`Could not parse command '${input.command}': ${command}.`);
return -1; return ExitCode.COMMAND_NOT_FOUND;
} }
if (command instanceof DocOnlyCommand) { if (command instanceof DocOnlyCommand) {
streams.err.writeLine(`Could not execute doc-only command. Try 'help ${input.command}' instead.`); streams.err.writeLine(`Could not execute doc-only command. Try 'help ${input.command}' instead.`);
return -1; return ExitCode.COMMAND_NOT_FOUND;
} }
const validation = command.validator.validate(input); const validation = command.validator.validate(input);
if (!validation[0]) { if (!validation[0]) {
streams.err.writeLine(this.createUsageErrorOutput(input.command, command, validation[1])); streams.err.writeLine(this.createUsageErrorOutput(input.command, command, validation[1]));
return -1; return ExitCode.USAGE;
} }
return command.fun.bind(this)(input, streams); return command.fun.bind(this)(input, streams);
@ -130,6 +130,7 @@ export class Commands {
"Directory": Directory, "Directory": Directory,
"DocOnlyCommand": DocOnlyCommand, "DocOnlyCommand": DocOnlyCommand,
"EscapeCharacters": EscapeCharacters, "EscapeCharacters": EscapeCharacters,
"ExitCode": ExitCode,
"File": File, "File": File,
"HashProvider": HashProvider, "HashProvider": HashProvider,
"InputParser": InputParser, "InputParser": InputParser,
@ -284,6 +285,58 @@ export class InputValidator {
} }
} }
/**
* Standard exit codes.
*
* Inspired by `/usr/include/sysexits.h` as seen in FreeBSD 12.1.
*/
export enum ExitCode {
/**
* Successful termination.
*/
OK = 0,
/**
* Some unspecified error.
*/
MISC = 1,
/**
* Command line usage error.
*/
USAGE = 64,
/**
* Input data was incorrect in one way or another.
*/
DATA_ERROR = 65,
/**
* An input file could not be found.
*/
FILE_NOT_FOUND = 66,
/**
* An output file could not be created.
*/
CANT_CREATE = 73,
/**
* Some I/O operation failed on a file.
*/
IO_ERROR = 74,
/**
* The failure is temporary, and the user should probably try again.
*/
TEMP_FAIL = 75,
/**
* Not enough permissions to execute the desired action.
*/
PERMISSION = 77,
/**
* Something was not configured correctly.
*/
CONFIG = 78,
/**
* The desired command could not be found.
*/
COMMAND_NOT_FOUND = 127
}
// An escaped newline escape symbol. // An escaped newline escape symbol.
const n = "\\\\\\"; const n = "\\\\\\";
@ -298,8 +351,8 @@ export const commandBinaries: { [key: string]: string } = {
"and": /* language=JavaScript */ `\ "and": /* language=JavaScript */ `\
return new Command( return new Command(
(input, streams) => { (input, streams) => {
const previousStatus = Number(josh.environment.getOrDefault("status", "0")); const previousStatus = Number(josh.environment.getOrDefault("status", ExitCode.OK.toString()));
if (previousStatus !== 0) if (previousStatus !== ExitCode.OK)
return previousStatus; return previousStatus;
return josh.interpreter.execute( return josh.interpreter.execute(
@ -310,7 +363,7 @@ return new Command(
\`execute command if previous command did not fail\`, \`execute command if previous command did not fail\`,
\`and <u>command</u>\`, \`and <u>command</u>\`,
\`Executes <u>command</u> with its associated options and arguments if and only if the status code of the ${n} \`Executes <u>command</u> with its associated options and arguments if and only if the status code of the ${n}
previously-executed command is 0. previously-executed command is ${ExitCode.OK}.
The exit code is retained if it was non-zero, and is changed to that of <u>command</u> otherwise.${n} The exit code is retained if it was non-zero, and is changed to that of <u>command</u> otherwise.${n}
\`.trimMultiLines(), \`.trimMultiLines(),
@ -325,7 +378,7 @@ return new Command(
const node = josh.fileSystem.get(path); const node = josh.fileSystem.get(path);
if (!(node instanceof File)) { if (!(node instanceof File)) {
streams.err.writeLine(\`cat: '\${path}': No such file.\`); streams.err.writeLine(\`cat: '\${path}': No such file.\`);
return -1; return ExitCode.FILE_NOT_FOUND;
} }
let contents = node.open("read").read(); let contents = node.open("read").read();
@ -335,9 +388,9 @@ return new Command(
contents += "\\n"; contents += "\\n";
streams.out.write(contents); streams.out.write(contents);
return 0; return ExitCode.OK;
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`concatenate and print files\`, \`concatenate and print files\`,
\`cat [<b>-e</b> | <b>--escape-html</b>] <u>file</u> <u>...</u>\`, \`cat [<b>-e</b> | <b>--escape-html</b>] <u>file</u> <u>...</u>\`,
@ -352,17 +405,21 @@ return new Command(
(input, streams) => { (input, streams) => {
if (input.argc === 0) { if (input.argc === 0) {
josh.environment.set("cwd", josh.environment.get("home")); josh.environment.set("cwd", josh.environment.get("home"));
return 0; return ExitCode.OK;
} }
const path = Path.interpret(josh.environment.get("cwd"), input.args[0]); const path = Path.interpret(josh.environment.get("cwd"), input.args[0]);
if (!(josh.fileSystem.get(path) instanceof Directory)) { const target = josh.fileSystem.get(path);
if (target === undefined) {
streams.err.writeLine(\`cd: The directory '\${path}' does not exist.\`); streams.err.writeLine(\`cd: The directory '\${path}' does not exist.\`);
return -1; return ExitCode.FILE_NOT_FOUND;
} else if (!(target instanceof Directory)) {
streams.err.writeLine(\`cd: '\${path}' is not a directory.\`);
return ExitCode.USAGE;
} }
josh.environment.set("cwd", path.toString()); josh.environment.set("cwd", path.toString());
return 0; return ExitCode.OK;
}, },
\`change directory\`, \`change directory\`,
\`cd [<u>directory</u>]\`, \`cd [<u>directory</u>]\`,
@ -374,7 +431,7 @@ return new Command(
return new Command( return new Command(
(input, streams) => { (input, streams) => {
streams.out.write(EscapeCharacters.Escape + EscapeCharacters.Clear); streams.out.write(EscapeCharacters.Escape + EscapeCharacters.Clear);
return 0; return ExitCode.OK;
}, },
\`clear terminal output\`, \`clear terminal output\`,
\`clear\`, \`clear\`,
@ -393,20 +450,20 @@ return new Command(
); );
} catch (error) { } catch (error) {
streams.err.writeLine(\`cp: \${error.message}\`); streams.err.writeLine(\`cp: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
return mappings return mappings
.map(([source, destination]) => { .map(([source, destination]) => {
try { try {
josh.fileSystem.copy(source, destination, input.hasAnyOption("-r", "-R", "--recursive")); josh.fileSystem.copy(source, destination, input.hasAnyOption("-r", "-R", "--recursive"));
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`cp: \${error.message}\`); streams.err.writeLine(\`cp: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`copy files\`, \`copy files\`,
\`cp [<b>-r</b> | <b>-R</b> | <b>--recursive</b>] <u>source</u> <u>target file</u> \`cp [<b>-r</b> | <b>-R</b> | <b>--recursive</b>] <u>source</u> <u>target file</u>
@ -431,7 +488,7 @@ return new Command(
else else
streams.out.writeLine(message); streams.out.writeLine(message);
return 0; return ExitCode.OK;
}, },
\`display text\`, \`display text\`,
\`echo [<b>-n</b> | <b>--newline</b>] [<u>text</u> <u>...</u>]\`, \`echo [<b>-n</b> | <b>--newline</b>] [<u>text</u> <u>...</u>]\`,
@ -444,7 +501,7 @@ return new Command(
return new Command( return new Command(
(input, streams) => { (input, streams) => {
josh.environment.set("user", ""); josh.environment.set("user", "");
return 0; return ExitCode.OK;
}, },
\`close session\`, \`close session\`,
\`exit\`, \`exit\`,
@ -463,7 +520,7 @@ return new Command(
const command = josh.interpreter.resolve(commandName); const command = josh.interpreter.resolve(commandName);
if (command === undefined) { if (command === undefined) {
streams.out.writeLine(\`Unknown command '\${commandName}'.\`); streams.out.writeLine(\`Unknown command '\${commandName}'.\`);
return -1; return ExitCode.COMMAND_NOT_FOUND;
} }
let helpString = "<b>Name</b>\\n" + commandName; let helpString = "<b>Name</b>\\n" + commandName;
@ -475,14 +532,14 @@ return new Command(
helpString += "\\n\\n<b>Description</b>\\n" + command.desc; helpString += "\\n\\n<b>Description</b>\\n" + command.desc;
streams.out.writeLine(helpString); streams.out.writeLine(helpString);
return 0; return ExitCode.OK;
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
} else { } else {
const cwd = josh.environment.get("cwd"); const cwd = josh.environment.get("cwd");
const slashBin = josh.fileSystem.get(Path.interpret(cwd, "/bin")); const slashBin = josh.fileSystem.get(Path.interpret(cwd, "/bin"));
if (!(slashBin instanceof Directory)) { if (!(slashBin instanceof Directory)) {
return -1; return ExitCode.FILE_NOT_FOUND;
} }
const commands = {}; const commands = {};
@ -510,7 +567,7 @@ return new Command(
Write "help [COMMAND]" or click a command in the list above for more information.\`.trimMultiLines() Write "help [COMMAND]" or click a command in the list above for more information.\`.trimMultiLines()
); );
return 0; return ExitCode.OK;
} }
}, },
\`display documentation\`, \`display documentation\`,
@ -549,11 +606,11 @@ return new Command(
const node = josh.fileSystem.get(path); const node = josh.fileSystem.get(path);
if (node === undefined) { if (node === undefined) {
streams.err.writeLine(\`ls: The directory '\${path}' does not exist.\`); streams.err.writeLine(\`ls: The directory '\${path}' does not exist.\`);
return -1; return ExitCode.FILE_NOT_FOUND;
} }
if (!(node instanceof Directory)) { if (!(node instanceof Directory)) {
streams.err.writeLine(\`ls: '\${path}' is not a directory.\`); streams.err.writeLine(\`ls: '\${path}' is not a directory.\`);
return -1; return ExitCode.USAGE;
} }
const dirList = [ const dirList = [
@ -582,9 +639,9 @@ return new Command(
if (input.argc > 1) if (input.argc > 1)
streams.out.writeLine(\`<b>\${path}</b>\`); streams.out.writeLine(\`<b>\${path}</b>\`);
streams.out.writeLine(dirList.concat(fileList).join("\\n")); streams.out.writeLine(dirList.concat(fileList).join("\\n"));
return 0; return ExitCode.OK;
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`list directory contents\`, \`list directory contents\`,
\`ls [<b>-a</b> | <b>-A</b> | <b>--all</b>] [<u>directory</u> <u>...</u>]\`, \`ls [<b>-a</b> | <b>-A</b> | <b>--all</b>] [<u>directory</u> <u>...</u>]\`,
@ -604,13 +661,13 @@ return new Command(
.map(path => { .map(path => {
try { try {
josh.fileSystem.add(path, new Directory(), input.hasAnyOption("-p", "--parents")); josh.fileSystem.add(path, new Directory(), input.hasAnyOption("-p", "--parents"));
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`mkdir: \${error.message}\`); streams.err.writeLine(\`mkdir: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`make directories\`, \`make directories\`,
\`mkdir [<b>-p</b> | <b>--parents</b>] <u>directory</u> <u>...</u>\`, \`mkdir [<b>-p</b> | <b>--parents</b>] <u>directory</u> <u>...</u>\`,
@ -632,20 +689,20 @@ return new Command(
); );
} catch (error) { } catch (error) {
streams.err.writeLine(\`mv: \${error.message}\`); streams.err.writeLine(\`mv: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
return mappings return mappings
.map(([source, destination]) => { .map(([source, destination]) => {
try { try {
josh.fileSystem.move(source, destination); josh.fileSystem.move(source, destination);
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`mv: \${error.message}\`); streams.err.writeLine(\`mv: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`move files\`, \`move files\`,
\`mv <u>source</u> <u>destination file</u> \`mv <u>source</u> <u>destination file</u>
@ -659,15 +716,17 @@ return new Command(
"not": /* language=JavaScript */ `\ "not": /* language=JavaScript */ `\
return new Command( return new Command(
(input, streams) => { (input, streams) => {
return Number(!josh.interpreter.execute( const exitCode = josh.interpreter.execute(
InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args),
streams streams
)); );
return exitCode === ExitCode.OK ? ExitCode.MISC : ExitCode.OK;
}, },
\`execute command and invert status code\`, \`execute command and invert status code\`,
\`not <u>command</u>\`, \`not <u>command</u>\`,
\`Executes <u>command</u> with its associated options and arguments and inverts its exit code. More precisely, ${n} \`Executes <u>command</u> with its associated options and arguments and inverts its exit code. More precisely, ${n}
the exit code is set to 0 if it was non-zero, and is set to 1 otherwise.\`.trimMultiLines(), the exit code is set to ${ExitCode.OK} if it was non-zero, and is set to ${ExitCode.MISC} otherwise.${n}
\`.trimMultiLines(),
new InputValidator({minArgs: 1}) new InputValidator({minArgs: 1})
)`, )`,
"open": /* language=JavaScript */ `\ "open": /* language=JavaScript */ `\
@ -681,13 +740,13 @@ return new Command(
? "_blank" ? "_blank"
: "_self"; : "_self";
window.open(josh.fileSystem.open(path, "read").read(), target); window.open(josh.fileSystem.open(path, "read").read(), target);
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`open: \${error.message}\`); streams.err.writeLine(\`open: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`open web pages\`, \`open web pages\`,
\`open [<b>-b</b> | <b>--blank</b>] <u>file</u> <u>...</u>\`, \`open [<b>-b</b> | <b>--blank</b>] <u>file</u> <u>...</u>\`,
@ -702,8 +761,8 @@ return new Command(
"or": /* language=JavaScript */ `\ "or": /* language=JavaScript */ `\
return new Command( return new Command(
(input, streams) => { (input, streams) => {
const previousStatus = Number(josh.environment.getOrDefault("status", "0")); const previousStatus = Number(josh.environment.getOrDefault("status", "" +ExitCode.OK));
if (previousStatus === 0) if (previousStatus === ExitCode.OK)
return previousStatus; return previousStatus;
return josh.interpreter.execute( return josh.interpreter.execute(
@ -714,7 +773,7 @@ return new Command(
\`execute command if previous command failed\`, \`execute command if previous command failed\`,
\`or <u>command</u>\`, \`or <u>command</u>\`,
\`Executes <u>command</u> with its associated options and arguments if and only if the status code of the ${n} \`Executes <u>command</u> with its associated options and arguments if and only if the status code of the ${n}
previously-executed command is not 0. previously-executed command is not ${ExitCode.OK}.
The exit code is retained if it was zero, and is changed to that of <u>command</u> otherwise.\`.trimMultiLines(), The exit code is retained if it was zero, and is changed to that of <u>command</u> otherwise.\`.trimMultiLines(),
new InputValidator({minArgs: 1}) new InputValidator({minArgs: 1})
@ -725,7 +784,7 @@ return new Command(
const userName = josh.environment.get("user"); const userName = josh.environment.get("user");
if (userName === "") { if (userName === "") {
streams.err.writeLine("poweroff: Cannot execute while not logged in."); streams.err.writeLine("poweroff: Cannot execute while not logged in.");
return -1; return ExitCode.MISC;
} }
Persistence.setPoweroff(true); Persistence.setPoweroff(true);
@ -741,7 +800,7 @@ return new Command(
System shutdown time has arrived\`.trimLines() System shutdown time has arrived\`.trimLines()
); );
return 0; return ExitCode.OK;
}, },
\`close down the system\`, \`close down the system\`,
\`poweroff\`, \`poweroff\`,
@ -752,7 +811,7 @@ return new Command(
return new Command( return new Command(
(input, streams) => { (input, streams) => {
streams.out.writeLine(josh.environment.get("cwd") || ""); streams.out.writeLine(josh.environment.get("cwd") || "");
return 0; return ExitCode.OK;
}, },
\`print working directory\`, \`print working directory\`,
\`pwd\`, \`pwd\`,
@ -769,30 +828,30 @@ return new Command(
const target = josh.fileSystem.get(path); const target = josh.fileSystem.get(path);
if (target === undefined) { if (target === undefined) {
if (input.hasAnyOption("-f", "--force")) if (input.hasAnyOption("-f", "--force"))
return 0; return ExitCode.OK;
streams.err.writeLine(\`rm: The file '\${path}' does not exist.\`); streams.err.writeLine(\`rm: The file '\${path}' does not exist.\`);
return -1; return ExitCode.FILE_NOT_FOUND;
} }
if (target instanceof Directory) { if (target instanceof Directory) {
if (!input.hasAnyOption("-r", "-R", "--recursive")) { if (!input.hasAnyOption("-r", "-R", "--recursive")) {
streams.err.writeLine(\`rm: '\${path}' is a directory.\`); streams.err.writeLine(\`rm: '\${path}' is a directory.\`);
return -1; return ExitCode.USAGE;
} }
if (path.toString() === "/" && !input.hasAnyOption("--no-preserve-root")) { if (path.toString() === "/" && !input.hasAnyOption("--no-preserve-root")) {
streams.err.writeLine("rm: Cannot remove root directory."); streams.err.writeLine("rm: Cannot remove root directory.");
return -1; return ExitCode.USAGE;
} }
} }
josh.fileSystem.remove(path); josh.fileSystem.remove(path);
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`rm: \${error.message}\`); streams.err.writeLine(\`rm: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`remove file\`, \`remove file\`,
\`rm [<b>-f</b> | <b>--force</b>] [<b>-r</b> | <b>-R</b> | <b>--recursive</b>] ${n} \`rm [<b>-f</b> | <b>--force</b>] [<b>-r</b> | <b>-R</b> | <b>--recursive</b>] ${n}
@ -818,25 +877,25 @@ return new Command(
const target = josh.fileSystem.get(path); const target = josh.fileSystem.get(path);
if (target === undefined) { if (target === undefined) {
streams.err.writeLine(\`rmdir: '\${path}' does not exist.\`); streams.err.writeLine(\`rmdir: '\${path}' does not exist.\`);
return -1; return ExitCode.FILE_NOT_FOUND;
} }
if (!(target instanceof Directory)) { if (!(target instanceof Directory)) {
streams.err.writeLine(\`rmdir: '\${path}' is not a directory.\`); streams.err.writeLine(\`rmdir: '\${path}' is not a directory.\`);
return -1; return ExitCode.USAGE;
} }
if (target.nodeCount !== 0) { if (target.nodeCount !== 0) {
streams.err.writeLine(\`rmdir: '\${path}' is not empty.\`); streams.err.writeLine(\`rmdir: '\${path}' is not empty.\`);
return -1; return ExitCode.MISC;
} }
josh.fileSystem.remove(path); josh.fileSystem.remove(path);
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`rmdir: \${error.message}\`); streams.err.writeLine(\`rmdir: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`remove directories\`, \`remove directories\`,
\`rmdir <u>directory</u> <u>...</u>\`, \`rmdir <u>directory</u> <u>...</u>\`,
@ -852,12 +911,12 @@ return new Command(
josh.environment.safeDelete(input.args[0]); josh.environment.safeDelete(input.args[0]);
else else
josh.environment.safeSet(input.args[0], input.args[1]); josh.environment.safeSet(input.args[0], input.args[1]);
return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`set: \${error.message}\`); streams.err.writeLine(\`set: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
return 0;
}, },
\`set environment variable\`, \`set environment variable\`,
\`set <u>key</u> [<u>value</u>]\`, \`set <u>key</u> [<u>value</u>]\`,
@ -873,13 +932,13 @@ return new Command(
.map(path => { .map(path => {
try { try {
josh.fileSystem.add(path, new File(), false); josh.fileSystem.add(path, new File(), false);
return 0; return ExitCode.OK;
} catch (error) { } catch (error) {
streams.err.writeLine(\`touch: \${error.message}\`); streams.err.writeLine(\`touch: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
}) })
.reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); .reduce((acc, exitCode) => exitCode === ExitCode.OK ? acc : exitCode);
}, },
\`change file timestamps\`, \`change file timestamps\`,
\`touch <u>file</u> <u>...</u>\`, \`touch <u>file</u> <u>...</u>\`,
@ -892,7 +951,7 @@ return new Command(
(input, streams) => { (input, streams) => {
if (josh.userList.has(input.args[0])) { if (josh.userList.has(input.args[0])) {
streams.err.writeLine(\`useradd: User '\${input.args[0]}' already exists.\`); streams.err.writeLine(\`useradd: User '\${input.args[0]}' already exists.\`);
return -1; return ExitCode.MISC;
} }
let user; let user;
@ -904,16 +963,16 @@ return new Command(
user.description = input.options["-d"] || input.options["--description"]; user.description = input.options["-d"] || input.options["--description"];
} catch (error) { } catch (error) {
streams.err.writeLine(\`useradd: \${error.message}\`); streams.err.writeLine(\`useradd: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
if (!josh.userList.add(user)) { if (!josh.userList.add(user)) {
streams.err.writeLine(\`useradd: Unexpectedly failed to add user '\${input.args[0]}'.\`); streams.err.writeLine(\`useradd: Unexpectedly failed to add user '\${input.args[0]}'.\`);
return -1; return ExitCode.MISC;
} }
streams.out.writeLine(\`useradd: Added user '\${input.args[0]}'.\`); streams.out.writeLine(\`useradd: Added user '\${input.args[0]}'.\`);
return 0; return ExitCode.OK;
}, },
\`add new user\`, \`add new user\`,
\`useradd ${n} \`useradd ${n}
@ -934,16 +993,16 @@ return new Command(
(input, streams) => { (input, streams) => {
if (!josh.userList.has(input.args[0])) { if (!josh.userList.has(input.args[0])) {
streams.err.writeLine(\`userdel: Could not delete non-existent user '\${input.args[0]}'.\`); streams.err.writeLine(\`userdel: Could not delete non-existent user '\${input.args[0]}'.\`);
return -1; return ExitCode.USAGE;
} }
if (!josh.userList.delete(input.args[0])) { if (!josh.userList.delete(input.args[0])) {
streams.err.writeLine(\`userdel: Unexpectedly failed to delete user '\${input.args[0]}'.\`); streams.err.writeLine(\`userdel: Unexpectedly failed to delete user '\${input.args[0]}'.\`);
return -1; return ExitCode.MISC;
} }
streams.out.writeLine(\`userdel: Deleted user '\${input.args[0]}'.\`); streams.out.writeLine(\`userdel: Deleted user '\${input.args[0]}'.\`);
return 0; return ExitCode.OK;
}, },
\`delete user\`, \`delete user\`,
\`userdel <u>name</u>\`, \`userdel <u>name</u>\`,
@ -956,7 +1015,7 @@ return new Command(
let user = josh.userList.get(input.args[0]); let user = josh.userList.get(input.args[0]);
if (user === undefined) { if (user === undefined) {
streams.err.writeLine(\`usermod: Could not modify non-existent user '\${input.args[0]}'.\`); streams.err.writeLine(\`usermod: Could not modify non-existent user '\${input.args[0]}'.\`);
return -1; return ExitCode.USAGE;
} }
try { try {
@ -970,16 +1029,16 @@ return new Command(
user.description = input.options["-d"] || input.options["--description"]; user.description = input.options["-d"] || input.options["--description"];
} catch (error) { } catch (error) {
streams.err.writeLine(\`usermod: \${error.message}\`); streams.err.writeLine(\`usermod: \${error.message}\`);
return -1; return ExitCode.MISC;
} }
if (!josh.userList.modify(user)) { if (!josh.userList.modify(user)) {
streams.err.writeLine(\`usermod: Unexpectedly failed to modify user '\${input.args[0]}'.\`); streams.err.writeLine(\`usermod: Unexpectedly failed to modify user '\${input.args[0]}'.\`);
return -1; return ExitCode.MISC;
} }
streams.out.writeLine(\`usermod: Modified user '\${input.args[0]}'.\`); streams.out.writeLine(\`usermod: Modified user '\${input.args[0]}'.\`);
return 0; return ExitCode.OK;
}, },
\'modify user\', \'modify user\',
\`usermod ${n} \`usermod ${n}
@ -997,11 +1056,11 @@ return new Command(
const user = josh.userList.get(josh.environment.get("user")); const user = josh.userList.get(josh.environment.get("user"));
if (user === undefined) { if (user === undefined) {
streams.err.writeLine("whoami: Cannot execute while not logged in."); streams.err.writeLine("whoami: Cannot execute while not logged in.");
return -1; return ExitCode.MISC;
} }
streams.out.writeLine(user.description); streams.out.writeLine(user.description);
return 0; return ExitCode.OK;
}, },
\`print short description of user\`, \`print short description of user\`,
\`whoami\`, \`whoami\`,

View File

@ -2,7 +2,7 @@ import {expect} from "chai";
import "jsdom-global"; import "jsdom-global";
import "mocha"; import "mocha";
import {Command, commandBinaries, Commands} from "../main/js/Commands"; import {Command, commandBinaries, Commands, ExitCode} from "../main/js/Commands";
import {Environment} from "../main/js/Environment"; import {Environment} from "../main/js/Environment";
import {Directory, File, FileSystem, Path} from "../main/js/FileSystem"; import {Directory, File, FileSystem, Path} from "../main/js/FileSystem";
import {InputParser} from "../main/js/InputParser"; import {InputParser} from "../main/js/InputParser";
@ -55,21 +55,21 @@ describe("commands", () => {
describe("execute", () => { describe("execute", () => {
it("writes an error if it cannot resolve the command", () => { it("writes an error if it cannot resolve the command", () => {
expect(execute("does-not-exist")).to.equal(-1); expect(execute("does-not-exist")).to.equal(ExitCode.COMMAND_NOT_FOUND);
expect(readErr()).to.equal("Unknown command 'does-not-exist'.\n"); expect(readErr()).to.equal("Unknown command 'does-not-exist'.\n");
}); });
it("writes an error if the command is invalid", () => { it("writes an error if the command is invalid", () => {
fileSystem.add(new Path("/command"), new File("invalid"), false); fileSystem.add(new Path("/command"), new File("invalid"), false);
expect(execute("/command")).to.equal(-1); expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
expect(readErr()).to.equal("Could not parse command '/command': ReferenceError: invalid is not defined.\n"); expect(readErr()).to.equal("Could not parse command '/command': ReferenceError: invalid is not defined.\n");
}); });
it("writes an error if the command is a doc-only command", () => { it("writes an error if the command is a doc-only command", () => {
fileSystem.add(new Path("/command"), new File(`return new DocOnlyCommand("", "")`), false); fileSystem.add(new Path("/command"), new File(`return new DocOnlyCommand("", "")`), false);
expect(execute("/command")).to.equal(-1); expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
expect(readErr()).to.equal("Could not execute doc-only command. Try 'help /command' instead.\n"); expect(readErr()).to.equal("Could not execute doc-only command. Try 'help /command' instead.\n");
}); });
@ -77,7 +77,7 @@ describe("commands", () => {
const command = `return new Command("", "", "", "", new InputValidator({minArgs: 2}))`; const command = `return new Command("", "", "", "", new InputValidator({minArgs: 2}))`;
fileSystem.add(new Path("/command"), new File(command), false); fileSystem.add(new Path("/command"), new File(command), false);
expect(execute("/command arg1")).to.equal(-1); expect(execute("/command arg1")).to.equal(ExitCode.USAGE);
expect(readErr()).to.contain("Invalid usage of '/command'. Expected at least 2 arguments but got 1."); expect(readErr()).to.contain("Invalid usage of '/command'. Expected at least 2 arguments but got 1.");
}); });
@ -142,17 +142,25 @@ describe("commands", () => {
}); });
it("does nothing if the previous command exited unsuccessfully", () => { it("does nothing if the previous command exited unsuccessfully with a negative code", () => {
environment.set("status", "-1"); environment.set("status", "-1");
expect(execute("and echo 'message'")).to.equal(-1); expect(execute("and echo 'message'")).to.equal(-1);
expect(readOut()).to.equal(""); expect(readOut()).to.equal("");
}); });
it("executes the command if the previous command exited successfully", () => { it("does nothing if the previous command exited unsuccessfully with a positive code", () => {
environment.set("status", "0"); environment.set("status", "1");
expect(execute("and echo 'message'")).to.equal(0); expect(execute("and echo 'message'")).to.equal(1);
expect(readOut()).to.equal("");
});
it("executes the command if the previous command exited successfully", () => {
environment.set("status", "" + ExitCode.OK);
// TODO (#129) Run `false` to check that the exit code changes
expect(execute("and echo 'message'")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("message\n"); expect(readOut()).to.equal("message\n");
}); });
}); });
@ -162,35 +170,35 @@ describe("commands", () => {
it("fails if the file does not exist", () => { it("fails if the file does not exist", () => {
expect(execute("cat /file")).to.equal(-1); expect(execute("cat /file")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal(`cat: '/file': No such file.\n`); expect(readErr()).to.equal(`cat: '/file': No such file.\n`);
}); });
it("fails if the target is not a file", () => { it("fails if the target is not a file", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("cat /dir")).to.equal(-1); expect(execute("cat /dir")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal(`cat: '/dir': No such file.\n`); expect(readErr()).to.equal(`cat: '/dir': No such file.\n`);
}); });
it("writes the contents of the file to the output stream", () => { it("writes the contents of the file to the output stream", () => {
fileSystem.add(new Path("/file"), new File("contents"), false); fileSystem.add(new Path("/file"), new File("contents"), false);
expect(execute("cat /file")).to.equal(0); expect(execute("cat /file")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("contents\n"); expect(readOut()).to.equal("contents\n");
}); });
it("does not add an extra newline at the end if there already is one", () => { it("does not add an extra newline at the end if there already is one", () => {
fileSystem.add(new Path("/file"), new File("contents\n"), false); fileSystem.add(new Path("/file"), new File("contents\n"), false);
expect(execute("cat /file")).to.equal(0); expect(execute("cat /file")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("contents\n"); expect(readOut()).to.equal("contents\n");
}); });
it("escapes HTML if prompted to do so", () => { it("escapes HTML if prompted to do so", () => {
fileSystem.add(new Path("/file"), new File("<i>contents</i>"), false); fileSystem.add(new Path("/file"), new File("<i>contents</i>"), false);
expect(execute("cat -e /file")).to.equal(0); expect(execute("cat -e /file")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("&lt;i&gt;contents&lt;/i&gt;\n"); expect(readOut()).to.equal("&lt;i&gt;contents&lt;/i&gt;\n");
}); });
@ -198,7 +206,7 @@ describe("commands", () => {
fileSystem.add(new Path("/file1"), new File("contents1"), false); fileSystem.add(new Path("/file1"), new File("contents1"), false);
fileSystem.add(new Path("/file2"), new File("contents2"), false); fileSystem.add(new Path("/file2"), new File("contents2"), false);
expect(execute("cat /file1 /file2")).to.equal(0); expect(execute("cat /file1 /file2")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("contents1\ncontents2\n"); expect(readOut()).to.equal("contents1\ncontents2\n");
}); });
}); });
@ -210,26 +218,26 @@ describe("commands", () => {
it("changes the directory to the home directory if no directory is given", () => { it("changes the directory to the home directory if no directory is given", () => {
environment.set("home", "/home"); environment.set("home", "/home");
expect(execute("cd")).to.equal(0); expect(execute("cd")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/home"); expect(environment.get("cwd")).to.equal("/home");
}); });
it("fails if the target directory does not exist", () => { it("fails if the target directory does not exist", () => {
expect(execute("cd target")).to.equal(-1); expect(execute("cd target")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal("cd: The directory '/target' does not exist.\n"); expect(readErr()).to.equal("cd: The directory '/target' does not exist.\n");
}); });
it("fails if the target is a file", () => { it("fails if the target is a file", () => {
fileSystem.add(new Path("/target"), new File(), false); fileSystem.add(new Path("/target"), new File(), false);
expect(execute("cd target")).to.equal(-1); expect(execute("cd target")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("cd: The directory '/target' does not exist.\n"); expect(readErr()).to.equal("cd: '/target' is not a directory.\n");
}); });
it("changes the directory to the given target", () => { it("changes the directory to the given target", () => {
fileSystem.add(new Path("/target"), new Directory(), false); fileSystem.add(new Path("/target"), new Directory(), false);
expect(execute("cd target")).to.equal(0); expect(execute("cd target")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/target"); expect(environment.get("cwd")).to.equal("/target");
}); });
}); });
@ -241,7 +249,7 @@ describe("commands", () => {
it("copies the source file to the exact target if it does not exist already", () => { it("copies the source file to the exact target if it does not exist already", () => {
fileSystem.add(new Path("/src"), new File("contents"), false); fileSystem.add(new Path("/src"), new File("contents"), false);
expect(execute("cp /src /dst")).to.equal(0); expect(execute("cp /src /dst")).to.equal(ExitCode.OK);
expect(fileSystem.open(new Path("/src"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/src"), "read").read()).to.equal("contents");
expect(fileSystem.open(new Path("/dst"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/dst"), "read").read()).to.equal("contents");
}); });
@ -249,14 +257,14 @@ describe("commands", () => {
it("fails if the source is a directory and the recursive option is not given", () => { it("fails if the source is a directory and the recursive option is not given", () => {
fileSystem.add(new Path("/src"), new Directory(), true); fileSystem.add(new Path("/src"), new Directory(), true);
expect(execute("cp /src /dst")).to.equal(-1); expect(execute("cp /src /dst")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("cp: '/src' is a directory.\n"); expect(readErr()).to.equal("cp: '/src' is a directory.\n");
}); });
it("copies the source directory to the exact target if it does not exist already", () => { it("copies the source directory to the exact target if it does not exist already", () => {
fileSystem.add(new Path("/src/file"), new File("contents"), true); fileSystem.add(new Path("/src/file"), new File("contents"), true);
expect(execute("cp -r /src /dst")).to.equal(0); expect(execute("cp -r /src /dst")).to.equal(ExitCode.OK);
expect(fileSystem.open(new Path("/src/file"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/src/file"), "read").read()).to.equal("contents");
expect(fileSystem.open(new Path("/dst/file"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/dst/file"), "read").read()).to.equal("contents");
}); });
@ -265,7 +273,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src1"), new File("contents1"), false); fileSystem.add(new Path("/src1"), new File("contents1"), false);
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
expect(execute("cp /src1 /src2 /dst")).to.equal(-1); expect(execute("cp /src1 /src2 /dst")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("cp: '/dst' is not a directory.\n"); expect(readErr()).to.equal("cp: '/dst' is not a directory.\n");
}); });
@ -274,7 +282,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
fileSystem.add(new Path("/dst"), new File(), false); fileSystem.add(new Path("/dst"), new File(), false);
expect(execute("cp /src1 /src2 /dst")).to.equal(-1); expect(execute("cp /src1 /src2 /dst")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("cp: '/dst' is not a directory.\n"); expect(readErr()).to.equal("cp: '/dst' is not a directory.\n");
}); });
@ -283,7 +291,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
fileSystem.add(new Path("/dst"), new Directory(), false); fileSystem.add(new Path("/dst"), new Directory(), false);
expect(execute("cp /src1 /src2 /dst")).to.equal(0); expect(execute("cp /src1 /src2 /dst")).to.equal(ExitCode.OK);
expect(fileSystem.open(new Path("/src1"), "read").read()).to.equal("contents1"); expect(fileSystem.open(new Path("/src1"), "read").read()).to.equal("contents1");
expect(fileSystem.open(new Path("/src2"), "read").read()).to.equal("contents2"); expect(fileSystem.open(new Path("/src2"), "read").read()).to.equal("contents2");
expect(fileSystem.open(new Path("/dst/src1"), "read").read()).to.equal("contents1"); expect(fileSystem.open(new Path("/dst/src1"), "read").read()).to.equal("contents1");
@ -296,12 +304,12 @@ describe("commands", () => {
it("adds a newline to the end by default", () => { it("adds a newline to the end by default", () => {
expect(execute("echo a b c \n")).to.equal(0); expect(execute("echo a b c \n")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("a b c \n\n"); expect(readOut()).to.equal("a b c \n\n");
}); });
it("does not add a newline if prompted to do so", () => { it("does not add a newline if prompted to do so", () => {
expect(execute("echo -n a b c")).to.equal(0); expect(execute("echo -n a b c")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("a b c"); expect(readOut()).to.equal("a b c");
}); });
}); });
@ -311,7 +319,7 @@ describe("commands", () => {
it("changes the current user", () => { it("changes the current user", () => {
expect(execute("exit")).to.equal(0); expect(execute("exit")).to.equal(ExitCode.OK);
expect(environment.get("user")).to.equal(""); expect(environment.get("user")).to.equal("");
}); });
}); });
@ -321,7 +329,7 @@ describe("commands", () => {
it("outputs something", () => { it("outputs something", () => {
expect(execute("help help")).to.equal(0); expect(execute("help help")).to.equal(ExitCode.OK);
expect(readOut()).to.not.equal(""); expect(readOut()).to.not.equal("");
}); });
}); });
@ -334,7 +342,7 @@ describe("commands", () => {
it("outputs something", () => { it("outputs something", () => {
expect(execute("help hier")).to.equal(0); expect(execute("help hier")).to.equal(ExitCode.OK);
expect(readOut()).to.not.equal(""); expect(readOut()).to.not.equal("");
}); });
}); });
@ -344,21 +352,21 @@ describe("commands", () => {
it("fails if the target does not exist", () => { it("fails if the target does not exist", () => {
expect(execute("ls dir")).to.equal(-1); expect(execute("ls dir")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal("ls: The directory '/dir' does not exist.\n"); expect(readErr()).to.equal("ls: The directory '/dir' does not exist.\n");
}); });
it("fails if the target is a file", () => { it("fails if the target is a file", () => {
fileSystem.add(new Path("file"), new File(), true); fileSystem.add(new Path("file"), new File(), true);
expect(execute("ls file")).to.equal(-1); expect(execute("ls file")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("ls: '/file' is not a directory.\n"); expect(readErr()).to.equal("ls: '/file' is not a directory.\n");
}); });
it("outputs something otherwise", () => { it("outputs something otherwise", () => {
fileSystem.add(new Path("dir"), new Directory(), true); fileSystem.add(new Path("dir"), new Directory(), true);
expect(execute("ls dir")).to.equal(0); expect(execute("ls dir")).to.equal(ExitCode.OK);
expect(readOut()).to.not.equal(""); expect(readOut()).to.not.equal("");
}); });
}); });
@ -370,22 +378,22 @@ describe("commands", () => {
it("fails if the given directory exists", () => { it("fails if the given directory exists", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("mkdir /dir")).to.equal(-1); expect(execute("mkdir /dir")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("mkdir: A file or directory already exists at '/dir'.\n"); expect(readErr()).to.equal("mkdir: A file or directory already exists at '/dir'.\n");
}); });
it("fails if the parent does not exist", () => { it("fails if the parent does not exist", () => {
expect(execute("mkdir /parent/dir")).to.equal(-1); expect(execute("mkdir /parent/dir")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("mkdir: The directory '/parent' does not exist.\n"); expect(readErr()).to.equal("mkdir: The directory '/parent' does not exist.\n");
}); });
it("creates the parents if opted to do so", () => { it("creates the parents if opted to do so", () => {
expect(execute("mkdir -p /parent/dir")).to.equal(0); expect(execute("mkdir -p /parent/dir")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/parent/dir"))).to.be.true; expect(fileSystem.has(new Path("/parent/dir"))).to.be.true;
}); });
it("creates the given directories in order", () => { it("creates the given directories in order", () => {
expect(execute("mkdir /parent /parent/dir1 /dir2")).to.equal(0); expect(execute("mkdir /parent /parent/dir1 /dir2")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/parent/dir1"))).to.be.true; expect(fileSystem.has(new Path("/parent/dir1"))).to.be.true;
expect(fileSystem.has(new Path("/dir2"))).to.be.true; expect(fileSystem.has(new Path("/dir2"))).to.be.true;
}); });
@ -398,7 +406,7 @@ describe("commands", () => {
it("moves the source file to the exact target if it does not exist already", () => { it("moves the source file to the exact target if it does not exist already", () => {
fileSystem.add(new Path("/src"), new File("contents"), false); fileSystem.add(new Path("/src"), new File("contents"), false);
expect(execute("mv /src /dst")).to.equal(0); expect(execute("mv /src /dst")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/src"))).to.be.false; expect(fileSystem.has(new Path("/src"))).to.be.false;
expect(fileSystem.open(new Path("/dst"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/dst"), "read").read()).to.equal("contents");
}); });
@ -406,7 +414,7 @@ describe("commands", () => {
it("moves the source directory to the exact target if it does not exist already", () => { it("moves the source directory to the exact target if it does not exist already", () => {
fileSystem.add(new Path("/src/file"), new File("contents"), true); fileSystem.add(new Path("/src/file"), new File("contents"), true);
expect(execute("mv -r /src /dst")).to.equal(0); expect(execute("mv -r /src /dst")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/src"))).to.be.false; expect(fileSystem.has(new Path("/src"))).to.be.false;
expect(fileSystem.has(new Path("/src/file"))).to.be.false; expect(fileSystem.has(new Path("/src/file"))).to.be.false;
expect(fileSystem.open(new Path("/dst/file"), "read").read()).to.equal("contents"); expect(fileSystem.open(new Path("/dst/file"), "read").read()).to.equal("contents");
@ -416,7 +424,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src1"), new File("contents1"), false); fileSystem.add(new Path("/src1"), new File("contents1"), false);
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
expect(execute("mv /src1 /src2 /dst")).to.equal(-1); expect(execute("mv /src1 /src2 /dst")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("mv: '/dst' is not a directory.\n"); expect(readErr()).to.equal("mv: '/dst' is not a directory.\n");
}); });
@ -425,7 +433,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
fileSystem.add(new Path("/dst"), new File(), false); fileSystem.add(new Path("/dst"), new File(), false);
expect(execute("mv /src1 /src2 /dst")).to.equal(-1); expect(execute("mv /src1 /src2 /dst")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("mv: '/dst' is not a directory.\n"); expect(readErr()).to.equal("mv: '/dst' is not a directory.\n");
}); });
@ -434,7 +442,7 @@ describe("commands", () => {
fileSystem.add(new Path("/src2"), new File("contents2"), false); fileSystem.add(new Path("/src2"), new File("contents2"), false);
fileSystem.add(new Path("/dst"), new Directory(), false); fileSystem.add(new Path("/dst"), new Directory(), false);
expect(execute("mv /src1 /src2 /dst")).to.equal(0); expect(execute("mv /src1 /src2 /dst")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/src1"))).to.be.false; expect(fileSystem.has(new Path("/src1"))).to.be.false;
expect(fileSystem.has(new Path("/src2"))).to.be.false; expect(fileSystem.has(new Path("/src2"))).to.be.false;
expect(fileSystem.open(new Path("/dst/src1"), "read").read()).to.equal("contents1"); expect(fileSystem.open(new Path("/dst/src1"), "read").read()).to.equal("contents1");
@ -450,7 +458,7 @@ describe("commands", () => {
it("returns 0 if the given command exits unsuccessfully", () => { it("returns 0 if the given command exits unsuccessfully", () => {
expect(execute("not rm /file")).to.equal(0); expect(execute("not rm /file")).to.equal(ExitCode.OK);
}); });
it("returns 1 if the command exits successfully", () => { it("returns 1 if the command exits successfully", () => {
@ -471,7 +479,7 @@ describe("commands", () => {
it("fails if the file does not exist", () => { it("fails if the file does not exist", () => {
expect(execute("open file.lnk")).to.equal(-1); expect(execute("open file.lnk")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("open: File '/file.lnk' does not exist.\n"); expect(readErr()).to.equal("open: File '/file.lnk' does not exist.\n");
}); });
}); });
@ -486,14 +494,14 @@ describe("commands", () => {
it("does nothing if the previous command exited successfully", () => { it("does nothing if the previous command exited successfully", () => {
environment.set("status", "0"); environment.set("status", "0");
expect(execute("or echo 'message'")).to.equal(0); expect(execute("or echo 'message'")).to.equal(ExitCode.OK);
expect(readOut()).to.equal(""); expect(readOut()).to.equal("");
}); });
it("executes the command if the previous command did not exit successfully", () => { it("executes the command if the previous command did not exit successfully", () => {
environment.set("status", "-1"); environment.set("status", "-1");
expect(execute("or echo 'message'")).to.equal(0); expect(execute("or echo 'message'")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("message\n"); expect(readOut()).to.equal("message\n");
}); });
}); });
@ -505,14 +513,14 @@ describe("commands", () => {
it("fails if no user is logged in", () => { it("fails if no user is logged in", () => {
environment.set("user", ""); environment.set("user", "");
expect(execute("poweroff")).to.equal(-1); expect(execute("poweroff")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("poweroff: Cannot execute while not logged in.\n"); expect(readErr()).to.equal("poweroff: Cannot execute while not logged in.\n");
}); });
it("it outputs something", () => { it("it outputs something", () => {
environment.set("user", "user"); environment.set("user", "user");
expect(execute("poweroff")).to.equal(0); expect(execute("poweroff")).to.equal(ExitCode.OK);
expect(readOut().trim()).to.not.equal(""); expect(readOut().trim()).to.not.equal("");
}); });
}); });
@ -524,14 +532,14 @@ describe("commands", () => {
it("writes the cwd variable to the output stream", () => { it("writes the cwd variable to the output stream", () => {
environment.set("cwd", "/dir"); environment.set("cwd", "/dir");
expect(execute("pwd")).to.equal(0); expect(execute("pwd")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("/dir\n"); expect(readOut()).to.equal("/dir\n");
}); });
it("writes an empty string if the cwd variable has no value", () => { it("writes an empty string if the cwd variable has no value", () => {
environment.set("cwd", ""); environment.set("cwd", "");
expect(execute("pwd")).to.equal(0); expect(execute("pwd")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("\n"); expect(readOut()).to.equal("\n");
}); });
}); });
@ -541,45 +549,45 @@ describe("commands", () => {
it("fails if the target does not exist", () => { it("fails if the target does not exist", () => {
expect(execute("rm /file")).to.equal(-1); expect(execute("rm /file")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal("rm: The file '/file' does not exist.\n"); expect(readErr()).to.equal("rm: The file '/file' does not exist.\n");
}); });
it("does nothing if the target does not exist but the force option is given", () => { it("does nothing if the target does not exist but the force option is given", () => {
expect(execute("rm -f /file")).to.equal(0); expect(execute("rm -f /file")).to.equal(ExitCode.OK);
expect(readErr()).to.equal(""); expect(readErr()).to.equal("");
}); });
it("fails if the target is a directory", () => { it("fails if the target is a directory", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("rm /dir")).to.equal(-1); expect(execute("rm /dir")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("rm: '/dir' is a directory.\n"); expect(readErr()).to.equal("rm: '/dir' is a directory.\n");
}); });
it("removes an empty directory if the recursive option is given", () => { it("removes an empty directory if the recursive option is given", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("rm -r /dir")).to.equal(0); expect(execute("rm -r /dir")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/dir"))).to.be.false; expect(fileSystem.has(new Path("/dir"))).to.be.false;
}); });
it("removes a non-empty directory if the recursive option is given", () => { it("removes a non-empty directory if the recursive option is given", () => {
fileSystem.add(new Path("/dir/file"), new File(), true); fileSystem.add(new Path("/dir/file"), new File(), true);
expect(execute("rm -r /dir")).to.equal(0); expect(execute("rm -r /dir")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/dir"))).to.be.false; expect(fileSystem.has(new Path("/dir"))).to.be.false;
}); });
it("fails if the target is the root even though the recursive option is given", () => { it("fails if the target is the root even though the recursive option is given", () => {
expect(execute("rm -r /")).to.equal(-1); expect(execute("rm -r /")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("rm: Cannot remove root directory.\n"); expect(readErr()).to.equal("rm: Cannot remove root directory.\n");
}); });
it("removes the root if the recursive and no-preserve-root options are given", () => { it("removes the root if the recursive and no-preserve-root options are given", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("rm -r --no-preserve-root /")).to.equal(0); expect(execute("rm -r --no-preserve-root /")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/dir"))).to.be.false; expect(fileSystem.has(new Path("/dir"))).to.be.false;
expect(fileSystem.has(new Path("/"))).to.be.true; expect(fileSystem.has(new Path("/"))).to.be.true;
}); });
@ -587,15 +595,15 @@ describe("commands", () => {
it("removes the given file", () => { it("removes the given file", () => {
fileSystem.add(new Path("/file"), new File(), false); fileSystem.add(new Path("/file"), new File(), false);
expect(execute("rm /file")).to.equal(0); expect(execute("rm /file")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/file"))).to.be.false; expect(fileSystem.has(new Path("/file"))).to.be.false;
}); });
it("removes files but not directories of the recursive option is not given", () => { it("removes files but not directories if the recursive option is not given", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
fileSystem.add(new Path("/file"), new File(), false); fileSystem.add(new Path("/file"), new File(), false);
expect(execute("rm /dir /file")).to.equal(-1); expect(execute("rm /dir /file")).to.equal(ExitCode.USAGE);
expect(fileSystem.has(new Path("/dir"))).to.be.true; expect(fileSystem.has(new Path("/dir"))).to.be.true;
expect(fileSystem.has(new Path("/file"))).to.be.false; expect(fileSystem.has(new Path("/file"))).to.be.false;
}); });
@ -606,35 +614,35 @@ describe("commands", () => {
it("fails if the target does not exist", () => { it("fails if the target does not exist", () => {
expect(execute("rmdir /dir")).to.equal(-1); expect(execute("rmdir /dir")).to.equal(ExitCode.FILE_NOT_FOUND);
expect(readErr()).to.equal("rmdir: '/dir' does not exist.\n"); expect(readErr()).to.equal("rmdir: '/dir' does not exist.\n");
}); });
it("fails if the target is not a directory", () => { it("fails if the target is not a directory", () => {
fileSystem.add(new Path("/file"), new File(), false); fileSystem.add(new Path("/file"), new File(), false);
expect(execute("rmdir /file")).to.equal(-1); expect(execute("rmdir /file")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("rmdir: '/file' is not a directory.\n"); expect(readErr()).to.equal("rmdir: '/file' is not a directory.\n");
}); });
it("fails if the target is not empty", () => { it("fails if the target is not empty", () => {
fileSystem.add(new Path("/dir/file"), new File(), true); fileSystem.add(new Path("/dir/file"), new File(), true);
expect(execute("rmdir /dir")).to.equal(-1); expect(execute("rmdir /dir")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("rmdir: '/dir' is not empty.\n"); expect(readErr()).to.equal("rmdir: '/dir' is not empty.\n");
}); });
it("removes the target if it is an empty directory", () => { it("removes the target if it is an empty directory", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("rmdir /dir")).to.equal(0); expect(execute("rmdir /dir")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/dir"))).to.be.false; expect(fileSystem.has(new Path("/dir"))).to.be.false;
}); });
it("removes the targets if they're empty directories", () => { it("removes the targets if they're empty directories", () => {
fileSystem.add(new Path("/dir1/dir2"), new Directory(), true); fileSystem.add(new Path("/dir1/dir2"), new Directory(), true);
expect(execute("rmdir /dir1/dir2 /dir1")).to.equal(0); expect(execute("rmdir /dir1/dir2 /dir1")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/dir1"))).to.be.false; expect(fileSystem.has(new Path("/dir1"))).to.be.false;
}); });
}); });
@ -644,28 +652,28 @@ describe("commands", () => {
it("creates a variable if it does not exist", () => { it("creates a variable if it does not exist", () => {
expect(execute("set var val")).to.equal(0); expect(execute("set var val")).to.equal(ExitCode.OK);
expect(environment.variables["var"]).to.equal("val"); expect(environment.variables["var"]).to.equal("val");
}); });
it("changes a variable if it exists", () => { it("changes a variable if it exists", () => {
environment.set("var", "old"); environment.set("var", "old");
expect(execute("set var new")).to.equal(0); expect(execute("set var new")).to.equal(ExitCode.OK);
expect(environment.variables["var"]).to.equal("new"); expect(environment.variables["var"]).to.equal("new");
}); });
it("removes the variable if no value is given", () => { it("removes the variable if no value is given", () => {
environment.set("var", "val"); environment.set("var", "val");
expect(execute("set var")).to.equal(0); expect(execute("set var")).to.equal(ExitCode.OK);
expect(environment.variables["var"]).to.be.undefined; expect(environment.variables["var"]).to.be.undefined;
}); });
it("cannot change a read-only variable", () => { it("cannot change a read-only variable", () => {
environment.set("cwd", "old"); environment.set("cwd", "old");
expect(execute("set cwd new")).to.equal(-1); expect(execute("set cwd new")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("set: Cannot set read-only environment variable.\n"); expect(readErr()).to.equal("set: Cannot set read-only environment variable.\n");
expect(environment.variables["cwd"]).to.equal("old"); expect(environment.variables["cwd"]).to.equal("old");
}); });
@ -673,7 +681,7 @@ describe("commands", () => {
it("cannot remove a read-only variable", () => { it("cannot remove a read-only variable", () => {
environment.set("cwd", "old"); environment.set("cwd", "old");
expect(execute("set cwd")).to.equal(-1); expect(execute("set cwd")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("set: Cannot set read-only environment variable.\n"); expect(readErr()).to.equal("set: Cannot set read-only environment variable.\n");
expect(environment.variables["cwd"]).to.equal("old"); expect(environment.variables["cwd"]).to.equal("old");
}); });
@ -686,29 +694,29 @@ describe("commands", () => {
it("fails if a directory already exists at the target", () => { it("fails if a directory already exists at the target", () => {
fileSystem.add(new Path("/dir"), new Directory(), false); fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("touch /dir")).to.equal(-1); expect(execute("touch /dir")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("touch: A file or directory already exists at '/dir'.\n"); expect(readErr()).to.equal("touch: A file or directory already exists at '/dir'.\n");
}); });
it("fails if the parent of the target does not exist", () => { it("fails if the parent of the target does not exist", () => {
expect(execute("touch /parent/file")).to.equal(-1); expect(execute("touch /parent/file")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("touch: The directory '/parent' does not exist.\n"); expect(readErr()).to.equal("touch: The directory '/parent' does not exist.\n");
}); });
it("fails if the parent of the target is a file", () => { it("fails if the parent of the target is a file", () => {
fileSystem.add(new Path("/parent"), new File(), false); fileSystem.add(new Path("/parent"), new File(), false);
expect(execute("touch /parent/file")).to.equal(-1); expect(execute("touch /parent/file")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("touch: '/parent' is not a directory.\n"); expect(readErr()).to.equal("touch: '/parent' is not a directory.\n");
}); });
it("creates a file at the target", () => { it("creates a file at the target", () => {
expect(execute("touch /file")).to.equal(0); expect(execute("touch /file")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/file"))).to.be.true; expect(fileSystem.has(new Path("/file"))).to.be.true;
}); });
it("creates files at the target", () => { it("creates files at the target", () => {
expect(execute("touch /file1 /file2")).to.equal(0); expect(execute("touch /file1 /file2")).to.equal(ExitCode.OK);
expect(fileSystem.has(new Path("/file1"))).to.be.true; expect(fileSystem.has(new Path("/file1"))).to.be.true;
expect(fileSystem.has(new Path("/file2"))).to.be.true; expect(fileSystem.has(new Path("/file2"))).to.be.true;
}); });
@ -725,32 +733,32 @@ describe("commands", () => {
it("fails if the user already exists", () => { it("fails if the user already exists", () => {
userList.add(new User("user", "old")); userList.add(new User("user", "old"));
expect(execute("useradd user new")).to.equal(-1); expect(execute("useradd user new")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("useradd: User 'user' already exists.\n"); expect(readErr()).to.equal("useradd: User 'user' already exists.\n");
}); });
it("fails if any of the fields is malformed", () => { it("fails if any of the fields is malformed", () => {
expect(execute("useradd user_name password")).to.equal(-1); expect(execute("useradd user_name password")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("useradd: Name must contain only alphanumerical characters.\n"); expect(readErr()).to.equal("useradd: Name must contain only alphanumerical characters.\n");
}); });
it("adds a user with default home and empty description if those fields are not given", () => { it("adds a user with default home and empty description if those fields are not given", () => {
expect(execute("useradd user password")).to.equal(0); expect(execute("useradd user password")).to.equal(ExitCode.OK);
expect(userList.get("user")).to.deep.equal(new User("user", "password")); expect(userList.get("user")).to.deep.equal(new User("user", "password"));
}); });
it("adds a new user with default home if that option is not given", () => { it("adds a new user with default home if that option is not given", () => {
expect(execute("useradd --description=description user password")).to.equal(0); expect(execute("useradd --description=description user password")).to.equal(ExitCode.OK);
expect(userList.get("user")).to.deep.equal(new User("user", "password", undefined, "description")); expect(userList.get("user")).to.deep.equal(new User("user", "password", undefined, "description"));
}); });
it("adds a user with default description if that option is not given", () => { it("adds a user with default description if that option is not given", () => {
expect(execute("useradd --home=/user user password")).to.equal(0); expect(execute("useradd --home=/user user password")).to.equal(ExitCode.OK);
expect(userList.get("user")).to.deep.equal(new User("user", "password", "/user")); expect(userList.get("user")).to.deep.equal(new User("user", "password", "/user"));
}); });
it("adds a user with the given home and description", () => { it("adds a user with the given home and description", () => {
expect(execute("useradd --home=/user --description=description user password")).to.equal(0); expect(execute("useradd --home=/user --description=description user password")).to.equal(ExitCode.OK);
expect(userList.get("user")).to.deep.equal(new User("user", "password", "/user", "description")); expect(userList.get("user")).to.deep.equal(new User("user", "password", "/user", "description"));
}); });
}); });
@ -760,14 +768,14 @@ describe("commands", () => {
it("fails if the target user does not exist", () => { it("fails if the target user does not exist", () => {
expect(execute("userdel user")).to.equal(-1); expect(execute("userdel user")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("userdel: Could not delete non-existent user 'user'.\n"); expect(readErr()).to.equal("userdel: Could not delete non-existent user 'user'.\n");
}); });
it("deletes the given user", () => { it("deletes the given user", () => {
userList.add(new User("user", "password")); userList.add(new User("user", "password"));
expect(execute("userdel user")).to.equal(0); expect(execute("userdel user")).to.equal(ExitCode.OK);
expect(userList.has("user")).to.be.false; expect(userList.has("user")).to.be.false;
}); });
}); });
@ -781,7 +789,7 @@ describe("commands", () => {
it("fails if the target user does not exist", () => { it("fails if the target user does not exist", () => {
expect(execute("usermod user")).to.equal(-1); expect(execute("usermod user")).to.equal(ExitCode.USAGE);
expect(readErr()).to.equal("usermod: Could not modify non-existent user 'user'.\n"); expect(readErr()).to.equal("usermod: Could not modify non-existent user 'user'.\n");
}); });
@ -789,7 +797,7 @@ describe("commands", () => {
const user = new User("user", "password", "/home"); const user = new User("user", "password", "/home");
userList.add(user); userList.add(user);
expect(execute("usermod -h=/ho|me user")).to.equal(-1); expect(execute("usermod -h=/ho|me user")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("usermod: Home must not contain pipe ('|') or newline character.\n"); expect(readErr()).to.equal("usermod: Home must not contain pipe ('|') or newline character.\n");
}); });
@ -797,7 +805,7 @@ describe("commands", () => {
const user = new User("user", "password", "/home", "description"); const user = new User("user", "password", "/home", "description");
userList.add(user); userList.add(user);
expect(execute("usermod user")).to.equal(0); expect(execute("usermod user")).to.equal(ExitCode.OK);
expect(userList.get("user")).to.deep.equal(user); expect(userList.get("user")).to.deep.equal(user);
}); });
@ -805,7 +813,7 @@ describe("commands", () => {
const user = new User("user", "old"); const user = new User("user", "old");
userList.add(user); userList.add(user);
expect(execute("usermod -p=new user")).to.equal(0); expect(execute("usermod -p=new user")).to.equal(ExitCode.OK);
expect(userList.get("user")?.passwordHash).to.equal("new"); expect(userList.get("user")?.passwordHash).to.equal("new");
}); });
@ -813,7 +821,7 @@ describe("commands", () => {
const user = new User("user", "pwd", "/old"); const user = new User("user", "pwd", "/old");
userList.add(user); userList.add(user);
expect(execute("usermod -h=/new user")).to.equal(0); expect(execute("usermod -h=/new user")).to.equal(ExitCode.OK);
expect(userList.get("user")?.home).to.equal("/new"); expect(userList.get("user")?.home).to.equal("/new");
}); });
@ -821,7 +829,7 @@ describe("commands", () => {
const user = new User("user", "password", undefined, "old"); const user = new User("user", "password", undefined, "old");
userList.add(user); userList.add(user);
expect(execute("usermod -d=new user")).to.equal(0); expect(execute("usermod -d=new user")).to.equal(ExitCode.OK);
expect(userList.get("user")?.description).to.equal("new"); expect(userList.get("user")?.description).to.equal("new");
}); });
}); });
@ -833,7 +841,7 @@ describe("commands", () => {
it("fails if no user is logged in", () => { it("fails if no user is logged in", () => {
environment.set("user", ""); environment.set("user", "");
expect(execute("whoami")).to.equal(-1); expect(execute("whoami")).to.equal(ExitCode.MISC);
expect(readErr()).to.equal("whoami: Cannot execute while not logged in.\n"); expect(readErr()).to.equal("whoami: Cannot execute while not logged in.\n");
}); });
@ -841,7 +849,7 @@ describe("commands", () => {
userList.add(new User("user", "pwd", "/", "Description")); userList.add(new User("user", "pwd", "/", "Description"));
environment.set("user", "user"); environment.set("user", "user");
expect(execute("whoami")).to.equal(0); expect(execute("whoami")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("Description\n"); expect(readOut()).to.equal("Description\n");
}); });
}); });