diff --git a/HELP.md b/HELP.md index 66215dc..ec19090 100644 --- a/HELP.md +++ b/HELP.md @@ -129,6 +129,10 @@ Restore a `str` session from a saved state file (default `~/.str.json`). #### `runfile ` Execute the contents of the given file as a series of `str` commands. +#### `script [...]` +Execute an external script and replace the current subject with the returned output. +The current subject is passed as the first argument, followed by the optional additional `args`. + ### Basic String Operations diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index ff9abfd..68fe300 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -50,6 +50,7 @@ import {Zip} from "./zip.js"; import {Concat} from "./concat.js"; import {Call} from "./call.js"; import {Chunk} from "./chunk.js"; +import {Script} from "./script.js"; export type Commands = Command[] export const commands: Commands = [ @@ -90,6 +91,7 @@ export const commands: Commands = [ new RSub, new RunFile, new Save, + new Script, new Set, new Show, new Sort, diff --git a/src/vm/commands/script.ts b/src/vm/commands/script.ts new file mode 100644 index 0000000..7976dc4 --- /dev/null +++ b/src/vm/commands/script.ts @@ -0,0 +1,45 @@ +import {Command, ParseContext, processPath, StrTerm, unwrapString, wrapString} from "./command.js"; +import {LexInput} from "../lexer.js"; +import {promisify} from "node:util"; +import {execFile} from "node:child_process"; +import {StrVM} from "../vm.js"; + +type ScriptData = { + path: StrTerm, + args: StrTerm[], +} + +export class Script extends Command { + async attemptParse(context: ParseContext): Promise { + const data: ScriptData = { + path: await context.popTerm(), + args: [], + } + + while ( await context.peekTerm() ) { + data.args.push(await context.popTerm()) + } + + return data + } + + getDisplayName(): string { + return 'script' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'script') + } + + async execute(vm: StrVM, data: ScriptData): Promise { + return vm.replaceContextMatchingTerm(ctx => ({ + override: async sub => { + const path = processPath(ctx.resolveString(data.path)) + const args = data.args.map(arg => ctx.resolveString(arg)) + + const { stdout, stderr } = await promisify(execFile)(path, [unwrapString(sub), ...args]) + return wrapString(stdout) + }, + })) + } +}