forked from tools/josh
parent
fd598bbefe
commit
17e8ab37a0
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "fwdekker.com",
|
"name": "fwdekker.com",
|
||||||
"version": "0.38.5",
|
"version": "0.39.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",
|
||||||
|
|
|
@ -66,6 +66,14 @@ export class Commands {
|
||||||
return ExitCode.COMMAND_NOT_FOUND;
|
return ExitCode.COMMAND_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command instanceof Script) {
|
||||||
|
const parser = InputParser.create(this.environment, this.fileSystem);
|
||||||
|
return command.lines
|
||||||
|
.map(line => parser.parseCommands(line))
|
||||||
|
.reduce((acc, input) => acc.concat(input))
|
||||||
|
.reduce((acc, code) => acc !== 0 ? acc : this.execute(code, streams), 0);
|
||||||
|
}
|
||||||
|
|
||||||
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]));
|
||||||
|
@ -82,7 +90,7 @@ export class Commands {
|
||||||
* @return the command addressed by the given name or path, an 'Error' if the command could be found but could not
|
* @return the command addressed by the given name or path, an 'Error' if the command could be found but could not
|
||||||
* be parsed, or `undefined` if the command could not be found
|
* be parsed, or `undefined` if the command could not be found
|
||||||
*/
|
*/
|
||||||
resolve(commandName: string): Command | Error | undefined {
|
resolve(commandName: string): Command | Script | Error | undefined {
|
||||||
const cwd = this.environment.get("cwd");
|
const cwd = this.environment.get("cwd");
|
||||||
|
|
||||||
let script: Node | undefined;
|
let script: Node | undefined;
|
||||||
|
@ -97,7 +105,11 @@ export class Commands {
|
||||||
|
|
||||||
const code = script.open("read").read();
|
const code = script.open("read").read();
|
||||||
try {
|
try {
|
||||||
return this.interpretScript(code, this.environment, this.userList, this.fileSystem);
|
if (code.startsWith("#!/bin/josh\n")) {
|
||||||
|
return new Script(code.split("\n").slice(1));
|
||||||
|
} else {
|
||||||
|
return this.interpretBinary(code, this.environment, this.userList, this.fileSystem);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to interpret script '${commandName}'.`, code, e);
|
console.error(`Failed to interpret script '${commandName}'.`, code, e);
|
||||||
return e;
|
return e;
|
||||||
|
@ -105,15 +117,15 @@ export class Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interprets the given code and returns the `Command` it describes.
|
* Interprets the given binary and returns the `Command` it describes.
|
||||||
*
|
*
|
||||||
* @param code a string describing a `Command`
|
* @param code a string describing a `Command`, i.e. a "binary"
|
||||||
* @param environment the environment in which the code is to be executed
|
* @param environment the environment in which the code is to be executed
|
||||||
* @param userList the list of users relevant to the code
|
* @param userList the list of users relevant to the code
|
||||||
* @param fileSystem the file system to refer to when executing code
|
* @param fileSystem the file system to refer to when executing code
|
||||||
* @return the `Command` described by the given code
|
* @return the `Command` described by the given code
|
||||||
*/
|
*/
|
||||||
private interpretScript(code: string, environment: Environment, userList: UserList,
|
private interpretBinary(code: string, environment: Environment, userList: UserList,
|
||||||
fileSystem: FileSystem): Command {
|
fileSystem: FileSystem): Command {
|
||||||
const josh = {
|
const josh = {
|
||||||
"environment": environment,
|
"environment": environment,
|
||||||
|
@ -161,6 +173,28 @@ export class Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A script referring to other commands that can be executed.
|
||||||
|
*/
|
||||||
|
export class Script {
|
||||||
|
/**
|
||||||
|
* The lines that make up the script.
|
||||||
|
*/
|
||||||
|
readonly lines: string[];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new script from the given lines.
|
||||||
|
*
|
||||||
|
* Each line should be a valid line in the command line, i.e. can be parsed into an `InputArgs`.
|
||||||
|
*
|
||||||
|
* @param lines the lines that make up the script
|
||||||
|
*/
|
||||||
|
constructor(lines: string[]) {
|
||||||
|
this.lines = lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command that can be executed.
|
* A command that can be executed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class FileSystem {
|
||||||
return [`${name}.lnk`, new File(entry.link)];
|
return [`${name}.lnk`, new File(entry.link)];
|
||||||
|
|
||||||
const dir = new Directory();
|
const dir = new Directory();
|
||||||
entry.entries.forEach((child: any) => dir.add(...(this.unpack(child))))
|
entry.entries.forEach((child: any) => dir.add(...(this.unpack(child))));
|
||||||
return [name, dir];
|
return [name, dir];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -708,14 +708,18 @@ export class File extends Node {
|
||||||
|
|
||||||
nameString(name: string, path: Path): string {
|
nameString(name: string, path: Path): string {
|
||||||
switch (this.mime ?? getFileExtension(name)) {
|
switch (this.mime ?? getFileExtension(name)) {
|
||||||
case "txt": {
|
case "jsh": {
|
||||||
const script = `execute('cat ${path.toString(true)}')`;
|
const script = `execute('${path.toString(true)}'); return false`;
|
||||||
return `<a href="#" class="fileLink" onclick="${script}">${name}</a>`;
|
return `<a href="#" class="fileLink" onclick="${script}">${name}</a>`;
|
||||||
}
|
}
|
||||||
case "lnk": {
|
case "lnk": {
|
||||||
const script = `execute('open ${path.toString(true)}'); return false`;
|
const script = `execute('open ${path.toString(true)}'); return false`;
|
||||||
return `<a href="${this.contents}" class="fileLink" onclick="${script}">${name}</a>`;
|
return `<a href="${this.contents}" class="fileLink" onclick="${script}">${name}</a>`;
|
||||||
}
|
}
|
||||||
|
case "txt": {
|
||||||
|
const script = `execute('cat ${path.toString(true)}')`;
|
||||||
|
return `<a href="#" class="fileLink" onclick="${script}">${name}</a>`;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {expect} from "chai";
|
||||||
import "jsdom-global";
|
import "jsdom-global";
|
||||||
import "mocha";
|
import "mocha";
|
||||||
|
|
||||||
import {Command, commandBinaries, Commands, ExitCode} from "../main/js/Commands";
|
import {Command, commandBinaries, Commands, ExitCode, Script} 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";
|
||||||
|
@ -54,43 +54,74 @@ describe("commands", () => {
|
||||||
|
|
||||||
|
|
||||||
describe("execute", () => {
|
describe("execute", () => {
|
||||||
it("writes an error if it cannot resolve the command", () => {
|
describe("error handling", () => {
|
||||||
expect(execute("does-not-exist")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
it("writes an error if it cannot resolve the target", () => {
|
||||||
expect(readErr()).to.equal("Unknown command 'does-not-exist'.\n");
|
expect(execute("does-not-exist")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
||||||
|
expect(readErr()).to.equal("Unknown command 'does-not-exist'.\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes an error if the target has no shebang and it is not a valid command", () => {
|
||||||
|
fileSystem.add(new Path("/command"), new File("echo hesitate"), false);
|
||||||
|
|
||||||
|
expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
||||||
|
expect(readErr()).to.equal("Could not parse command '/command': SyntaxError: Unexpected identifier.\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes an error if the target is a doc-only command", () => {
|
||||||
|
fileSystem.add(new Path("/command"), new File(`return new DocOnlyCommand("", "")`), false);
|
||||||
|
|
||||||
|
expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
||||||
|
expect(readErr()).to.equal("Could not execute doc-only command. Try 'help /command' instead.\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes an error if the arguments to the command are invalid", () => {
|
||||||
|
const command = `return new Command("", "", "", "", new InputValidator({minArgs: 2}))`;
|
||||||
|
fileSystem.add(new Path("/command"), new File(command), false);
|
||||||
|
|
||||||
|
expect(execute("/command arg1")).to.equal(ExitCode.USAGE);
|
||||||
|
expect(readErr()).to.contain("Invalid usage of '/command'. Expected at least 2 arguments but got 1.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes an error if the command is invalid", () => {
|
describe("scripts", () => {
|
||||||
fileSystem.add(new Path("/command"), new File("invalid"), false);
|
it("executes an empty script", () => {
|
||||||
|
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n"), false);
|
||||||
|
|
||||||
expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
expect(execute("/script")).to.equal(0);
|
||||||
expect(readErr()).to.equal("Could not parse command '/command': ReferenceError: invalid is not defined.\n");
|
expect(readOut()).to.equal("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("executes the target as a script if there is a shebang", () => {
|
||||||
|
loadCommand("echo");
|
||||||
|
|
||||||
|
fileSystem.add(new Path("/script"), new File("#!/bin/josh\necho though\necho only"), false);
|
||||||
|
|
||||||
|
expect(execute("/script")).to.equal(0);
|
||||||
|
expect(readOut()).to.equal("though\nonly\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores whitespace around individual lines except the shebang", () => {
|
||||||
|
loadCommand("echo");
|
||||||
|
|
||||||
|
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n echo rescue \n echo flour "), false);
|
||||||
|
|
||||||
|
expect(execute("/script")).to.equal(0);
|
||||||
|
expect(readOut()).to.equal("rescue\nflour\n");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes an error if the command is a doc-only command", () => {
|
describe("commands", () => {
|
||||||
fileSystem.add(new Path("/command"), new File(`return new DocOnlyCommand("", "")`), false);
|
it("executes the target as a command if there is no shebang", () => {
|
||||||
|
const command = `return new Command(
|
||||||
expect(execute("/command")).to.equal(ExitCode.COMMAND_NOT_FOUND);
|
|
||||||
expect(readErr()).to.equal("Could not execute doc-only command. Try 'help /command' instead.\n");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("writes an error if the arguments to the command are invalid", () => {
|
|
||||||
const command = `return new Command("", "", "", "", new InputValidator({minArgs: 2}))`;
|
|
||||||
fileSystem.add(new Path("/command"), new File(command), false);
|
|
||||||
|
|
||||||
expect(execute("/command arg1")).to.equal(ExitCode.USAGE);
|
|
||||||
expect(readErr()).to.contain("Invalid usage of '/command'. Expected at least 2 arguments but got 1.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("executes the command otherwise", () => {
|
|
||||||
const command = `return new Command(
|
|
||||||
(input, streams) => { streams.out.writeLine(input.args[0]); return Number(input.args[1]); },
|
(input, streams) => { streams.out.writeLine(input.args[0]); return Number(input.args[1]); },
|
||||||
"", "", "",
|
"", "", "",
|
||||||
new InputValidator()
|
new InputValidator()
|
||||||
)`.trimMultiLines();
|
)`.trimMultiLines();
|
||||||
fileSystem.add(new Path("/command"), new File(command), false);
|
fileSystem.add(new Path("/command"), new File(command), false);
|
||||||
|
|
||||||
expect(execute("/command output 42")).to.equal(42);
|
expect(execute("/command output 42")).to.equal(42);
|
||||||
expect(readOut()).to.equal("output\n");
|
expect(readOut()).to.equal("output\n");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,6 +158,18 @@ describe("commands", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("scripts", () => {
|
||||||
|
it("resolves a script if it exists", () => {
|
||||||
|
fileSystem.add(new Path("/script"), new File("#!/bin/josh\necho square"), true);
|
||||||
|
|
||||||
|
expect((commands.resolve("/script") as Script).lines).to.deep.equal(["echo square"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cannot resolve a script if it does not exist", () => {
|
||||||
|
expect(commands.resolve("/script")).to.equal(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("cannot resolve a command if the file cannot be parsed", () => {
|
it("cannot resolve a command if the file cannot be parsed", () => {
|
||||||
fileSystem.add(new Path("/command"), new File("invalid"), true);
|
fileSystem.add(new Path("/command"), new File("invalid"), true);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue