parent
fee631e216
commit
8f283be633
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fwdekker.com",
|
||||
"version": "0.39.8",
|
||||
"version": "0.39.9",
|
||||
"description": "The source code of [my personal website](https://fwdekker.com/).",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -5,7 +5,7 @@ import {InputArgs} from "./InputArgs";
|
|||
import {InputParser} from "./InputParser";
|
||||
import {Persistence} from "./Persistence";
|
||||
import {escapeHtml, IllegalArgumentError, IllegalStateError, isStandalone} from "./Shared";
|
||||
import {StreamSet} from "./Stream";
|
||||
import {OutputStream, StreamSet} from "./Stream";
|
||||
import {EscapeCharacters} from "./Terminal";
|
||||
import {HashProvider, User, UserList} from "./UserList";
|
||||
|
||||
|
@ -52,39 +52,48 @@ export class Commands {
|
|||
if (input.command === "")
|
||||
return ExitCode.OK;
|
||||
|
||||
let target = this.resolve(input.command);
|
||||
const localStreams = streams.copy();
|
||||
try {
|
||||
localStreams.out = this.toStream(input.redirectTargets[1]) ?? localStreams.out;
|
||||
localStreams.err = this.toStream(input.redirectTargets[2]) ?? localStreams.err;
|
||||
} catch (error) {
|
||||
streams.err.writeLine(`Error while redirecting output:\n${error.message}`);
|
||||
return ExitCode.MISC;
|
||||
}
|
||||
|
||||
const target = this.resolve(input.command);
|
||||
if (target === undefined) {
|
||||
streams.err.writeLine(`Unknown command '${input.command}'.`);
|
||||
localStreams.err.writeLine(`Unknown command '${input.command}'.`);
|
||||
return ExitCode.COMMAND_NOT_FOUND;
|
||||
}
|
||||
if (target instanceof Error) {
|
||||
streams.err.writeLine(`Could not parse command '${input.command}': ${target}.`);
|
||||
localStreams.err.writeLine(`Could not parse command '${input.command}': ${target}.`);
|
||||
return ExitCode.COMMAND_NOT_FOUND;
|
||||
}
|
||||
if (target instanceof DocOnlyCommand) {
|
||||
streams.err.writeLine(`Could not execute doc-only command. Try 'help ${input.command}' instead.`);
|
||||
localStreams.err.writeLine(`Could not execute doc-only command. Try 'help ${input.command}' instead.`);
|
||||
return ExitCode.COMMAND_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (target instanceof Directory) {
|
||||
return this.execute(
|
||||
new InputArgs("/bin/cd", input.options, [input.command].concat(input.args), input.redirectTargets),
|
||||
streams
|
||||
localStreams
|
||||
);
|
||||
} else if (target instanceof Script) {
|
||||
const parser = InputParser.create(this.environment, this.fileSystem);
|
||||
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);
|
||||
.reduce((acc, input) => acc.concat(input), []) // .flat()
|
||||
.reduce((acc, code) => acc !== 0 ? acc : this.execute(code, localStreams), 0);
|
||||
} else {
|
||||
const validation = target.validator.validate(input);
|
||||
if (!validation[0]) {
|
||||
streams.err.writeLine(this.createUsageErrorOutput(input.command, target, validation[1]));
|
||||
localStreams.err.writeLine(this.createUsageErrorOutput(input.command, target, validation[1]));
|
||||
return ExitCode.USAGE;
|
||||
}
|
||||
|
||||
return target.fun.bind(this)(input, streams);
|
||||
return target.fun.bind(this)(input, localStreams);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,6 +196,22 @@ export class Commands {
|
|||
<b>Usage</b>
|
||||
${command.usage}`.trimLines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a redirect target to an output stream, or `undefined` if the default stream is used.
|
||||
*
|
||||
* @param target the target to convert
|
||||
* @throws if the stream could not be opened
|
||||
*/
|
||||
private toStream(target: InputArgs.RedirectTarget | undefined): OutputStream | undefined {
|
||||
if (target === undefined)
|
||||
return undefined;
|
||||
|
||||
if (target.target === undefined)
|
||||
throw new IllegalStateError("Redirect target's target is undefined.");
|
||||
|
||||
return this.fileSystem.open(Path.interpret(this.environment.get("cwd"), target.target), target.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Commands} from "./Commands";
|
||||
import {Commands, ExitCode} from "./Commands";
|
||||
import {Environment} from "./Environment";
|
||||
import {Directory, FileSystem, Path} from "./FileSystem";
|
||||
import {InputArgs} from "./InputArgs";
|
||||
|
@ -164,23 +164,13 @@ export class Shell {
|
|||
inputs = InputParser.create(this.environment, this.fileSystem).parseCommands(inputString);
|
||||
} catch (error) {
|
||||
streams.err.writeLine(`Could not parse input: ${error.message}`);
|
||||
this.environment.set("status", "-1");
|
||||
this.environment.set("status", "" + ExitCode.USAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
inputs.forEach(input => {
|
||||
const localStreams = streams.copy();
|
||||
try {
|
||||
localStreams.out = this.toStream(input.redirectTargets[1]) ?? localStreams.out;
|
||||
localStreams.err = this.toStream(input.redirectTargets[2]) ?? localStreams.err;
|
||||
} catch (error) {
|
||||
streams.err.writeLine(`Error while redirecting output:\n${error.message}`);
|
||||
this.environment.set("status", "-1");
|
||||
return;
|
||||
}
|
||||
|
||||
const output = this.commands.execute(input, localStreams);
|
||||
this.environment.set("status", "" + output);
|
||||
const status = this.commands.execute(input, streams);
|
||||
this.environment.set("status", "" + status);
|
||||
|
||||
if (this.environment.get("user") === "") {
|
||||
this.inputHistory.clear();
|
||||
|
@ -215,20 +205,4 @@ export class Shell {
|
|||
Persistence.setEnvironment(this.environment);
|
||||
Persistence.setFileSystem(this.fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a redirect target to an output stream, or `undefined` if the default stream is used.
|
||||
*
|
||||
* @param target the target to convert
|
||||
* @throws if the stream could not be opened
|
||||
*/
|
||||
private toStream(target: InputArgs.RedirectTarget | undefined): OutputStream | undefined {
|
||||
if (target === undefined)
|
||||
return undefined;
|
||||
|
||||
if (target.target === undefined)
|
||||
throw new IllegalStateError("Redirect target's target is undefined.");
|
||||
|
||||
return this.fileSystem.open(Path.interpret(this.environment.get("cwd"), target.target), target.type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ export class StreamSet {
|
|||
|
||||
|
||||
/**
|
||||
* Returns a copy of this stream set.
|
||||
* Returns a shallow copy of this stream set.
|
||||
*/
|
||||
copy(): StreamSet {
|
||||
return new StreamSet(this.ins, this.out, this.err);
|
||||
|
|
|
@ -97,6 +97,12 @@ describe("commands", () => {
|
|||
});
|
||||
|
||||
describe("scripts", () => {
|
||||
beforeEach(() => loadCommand("echo"));
|
||||
|
||||
|
||||
const readFile = (pathString: string) => (fileSystem.get(new Path(pathString)) as File).contents
|
||||
|
||||
|
||||
it("executes an empty script", () => {
|
||||
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n"), false);
|
||||
|
||||
|
@ -105,22 +111,35 @@ describe("commands", () => {
|
|||
});
|
||||
|
||||
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(ExitCode.OK);
|
||||
expect(readOut()).to.equal("though\nonly\n");
|
||||
});
|
||||
|
||||
it("ignores whitespace around individual lines except the shebang", () => {
|
||||
loadCommand("echo");
|
||||
|
||||
it("ignores whitespace around individual lines (other than the shebang)", () => {
|
||||
fileSystem.add(new Path("/script"), new File("#!/bin/josh\n echo rescue \n echo flour "), false);
|
||||
|
||||
expect(execute("/script")).to.equal(ExitCode.OK);
|
||||
expect(readOut()).to.equal("rescue\nflour\n");
|
||||
});
|
||||
|
||||
it("supports output redirection", () => {
|
||||
fileSystem.add(new Path("/script"), new File("#!/bin/josh\necho flower > /file.txt"), false);
|
||||
|
||||
expect(execute("/script")).to.equal(ExitCode.OK);
|
||||
expect(readOut()).to.equal("");
|
||||
expect(readFile("/file.txt")).to.equal("flower\n");
|
||||
});
|
||||
|
||||
it("support different output redirection than the one the script is invoked under", () => {
|
||||
fileSystem.add(new Path("/script"), new File("#!/bin/josh\necho sand > /file2.txt\necho hat"), false);
|
||||
|
||||
expect(execute("/script > /file1.txt")).to.equal(ExitCode.OK);
|
||||
expect(readOut()).to.equal("");
|
||||
expect(readFile("/file1.txt")).to.equal("hat\n");
|
||||
expect(readFile("/file2.txt")).to.equal("sand\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("commands", () => {
|
||||
|
|
Loading…
Reference in New Issue