forked from tools/josh
1
0
Fork 0

Allow output redirection inside scripts

Fixes #146.
This commit is contained in:
Florine W. Dekker 2020-12-07 18:08:20 +01:00
parent fee631e216
commit 8f283be633
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
5 changed files with 65 additions and 47 deletions

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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", () => {