forked from tools/josh
1
0
Fork 0

Allow navigating to directories without `cd`

Fixes #147.
This commit is contained in:
Florine W. Dekker 2020-12-07 12:56:45 +01:00
parent cd17170657
commit 86b07555da
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
4 changed files with 100 additions and 34 deletions

View File

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

View File

@ -52,58 +52,71 @@ export class Commands {
if (input.command === "")
return ExitCode.OK;
const command = this.resolve(input.command);
if (command === undefined) {
let target = this.resolve(input.command);
if (target === undefined) {
streams.err.writeLine(`Unknown command '${input.command}'.`);
return ExitCode.COMMAND_NOT_FOUND;
}
if (command instanceof Error) {
streams.err.writeLine(`Could not parse command '${input.command}': ${command}.`);
if (target instanceof Error) {
streams.err.writeLine(`Could not parse command '${input.command}': ${target}.`);
return ExitCode.COMMAND_NOT_FOUND;
}
if (command instanceof DocOnlyCommand) {
if (target instanceof DocOnlyCommand) {
streams.err.writeLine(`Could not execute doc-only command. Try 'help ${input.command}' instead.`);
return ExitCode.COMMAND_NOT_FOUND;
}
if (command instanceof Script) {
if (target instanceof Directory) {
return this.execute(
new InputArgs("/bin/cd", input.options, [input.command].concat(input.args), input.redirectTargets),
streams
);
} else if (target instanceof Script) {
const parser = InputParser.create(this.environment, this.fileSystem);
return command.lines
return target.lines
.map(line => parser.parseCommands(line))
.reduce((acc, input) => acc.concat(input))
.reduce((acc, code) => acc !== 0 ? acc : this.execute(code, streams), 0);
}
} else {
const validation = target.validator.validate(input);
if (!validation[0]) {
streams.err.writeLine(this.createUsageErrorOutput(input.command, target, validation[1]));
return ExitCode.USAGE;
}
const validation = command.validator.validate(input);
if (!validation[0]) {
streams.err.writeLine(this.createUsageErrorOutput(input.command, command, validation[1]));
return ExitCode.USAGE;
return target.fun.bind(this)(input, streams);
}
return command.fun.bind(this)(input, streams);
}
/**
* Finds the `Command` with the given name and returns it.
* Resolves a target from its name by looking through the file system.
*
* @param commandName the name of or path to the command to find
* @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
* If the target is a command, the command is parsed (but not yet invoked) and returned.
*
* Targets are resolved as follows. If the target name contains a slash, then interpret that path literally.
* Otherwise, look for the target in /bin. If no such target exists or it is not a file, then interpret it as a
* relative path.
*
* @param targetName the name of the target to be resolved
* @return the `Command`, `Script`, or `Directory` addressed by the target, an `Error` if the target could be found
* but could not be parsed, or `undefined` if the target could not be found
*/
resolve(commandName: string): Command | Script | Error | undefined {
resolve(targetName: string): Command | Script | Directory | Error | undefined {
const cwd = this.environment.get("cwd");
let script: Node | undefined;
if (commandName.includes("/")) {
script = this.fileSystem.get(Path.interpret(cwd, commandName));
let target: Node | undefined;
if (targetName.includes("/")) {
target = this.fileSystem.get(Path.interpret(cwd, targetName));
} else {
script = this.fileSystem.get(Path.interpret(cwd, "/bin", commandName));
}
if (!(script instanceof File)) {
return undefined;
target = this.fileSystem.get(Path.interpret(cwd, "/bin", targetName));
if (!(target instanceof File))
target = this.fileSystem.get(Path.interpret(cwd, targetName));
}
const code = script.open("read").read();
if (!(target instanceof File))
return target instanceof Directory ? target : undefined;
const code = target.open("read").read();
try {
if (code.startsWith("#!/bin/josh\n")) {
return new Script(code.split("\n").slice(1));
@ -111,7 +124,7 @@ export class Commands {
return this.interpretBinary(code, this.environment, this.userList, this.fileSystem);
}
} catch (e) {
console.error(`Failed to interpret script '${commandName}'.`, code, e);
console.error(`Failed to interpret script '${targetName}'.`, code, e);
return e;
}
}

View File

@ -610,7 +610,7 @@ export class Directory extends Node {
* @param path the path to this node
*/
nameString(name: string, path: Path): string {
return `<a href="#" class="dirLink" onclick="execute('cd ${path.toString(true)}; and ls -l')">${name}</a>`;
return `<a href="#" class="dirLink" onclick="execute('${path.toString(true)}; and ls -l')">${name}</a>`;
}
visit(path: string,

View File

@ -83,11 +83,64 @@ describe("commands", () => {
});
});
describe("directories", () => {
beforeEach(() => loadCommand("cd"));
it("navigates to an absolute directory", () => {
fileSystem.add(new Path("/dir"), new Directory(), false);
expect(execute("/dir")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/dir");
});
it("navigates to a relative directory without a slash", () => {
fileSystem.add(new Path("/dir1/dir2"), new Directory(), true);
environment.set("cwd", "/dir1");
expect(execute("dir2")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/dir1/dir2");
});
it("navigates to a relative directory with a slash", () => {
fileSystem.add(new Path("/dir1/dir2"), new Directory(), true);
environment.set("cwd", "dir1");
expect(execute("dir2/")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/dir1/dir2");
});
it("navigates to a nested relative directory", () => {
fileSystem.add(new Path("/dir1/dir2/dir3"), new Directory(), true);
environment.set("cwd", "dir1");
expect(execute("dir2/dir3")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/dir1/dir2/dir3");
});
describe("/bin", () => {
it("cannot navigate to a directory in /bin by only typing its name if not currently in /bin", () => {
fileSystem.add(new Path("/bin/dir"), new Directory(), true);
expect(execute("dir")).to.equal(ExitCode.COMMAND_NOT_FOUND);
expect(environment.get("cwd")).to.equal("/");
});
it("navigates to a directory in /bin by only typing its name if currently in /bin", () => {
fileSystem.add(new Path("/bin/dir"), new Directory(), true);
environment.set("cwd", "/bin");
expect(execute("dir")).to.equal(ExitCode.OK);
expect(environment.get("cwd")).to.equal("/bin/dir");
});
});
});
describe("scripts", () => {
it("executes an empty script", () => {
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n"), false);
expect(execute("/script")).to.equal(0);
expect(execute("/script")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("");
});
@ -96,7 +149,7 @@ describe("commands", () => {
fileSystem.add(new Path("/script"), new File("#!/bin/josh\necho though\necho only"), false);
expect(execute("/script")).to.equal(0);
expect(execute("/script")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("though\nonly\n");
});
@ -105,7 +158,7 @@ describe("commands", () => {
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n echo rescue \n echo flour "), false);
expect(execute("/script")).to.equal(0);
expect(execute("/script")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("rescue\nflour\n");
});
});
@ -576,7 +629,7 @@ describe("commands", () => {
it("does nothing if the previous command exited successfully", () => {
environment.set("status", "0");
environment.set("status", "" + ExitCode.OK);
expect(execute("or echo 'message'")).to.equal(ExitCode.OK);
expect(readOut()).to.equal("");