From 8dfcc7f6af67c6b61d94c85a3831f32d51781036 Mon Sep 17 00:00:00 2001 From: "Felix W. Dekker" Date: Wed, 25 Mar 2020 12:57:39 +0100 Subject: [PATCH] Fix indenting in binaries Fixes #122. --- package.json | 2 +- src/main/js/Commands.ts | 1314 ++++++++++++++++++++------------------- 2 files changed, 669 insertions(+), 647 deletions(-) diff --git a/package.json b/package.json index 77da663..cee579c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fwdekker.com", - "version": "0.33.4", + "version": "0.33.5", "description": "The source code of [my personal website](https://fwdekker.com/).", "author": "Felix W. Dekker", "browser": "bundle.js", diff --git a/src/main/js/Commands.ts b/src/main/js/Commands.ts index 846a23d..9c58698 100644 --- a/src/main/js/Commands.ts +++ b/src/main/js/Commands.ts @@ -295,695 +295,717 @@ const n = "\\\\\\"; * @return the script contents of the binaries in the `/bin` directory */ export const commandBinaries: { [key: string]: string } = { - "and": `return new Command( - (input, streams) => { - const previousStatus = Number(josh.environment.getOrDefault("status", "0")); - if (previousStatus !== 0) - return previousStatus; + "and": `\ +return new Command( + (input, streams) => { + const previousStatus = Number(josh.environment.getOrDefault("status", "0")); + if (previousStatus !== 0) + return previousStatus; - return josh.interpreter.execute( - InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), - streams - ); - }, - \`execute command if previous command did not fail\`, - \`and command\`, - \`Executes command with its associated options and arguments if and only if the status code of the ${n} - previously-executed command is 0. + return josh.interpreter.execute( + InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), + streams + ); + }, + \`execute command if previous command did not fail\`, + \`and command\`, + \`Executes command with its associated options and arguments if and only if the status code of the ${n} + previously-executed command is 0. - The exit code is retained if it was non-zero, and is changed to that of command otherwise.${n} - \`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "cat": `return new Command( - (input, streams) => { - return input.args - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map(path => { - const node = josh.fileSystem.get(path); - if (!(node instanceof File)) { - streams.err.writeLine(\`cat: '\${path}': No such file.\`); - return -1; - } - - let contents = node.open("read").read(); - if (input.hasAnyOption("-e", "--escape-html")) - contents = josh.util.escapeHtml(contents); - if (!contents.endsWith("\\n")) - contents += "\\n"; - - streams.out.write(contents); - return 0; - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`concatenate and print files\`, - \`cat [-e | --escape-html] file ...\`, - \`Reads files sequentially, writing them to the standard output. - - If the file contains valid HTML, it will be displayed as such by default. If the --escape-html ${n} - option is given, special HTML characters are escaped and the raw text contents can be inspected.${n} - \`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "cd": `return new Command( - (input, streams) => { - if (input.argc === 0) { - josh.environment.set("cwd", josh.environment.get("home")); - return 0; - } - - const path = Path.interpret(josh.environment.get("cwd"), input.args[0]); - if (!(josh.fileSystem.get(path) instanceof Directory)) { - streams.err.writeLine(\`cd: The directory '\${path}' does not exist.\`); - return -1; - } - - josh.environment.set("cwd", path.toString()); - return 0; - }, - \`change directory\`, - \`cd [directory]\`, - \`Changes the current working directory to directory. If no directory is supplied, the ${n} - current working directory is changed to the current user's home directory.\`.trimMultiLines(), - new InputValidator({maxArgs: 1}) - )`, - "clear": `return new Command( - (input, streams) => { - streams.out.write(EscapeCharacters.Escape + EscapeCharacters.Clear); - return 0; - }, - \`clear terminal output\`, - \`clear\`, - \`Clears all previous terminal output.\`, - new InputValidator({maxArgs: 0}) - )`, - "cp": `return new Command( - (input, streams) => { - let mappings; - try { - const cwd = josh.environment.get("cwd"); - mappings = josh.fileSystem.determineMoveMappings( - input.args.slice(0, -1).map((it) => Path.interpret(cwd, it)), - Path.interpret(cwd, input.args.slice(-1)[0]) - ); - } catch (error) { - streams.err.writeLine(\`cp: \${error.message}\`); - return -1; - } - - return mappings - .map(([source, destination]) => { - try { - josh.fileSystem.copy(source, destination, input.hasAnyOption("-r", "-R", "--recursive")); - return 0; - } catch (error) { - streams.err.writeLine(\`cp: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`copy files\`, - \`cp [-r | -R | --recursive] source target file - cp [-r | -R | --recursive] source ... target directory\`, - \`In its first form, source is copied to target file. This form is used if there is no file ${n} - or directory at target file beforehand. - - In its second form, all source files are copied into target directory, which must be a ${n} - pre-existing directory. The file names of the source files are retained. - - In both forms, source files are not copied if they are directories and the -R option is not ${n} - given.\`.trimMultiLines(), - new InputValidator({minArgs: 2}) - )`, - "echo": `return new Command( - (input, streams) => { - const message = input.args.join(" ").replace("hunter2", "*******"); - - if (input.hasAnyOption("-n", "--newline")) - streams.out.write(message); - else - streams.out.writeLine(message); - - return 0; - }, - \`display text\`, - \`echo [-n | --newline] [text ...]\`, - \`Displays each text separated by a single whitespace. - - Unless the --newline parameter is given, a newline is appended to the end.\`.trimMultiLines(), - new InputValidator() - )`, - "exit": `return new Command( - (input, streams) => { - josh.environment.set("user", ""); - return 0; - }, - \`close session\`, - \`exit\`, - \`Closes the terminal session.\`, - new InputValidator({maxArgs: 0}) - )`, - "help": `return new Command( - (input, streams) => { - if (input.argc > 0) { - return input.args - .map((commandName, i) => { - if (i > 0) - streams.out.write("\\n\\n"); - - const command = josh.interpreter.resolve(commandName); - if (command === undefined) { - streams.out.writeLine(\`Unknown command '\${commandName}'.\`); - return -1; - } - - let helpString = "Name\\n" + commandName; - if (command.summary !== null) - helpString += "\\n\\nSummary\\n" + command.summary; - if (command.usage !== null) - helpString += "\\n\\nUsage\\n" + command.usage; - if (command.desc !== null) - helpString += "\\n\\nDescription\\n" + command.desc; - - streams.out.writeLine(helpString); - return 0; - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - } else { - const cwd = josh.environment.get("cwd"); - const slashBin = josh.fileSystem.get(Path.interpret(cwd, "/bin")); - if (!(slashBin instanceof Directory)) { + The exit code is retained if it was non-zero, and is changed to that of command otherwise.${n} + \`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "cat": `\ +return new Command( + (input, streams) => { + return input.args + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map(path => { + const node = josh.fileSystem.get(path); + if (!(node instanceof File)) { + streams.err.writeLine(\`cat: '\${path}': No such file.\`); return -1; } - const commands = {}; - Object.keys(slashBin.nodes).map(it => { - const command = josh.interpreter.resolve(it); - if (command !== undefined) commands[it] = command; - }); - const commandNames = Object.keys(commands).filter(it => !(commands[it] instanceof DocOnlyCommand)); + let contents = node.open("read").read(); + if (input.hasAnyOption("-e", "--escape-html")) + contents = josh.util.escapeHtml(contents); + if (!contents.endsWith("\\n")) + contents += "\\n"; - const commandWidth = Math.max.apply(null, commandNames.map(it => it.length)) + 4; - const commandPaddings = commandNames.map(it => commandWidth - it.length); - const commandLinks = commandNames - .map(it => \`\${it}\`) - .map((it, i) => \`\${it.padEnd(it.length + commandPaddings[i], " ")}\`); - const commandEntries = commandNames - .map((it, i) => \`\${commandLinks[i]}\${commands[it].summary}\`); - - const target = josh.util.isStandalone() ? \`target="_blank"\` : ""; - streams.out.writeLine( - \`The source code of this website is ${n} - available on git. - - List of commands - \${commandEntries.join("\\n")} - - Write "help [COMMAND]" or click a command in the list above for more information.\`.trimMultiLines() - ); + streams.out.write(contents); return 0; - } - }, - \`display documentation\`, - \`help [command ...]\`, - \`Displays help documentation for each command. + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`concatenate and print files\`, + \`cat [-e | --escape-html] file ...\`, + \`Reads files sequentially, writing them to the standard output. - If no commands are given, a list of all commands is shown.\`.trimMultiLines(), - new InputValidator() - )`, - "hier": `return new DocOnlyCommand( - \`description of the file system hierarchy\`, - \`A typical josh system has, among others, the following directories: + If the file contains valid HTML, it will be displayed as such by default. If the --escape-html option is ${n} + given, special HTML characters are escaped and the raw text contents can be inspected.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "cd": `\ +return new Command( + (input, streams) => { + if (input.argc === 0) { + josh.environment.set("cwd", josh.environment.get("home")); + return 0; + } - / This is the root directory. This is where the whole tree starts. + const path = Path.interpret(josh.environment.get("cwd"), input.args[0]); + if (!(josh.fileSystem.get(path) instanceof Directory)) { + streams.err.writeLine(\`cd: The directory '\${path}' does not exist.\`); + return -1; + } - /bin Executable programs fundamental to user environments. + josh.environment.set("cwd", path.toString()); + return 0; + }, + \`change directory\`, + \`cd [directory]\`, + \`Changes the current working directory to directory. If no directory is supplied, the current ${n} + working directory is changed to the current user's home directory.\`.trimMultiLines(), + new InputValidator({maxArgs: 1}) +)`, + "clear": `\ +return new Command( + (input, streams) => { + streams.out.write(EscapeCharacters.Escape + EscapeCharacters.Clear); + return 0; + }, + \`clear terminal output\`, + \`clear\`, + \`Clears all previous terminal output.\`, + new InputValidator({maxArgs: 0}) +)`, + "cp": `\ +return new Command( + (input, streams) => { + let mappings; + try { + const cwd = josh.environment.get("cwd"); + mappings = josh.fileSystem.determineMoveMappings( + input.args.slice(0, -1).map((it) => Path.interpret(cwd, it)), + Path.interpret(cwd, input.args.slice(-1)[0]) + ); + } catch (error) { + streams.err.writeLine(\`cp: \${error.message}\`); + return -1; + } - /dev Contains special files and device files that refer to physical devices. + return mappings + .map(([source, destination]) => { + try { + josh.fileSystem.copy(source, destination, input.hasAnyOption("-r", "-R", "--recursive")); + return 0; + } catch (error) { + streams.err.writeLine(\`cp: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`copy files\`, + \`cp [-r | -R | --recursive] source target file + cp [-r | -R | --recursive] source ... target directory\`, + \`In its first form, source is copied to target file. This form is used if there is no file or ${n} + directory at target file beforehand. - /etc System configuration files and scripts. + In its second form, all source files are copied into target directory, which must be a ${n} + pre-existing directory. The file names of the source files are retained. - /home Contains directories for users to store personal files in. + In both forms, source files are not copied if they are directories and the -R option is not given.${n} + \`.trimMultiLines(), + new InputValidator({minArgs: 2}) +)`, + "echo": `\ +return new Command( + (input, streams) => { + const message = input.args.join(" ").replace("hunter2", "*******"); - /root The home directory of the root user.\`.trimMultiLines() - )`, - "ls": `return new Command( - (input, streams) => { - return (input.argc === 0 ? [""] : input.args) - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map((path, i) => { + if (input.hasAnyOption("-n", "--newline")) + streams.out.write(message); + else + streams.out.writeLine(message); + + return 0; + }, + \`display text\`, + \`echo [-n | --newline] [text ...]\`, + \`Displays each text separated by a single whitespace. + + Unless the --newline parameter is given, a newline is appended to the end.\`.trimMultiLines(), + new InputValidator() +)`, + "exit": `\ +return new Command( + (input, streams) => { + josh.environment.set("user", ""); + return 0; + }, + \`close session\`, + \`exit\`, + \`Closes the terminal session.\`, + new InputValidator({maxArgs: 0}) +)`, + "help": `\ +return new Command( + (input, streams) => { + if (input.argc > 0) { + return input.args + .map((commandName, i) => { if (i > 0) - streams.out.write("\\n"); + streams.out.write("\\n\\n"); - const node = josh.fileSystem.get(path); - if (node === undefined) { - streams.err.writeLine(\`ls: The directory '\${path}' does not exist.\`); - return -1; - } - if (!(node instanceof Directory)) { - streams.err.writeLine(\`ls: '\${path}' is not a directory.\`); + const command = josh.interpreter.resolve(commandName); + if (command === undefined) { + streams.out.writeLine(\`Unknown command '\${commandName}'.\`); return -1; } - const dirList = [ - new Directory().nameString("./", path), - new Directory().nameString("../", path.parent) - ]; - const fileList = []; + let helpString = "Name\\n" + commandName; + if (command.summary !== null) + helpString += "\\n\\nSummary\\n" + command.summary; + if (command.usage !== null) + helpString += "\\n\\nUsage\\n" + command.usage; + if (command.desc !== null) + helpString += "\\n\\nDescription\\n" + command.desc; - const nodes = node.nodes; - Object.keys(nodes) - .sortAlphabetically(it => it, true) - .forEach(name => { - const node = nodes[name]; - if (name.startsWith(".") && !input.hasAnyOption("-a", "-A", "--all")) - 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( - \`ls: '\${path.getChild(name)}' is neither a file nor a directory.\`); - }); - - if (input.argc > 1) - streams.out.writeLine(\`\${path}\`); - streams.out.writeLine(dirList.concat(fileList).join("\\n")); + streams.out.writeLine(helpString); return 0; }) .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`list directory contents\`, - \`ls [-a | -A | --all] [directory ...]\`, - \`Displays the files and directories in each directory. If no directory is given, the files and ${n} - directories in the current working directory are shown. If more than one directory is given, the files and ${n} - directories are shown for each given directory in order. - - Files starting with a . are only shown if the --all option is given, with the exception of ${n} - . and .., which are always shown.\`.trimMultiLines(), - new InputValidator() - )`, - "mkdir": `return new Command( - (input, streams) => { - return input.args - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map(path => { - try { - josh.fileSystem.add(path, new Directory(), input.hasAnyOption("-p", "--parents")); - return 0; - } catch (error) { - streams.err.writeLine(\`mkdir: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`make directories\`, - \`mkdir [-p | --parents] directory ...\`, - \`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 ${n} - the --parents option is given, parent directories that do not exist are created as well.${n} - \`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "mv": `return new Command( - (input, streams) => { - let mappings; - try { - const cwd = josh.environment.get("cwd"); - mappings = josh.fileSystem.determineMoveMappings( - input.args.slice(0, -1).map((it) => Path.interpret(cwd, it)), - Path.interpret(cwd, input.args.slice(-1)[0]) - ); - } catch (error) { - streams.err.writeLine(\`mv: \${error.message}\`); + } else { + const cwd = josh.environment.get("cwd"); + const slashBin = josh.fileSystem.get(Path.interpret(cwd, "/bin")); + if (!(slashBin instanceof Directory)) { return -1; } - return mappings - .map(([source, destination]) => { - try { - josh.fileSystem.move(source, destination); - return 0; - } catch (error) { - streams.err.writeLine(\`mv: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`move files\`, - \`mv source destination file - mv source ... destination directory\`, - \`In its first form, source is renamed to target file. target file must not exist yet. + const commands = {}; + Object.keys(slashBin.nodes).map(it => { + const command = josh.interpreter.resolve(it); + if (command !== undefined) commands[it] = command; + }); + const commandNames = Object.keys(commands).filter(it => !(commands[it] instanceof DocOnlyCommand)); - In its second form, all source files are moved into target directory, which must be a ${n} - pre-existing directory. The file names of the source files are retained.\`.trimMultiLines(), - new InputValidator({minArgs: 2}) - )`, - "not": `return new Command( - (input, streams) => { - return Number(!josh.interpreter.execute( - InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), - streams - )); - }, - \`execute command and invert status code\`, - \`not command\`, - \`Executes command with its associated options and arguments and inverts its exit code. More ${n} - precisely, the exit code is set to 0 if it was non-zero, and is set to 1 otherwise.\`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "open": `return new Command( - (input, streams) => { - return input.args - .map(it => Path.interpret(josh.environment.get("cwd"), it)) - .map((path, i) => { - try { - const target = i > 0 || input.hasAnyOption("-b", "--blank") || josh.util.isStandalone() - ? "_blank" - : "_self"; - window.open(josh.fileSystem.open(path, "read").read(), target); - return 0; - } catch (error) { - streams.err.writeLine(\`open: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`open web pages\`, - \`open [-b | --blank] file ...\`, - \`Opens the web pages linked to by file. The first file is opened in this tab and the ${n} - subsequent files are opened in new tabs. If --blank is set, the first file is opened ${n} - in a new tab as well. - - If this command is executed inside of a standalone app instead of a browser, every file is opened in ${n} - a tab regardless of whether --blank is given.\`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "or": `return new Command( - (input, streams) => { - const previousStatus = Number(josh.environment.getOrDefault("status", "0")); - if (previousStatus === 0) - return previousStatus; - - return josh.interpreter.execute( - InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), - streams - ); - }, - \`execute command if previous command failed\`, - \`or command\`, - \`Executes command with its associated options and arguments if and only if the status code of the ${n} - previously-executed command is not 0. - - The exit code is retained if it was zero, and is changed to that of command otherwise.${n} - \`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "poweroff": `return new Command( - (input, streams) => { - const userName = josh.environment.get("user"); - if (userName === "") { - streams.err.writeLine("poweroff: Cannot execute while not logged in."); - return -1; - } - - Persistence.setPoweroff(true); - setTimeout(() => location.reload(), 2000); + const commandWidth = Math.max.apply(null, commandNames.map(it => it.length)) + 4; + const commandPaddings = commandNames.map(it => commandWidth - it.length); + const commandLinks = commandNames + .map(it => \`\${it}\`) + .map((it, i) => \`\${it.padEnd(it.length + commandPaddings[i], " ")}\`); + const commandEntries = commandNames + .map((it, i) => \`\${commandLinks[i]}\${commands[it].summary}\`); + const target = josh.util.isStandalone() ? \`target="_blank"\` : ""; streams.out.writeLine( - \`Shutdown NOW! + \`The source code of this website is ${n} + available on git. - *** FINAL System shutdown message from \${userName}@fwdekker.com *** + List of commands + \${commandEntries.join("\\n")} - System going down IMMEDIATELY - - - System shutdown time has arrived\`.trimLines() + Write "help [COMMAND]" or click a command in the list above for more information.\`.trimMultiLines() ); return 0; - }, - \`close down the system\`, - \`poweroff\`, - \`Automated shutdown procedure to nicely notify users when the system is shutting down.\`, - new InputValidator({maxArgs: 0}) - )`, - "pwd": `return new Command( - (input, streams) => { - streams.out.writeLine(josh.environment.get("cwd") || ""); - return 0; - }, - \`print working directory\`, - \`pwd\`, - \`Displays the current working directory.\`, - new InputValidator({maxArgs: 0}) - )`, - "rm": `return new Command( - (input, streams) => { - return input.args - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map(path => { - try { - const target = josh.fileSystem.get(path); - if (target === undefined) { - if (input.hasAnyOption("-f", "--force")) - return 0; + } + }, + \`display documentation\`, + \`help [command ...]\`, + \`Displays help documentation for each command. - streams.err.writeLine(\`rm: The file '\${path}' does not exist.\`); - return -1; - } - if (target instanceof Directory) { - if (!input.hasAnyOption("-r", "-R", "--recursive")) { - streams.err.writeLine(\`rm: '\${path}' is a directory.\`); - return -1; - } - if (path.toString() === "/" && !input.hasAnyOption("--no-preserve-root")) { - streams.err.writeLine("rm: Cannot remove root directory."); - return -1; - } - } + If no commands are given, a list of all commands is shown.\`.trimMultiLines(), + new InputValidator() +)`, + "hier": `\ +return new DocOnlyCommand( + \`description of the file system hierarchy\`, + \`A typical josh system has, among others, the following directories: - josh.fileSystem.remove(path); - return 0; - } catch (error) { - streams.err.writeLine(\`rm: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`remove file\`, - \`rm [-f | --force] [-r | -R | --recursive] ${n} - [--no-preserve-root] file ...\`.trimMultiLines(), - \`Removes each given file. If more than one file is given, they are removed in the order they ${n} - are given in. + / This is the root directory. This is where the whole tree starts. - If --force is set, no warning is given if a file could not be removed. + /bin Executable programs fundamental to user environments. - If --recursive is set, files and directories are removed recursively; without this option ${n} - directories cannot be removed. + /dev Contains special files and device files that refer to physical devices. - Unless --no-preserve-root is set, the root directory cannot be removed.\`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "rmdir": `return new Command( - (input, streams) => { - return input.args - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map(path => { - try { - const target = josh.fileSystem.get(path); - if (target === undefined) { - streams.err.writeLine(\`rmdir: '\${path}' does not exist.\`); - return -1; - } - if (!(target instanceof Directory)) { - streams.err.writeLine(\`rmdir: '\${path}' is not a directory.\`); - return -1; - } - if (target.nodeCount !== 0) { - streams.err.writeLine(\`rmdir: '\${path}' is not empty.\`); - return -1; - } + /etc System configuration files and scripts. - josh.fileSystem.remove(path); - return 0; - } catch (error) { - streams.err.writeLine(\`rmdir: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`remove directories\`, - \`rmdir directory ...\`, - \`Removes each given directory. If more than one directory is given, they are removed in the ${n} - order they are given in. Non-empty directories will not be removed.\`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "set": `return new Command( - (input, streams) => { - try { - if (input.argc === 1) - josh.environment.safeDelete(input.args[0]); - else - josh.environment.safeSet(input.args[0], input.args[1]); - } catch (error) { - streams.err.writeLine(\`set: \${error.message}\`); - return -1; - } + /home Contains directories for users to store personal files in. - return 0; - }, - \`set environment variable\`, - \`set key [value]\`, - \`Sets the environment variable key to value. If no value is given, the environment ${n} - variable is cleared. Read-only variables cannot be set.\`.trimMultiLines(), - new InputValidator({minArgs: 1, maxArgs: 2}) - )`, - "touch": `return new Command( - (input, streams) => { - return input.args - .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) - .map(path => { - try { - josh.fileSystem.add(path, new File(), false); - return 0; - } catch (error) { - streams.err.writeLine(\`touch: \${error.message}\`); - return -1; - } - }) - .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); - }, - \`change file timestamps\`, - \`touch file ...\`, - \`Update the access and modification times of each file to the current time. If a file does ${n} - not exist, it is created.\`.trimMultiLines(), - new InputValidator({minArgs: 1}) - )`, - "useradd": `return new Command( - (input, streams) => { - if (josh.userList.has(input.args[0])) { - streams.err.writeLine(\`useradd: User '\${input.args[0]}' already exists.\`); - return -1; - } + /root The home directory of the root user.\`.trimMultiLines() +)`, + "ls": `\ +return new Command( + (input, streams) => { + return (input.argc === 0 ? [""] : input.args) + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map((path, i) => { + if (i > 0) + streams.out.write("\\n"); - let user; - try { - user = new User(input.args[0], HashProvider.default.hashPassword(input.args[1])); - if (input.hasAnyOption("-h", "--home")) - user.home = input.options["-h"] || input.options["--home"]; - if (input.hasAnyOption("-d", "--description")) - user.description = input.options["-d"] || input.options["--description"]; - } catch (error) { - streams.err.writeLine(\`useradd: \${error.message}\`); - return -1; - } - - if (!josh.userList.add(user)) { - streams.err.writeLine(\`useradd: Unexpectedly failed to add user '\${input.args[0]}'.\`); - return -1; - } - - streams.out.writeLine(\`useradd: Added user '\${input.args[0]}'.\`); - return 0; - }, - \`add new user\`, - \`useradd ${n} - [-h/--home=home] ${n} - [-d/--description=description] ${n} - name password\`.trimMultiLines(), - \`Adds a user with the given data to the system. - - The name must consist solely of alphanumerical characters. - The home directory and the description must not contain the pipe character (') or the newline ${n} - character ('\\\\n'). - - If no home is given, it defaults to "/home/name".\`.trimMultiLines(), - new InputValidator({minArgs: 2, maxArgs: 4}) - )`, - "userdel": `return new Command( - (input, streams) => { - if (!josh.userList.has(input.args[0])) { - streams.err.writeLine(\`userdel: Could not delete non-existent user '\${input.args[0]}'.\`); - return -1; - } - - if (!josh.userList.delete(input.args[0])) { - streams.err.writeLine(\`userdel: Unexpectedly failed to delete user '\${input.args[0]}'.\`); - return -1; - } - - streams.out.writeLine(\`userdel: Deleted user '\${input.args[0]}'.\`); - return 0; - }, - \`delete user\`, - \`userdel name\`, - \`Deletes the user with the given name.\`.trimMultiLines(), - new InputValidator({minArgs: 1, maxArgs: 1}) - )`, - "usermod": `return new Command( - (input, streams) => { - let user = josh.userList.get(input.args[0]); - if (user === undefined) { - streams.err.writeLine(\`usermod: Could not modify non-existent user '\${input.args[0]}'.\`); - return -1; - } - - try { - if (input.hasAnyOption("-p", "--password")) { - const password = input.options["-p"] || input.options["--password"]; - user.passwordHash = HashProvider.default.hashPassword(password); + const node = josh.fileSystem.get(path); + if (node === undefined) { + streams.err.writeLine(\`ls: The directory '\${path}' does not exist.\`); + return -1; + } + if (!(node instanceof Directory)) { + streams.err.writeLine(\`ls: '\${path}' is not a directory.\`); + return -1; } - if (input.hasAnyOption("-h", "--home")) - user.home = input.options["-h"] || input.options["--home"]; - if (input.hasAnyOption("-d", "--description")) - user.description = input.options["-d"] || input.options["--description"]; - } catch (error) { - streams.err.writeLine(\`usermod: \${error.message}\`); - return -1; - } - if (!josh.userList.modify(user)) { - streams.err.writeLine(\`usermod: Unexpectedly failed to modify user '\${input.args[0]}'.\`); - return -1; - } + const dirList = [ + new Directory().nameString("./", path), + new Directory().nameString("../", path.parent) + ]; + const fileList = []; - streams.out.writeLine(\`usermod: Modified user '\${input.args[0]}'.\`); - return 0; - }, - \'modify user\', - \`usermod ${n} - [-p/--password=password] ${n} - [-h/--home=home] ${n} - [-d/--description=description] ${n} - name\`.trimMultiLines(), - \`Modifies the user with the given name. See the "useradd" command for more information on the ${n} - fields that can be modified.\`.trimMultiLines(), - new InputValidator({minArgs: 1, maxArgs: 1}) - )`, - "whoami": `return new Command( - (input, streams) => { - const user = josh.userList.get(josh.environment.get("user")); - if (user === undefined) { - streams.err.writeLine("whoami: Cannot execute while not logged in."); - return -1; - } + const nodes = node.nodes; + Object.keys(nodes) + .sortAlphabetically(it => it, true) + .forEach(name => { + const node = nodes[name]; + if (name.startsWith(".") && !input.hasAnyOption("-a", "-A", "--all")) + return; - streams.out.writeLine(user.description); - return 0; - }, - \`print short description of user\`, - \`whoami\`, - \`Print a description of the user associated with the current effective user ID.\`, - new InputValidator({maxArgs: 0}) - )`, + 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( + \`ls: '\${path.getChild(name)}' is neither a file nor a directory.\`); + }); + + if (input.argc > 1) + streams.out.writeLine(\`\${path}\`); + streams.out.writeLine(dirList.concat(fileList).join("\\n")); + return 0; + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`list directory contents\`, + \`ls [-a | -A | --all] [directory ...]\`, + \`Displays the files and directories in each directory. If no directory is given, the files and ${n} + directories in the current working directory are shown. If more than one directory is given, the files and ${n} + directories are shown for each given directory in order. + + Files starting with a . are only shown if the --all option is given, with the exception of ${n} + . and .., which are always shown.\`.trimMultiLines(), + new InputValidator() +)`, + "mkdir": `\ +return new Command( + (input, streams) => { + return input.args + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map(path => { + try { + josh.fileSystem.add(path, new Directory(), input.hasAnyOption("-p", "--parents")); + return 0; + } catch (error) { + streams.err.writeLine(\`mkdir: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`make directories\`, + \`mkdir [-p | --parents] directory ...\`, + \`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 ${n} + --parents option is given, parent directories that do not exist are created as well.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "mv": `\ +return new Command( + (input, streams) => { + let mappings; + try { + const cwd = josh.environment.get("cwd"); + mappings = josh.fileSystem.determineMoveMappings( + input.args.slice(0, -1).map((it) => Path.interpret(cwd, it)), + Path.interpret(cwd, input.args.slice(-1)[0]) + ); + } catch (error) { + streams.err.writeLine(\`mv: \${error.message}\`); + return -1; + } + + return mappings + .map(([source, destination]) => { + try { + josh.fileSystem.move(source, destination); + return 0; + } catch (error) { + streams.err.writeLine(\`mv: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`move files\`, + \`mv source destination file + mv source ... destination directory\`, + \`In its first form, source is renamed to target file. target file must not exist yet. + + In its second form, all source files are moved into target directory, which must be a ${n} + pre-existing directory. The file names of the source files are retained.\`.trimMultiLines(), + new InputValidator({minArgs: 2}) +)`, + "not": `\ +return new Command( + (input, streams) => { + return Number(!josh.interpreter.execute( + InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), + streams + )); + }, + \`execute command and invert status code\`, + \`not command\`, + \`Executes command 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(), + new InputValidator({minArgs: 1}) +)`, + "open": `\ +return new Command( + (input, streams) => { + return input.args + .map(it => Path.interpret(josh.environment.get("cwd"), it)) + .map((path, i) => { + try { + const target = i > 0 || input.hasAnyOption("-b", "--blank") || josh.util.isStandalone() + ? "_blank" + : "_self"; + window.open(josh.fileSystem.open(path, "read").read(), target); + return 0; + } catch (error) { + streams.err.writeLine(\`open: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`open web pages\`, + \`open [-b | --blank] file ...\`, + \`Opens the web pages linked to by file. The first file is opened in this tab and the subsequent ${n} + files are opened in new tabs. If --blank is set, the first file is opened in a new tab as ${n} + well. + + If this command is executed inside of a standalone app instead of a browser, every file is opened in a ${n} + tab regardless of whether --blank is given.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "or": `\ +return new Command( + (input, streams) => { + const previousStatus = Number(josh.environment.getOrDefault("status", "0")); + if (previousStatus === 0) + return previousStatus; + + return josh.interpreter.execute( + InputParser.create(josh.environment, josh.fileSystem).parseCommand(input.args), + streams + ); + }, + \`execute command if previous command failed\`, + \`or command\`, + \`Executes command with its associated options and arguments if and only if the status code of the ${n} + previously-executed command is not 0. + + The exit code is retained if it was zero, and is changed to that of command otherwise.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "poweroff": `\ +return new Command( + (input, streams) => { + const userName = josh.environment.get("user"); + if (userName === "") { + streams.err.writeLine("poweroff: Cannot execute while not logged in."); + return -1; + } + + Persistence.setPoweroff(true); + setTimeout(() => location.reload(), 2000); + + streams.out.writeLine( + \`Shutdown NOW! + + *** FINAL System shutdown message from \${userName}@fwdekker.com *** + + System going down IMMEDIATELY + + + System shutdown time has arrived\`.trimLines() + ); + return 0; + }, + \`close down the system\`, + \`poweroff\`, + \`Automated shutdown procedure to nicely notify users when the system is shutting down.\`, + new InputValidator({maxArgs: 0}) +)`, + "pwd": `\ +return new Command( + (input, streams) => { + streams.out.writeLine(josh.environment.get("cwd") || ""); + return 0; + }, + \`print working directory\`, + \`pwd\`, + \`Displays the current working directory.\`, + new InputValidator({maxArgs: 0}) +)`, + "rm": `\ +return new Command( + (input, streams) => { + return input.args + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map(path => { + try { + const target = josh.fileSystem.get(path); + if (target === undefined) { + if (input.hasAnyOption("-f", "--force")) + return 0; + + streams.err.writeLine(\`rm: The file '\${path}' does not exist.\`); + return -1; + } + if (target instanceof Directory) { + if (!input.hasAnyOption("-r", "-R", "--recursive")) { + streams.err.writeLine(\`rm: '\${path}' is a directory.\`); + return -1; + } + if (path.toString() === "/" && !input.hasAnyOption("--no-preserve-root")) { + streams.err.writeLine("rm: Cannot remove root directory."); + return -1; + } + } + + josh.fileSystem.remove(path); + return 0; + } catch (error) { + streams.err.writeLine(\`rm: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`remove file\`, + \`rm [-f | --force] [-r | -R | --recursive] ${n} + [--no-preserve-root] file ...\`.trimMultiLines(), + \`Removes each given file. If more than one file is given, they are removed in the order they are ${n} + given in. + + If --force is set, no warning is given if a file could not be removed. + + If --recursive is set, files and directories are removed recursively; without this option directories ${n} + cannot be removed. + + Unless --no-preserve-root is set, the root directory cannot be removed.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "rmdir": `\ +return new Command( + (input, streams) => { + return input.args + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map(path => { + try { + const target = josh.fileSystem.get(path); + if (target === undefined) { + streams.err.writeLine(\`rmdir: '\${path}' does not exist.\`); + return -1; + } + if (!(target instanceof Directory)) { + streams.err.writeLine(\`rmdir: '\${path}' is not a directory.\`); + return -1; + } + if (target.nodeCount !== 0) { + streams.err.writeLine(\`rmdir: '\${path}' is not empty.\`); + return -1; + } + + josh.fileSystem.remove(path); + return 0; + } catch (error) { + streams.err.writeLine(\`rmdir: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`remove directories\`, + \`rmdir directory ...\`, + \`Removes each given directory. If more than one directory is given, they are removed in the ${n} + order they are given in. Non-empty directories will not be removed.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "set": `\ +return new Command( + (input, streams) => { + try { + if (input.argc === 1) + josh.environment.safeDelete(input.args[0]); + else + josh.environment.safeSet(input.args[0], input.args[1]); + } catch (error) { + streams.err.writeLine(\`set: \${error.message}\`); + return -1; + } + + return 0; + }, + \`set environment variable\`, + \`set key [value]\`, + \`Sets the environment variable key to value. If no value is given, the environment ${n} + variable is cleared. Read-only variables cannot be set.\`.trimMultiLines(), + new InputValidator({minArgs: 1, maxArgs: 2}) +)`, + "touch": `\ +return new Command( + (input, streams) => { + return input.args + .map(arg => Path.interpret(josh.environment.get("cwd"), arg)) + .map(path => { + try { + josh.fileSystem.add(path, new File(), false); + return 0; + } catch (error) { + streams.err.writeLine(\`touch: \${error.message}\`); + return -1; + } + }) + .reduce((acc, exitCode) => exitCode === 0 ? acc : exitCode); + }, + \`change file timestamps\`, + \`touch file ...\`, + \`Update the access and modification times of each file to the current time. If a file does not ${n} + exist, it is created.\`.trimMultiLines(), + new InputValidator({minArgs: 1}) +)`, + "useradd": `\ +return new Command( + (input, streams) => { + if (josh.userList.has(input.args[0])) { + streams.err.writeLine(\`useradd: User '\${input.args[0]}' already exists.\`); + return -1; + } + + let user; + try { + user = new User(input.args[0], HashProvider.default.hashPassword(input.args[1])); + if (input.hasAnyOption("-h", "--home")) + user.home = input.options["-h"] || input.options["--home"]; + if (input.hasAnyOption("-d", "--description")) + user.description = input.options["-d"] || input.options["--description"]; + } catch (error) { + streams.err.writeLine(\`useradd: \${error.message}\`); + return -1; + } + + if (!josh.userList.add(user)) { + streams.err.writeLine(\`useradd: Unexpectedly failed to add user '\${input.args[0]}'.\`); + return -1; + } + + streams.out.writeLine(\`useradd: Added user '\${input.args[0]}'.\`); + return 0; + }, + \`add new user\`, + \`useradd ${n} + [-h/--home=home] ${n} + [-d/--description=description] ${n} + name password\`.trimMultiLines(), + \`Adds a user with the given data to the system. + + The name must consist solely of alphanumerical characters. + The home directory and the description must not contain the pipe character (') or the newline ${n} + character ('\\\\n'). + + If no home is given, it defaults to "/home/name".\`.trimMultiLines(), + new InputValidator({minArgs: 2, maxArgs: 4}) +)`, + "userdel": `\ +return new Command( + (input, streams) => { + if (!josh.userList.has(input.args[0])) { + streams.err.writeLine(\`userdel: Could not delete non-existent user '\${input.args[0]}'.\`); + return -1; + } + + if (!josh.userList.delete(input.args[0])) { + streams.err.writeLine(\`userdel: Unexpectedly failed to delete user '\${input.args[0]}'.\`); + return -1; + } + + streams.out.writeLine(\`userdel: Deleted user '\${input.args[0]}'.\`); + return 0; + }, + \`delete user\`, + \`userdel name\`, + \`Deletes the user with the given name.\`.trimMultiLines(), + new InputValidator({minArgs: 1, maxArgs: 1}) +)`, + "usermod": `\ +return new Command( + (input, streams) => { + let user = josh.userList.get(input.args[0]); + if (user === undefined) { + streams.err.writeLine(\`usermod: Could not modify non-existent user '\${input.args[0]}'.\`); + return -1; + } + + try { + if (input.hasAnyOption("-p", "--password")) { + const password = input.options["-p"] || input.options["--password"]; + user.passwordHash = HashProvider.default.hashPassword(password); + } + if (input.hasAnyOption("-h", "--home")) + user.home = input.options["-h"] || input.options["--home"]; + if (input.hasAnyOption("-d", "--description")) + user.description = input.options["-d"] || input.options["--description"]; + } catch (error) { + streams.err.writeLine(\`usermod: \${error.message}\`); + return -1; + } + + if (!josh.userList.modify(user)) { + streams.err.writeLine(\`usermod: Unexpectedly failed to modify user '\${input.args[0]}'.\`); + return -1; + } + + streams.out.writeLine(\`usermod: Modified user '\${input.args[0]}'.\`); + return 0; + }, + \'modify user\', + \`usermod ${n} + [-p/--password=password] ${n} + [-h/--home=home] ${n} + [-d/--description=description] ${n} + name\`.trimMultiLines(), + \`Modifies the user with the given name. See the "useradd" command for more information on the fields ${n} + that can be modified.\`.trimMultiLines(), + new InputValidator({minArgs: 1, maxArgs: 1}) +)`, + "whoami": `\ +return new Command( + (input, streams) => { + const user = josh.userList.get(josh.environment.get("user")); + if (user === undefined) { + streams.err.writeLine("whoami: Cannot execute while not logged in."); + return -1; + } + + streams.out.writeLine(user.description); + return 0; + }, + \`print short description of user\`, + \`whoami\`, + \`Print a description of the user associated with the current effective user ID.\`, + new InputValidator({maxArgs: 0}) +)`, };