#!/usr/bin/env -S deno run --allow-run export interface ShellSpawnerOpts extends Omit { shell: string prefix?: string shellArgs: (commandStr: string) => string[] autospawn: boolean, } /** * Shell spawner. * ``` * const $: ShellSpawner = koopa() * await $`rm -rf /home/*` * ``` */ export type ShellSpawner = (a: string | TemplateStringsArray, ...values: unknown[]) => R export class Troopa implements PromiseLike { cmd: Deno.Command #process?: Deno.ChildProcess #writer?: WritableStreamDefaultWriter constructor( public cmdStr: string, public opts: ShellSpawnerOpts ) { this.cmd = new Deno.Command(this.opts.shell, { args: this.opts.shellArgs(this.cmdStr), stdin: 'inherit', stdout: 'piped', stderr: 'piped', ...opts }) } /** * Starts the process, if not yet started. * @author MindfulMinun * @since 2023-12-26 */ start(): Deno.ChildProcess { return this.process } get process(): Deno.ChildProcess { if (this.#process) return this.#process this.#process = this.cmd.spawn() return this.#process } get stdin(): WritableStream { return this.process.stdin } get stdout(): ReadableStream { return this.process.stdout } get stderr(): ReadableStream { return this.process.stderr } response(src: 'stdout' | 'stderr' = 'stdout'): Response { const source = this.process[src] return new Response(source) } async code(): Promise { return (await this).code } private get writer() { if (!this.#writer) { this.#writer = this.stdin.getWriter() } return this.#writer } write(input: string): Promise write(input: Uint8Array): Promise write(input: Blob): Promise async write(input: string | Uint8Array | Blob) { let encoded: Uint8Array switch (true) { case input instanceof Uint8Array: encoded = input break case input instanceof Blob: encoded = new Uint8Array(await input.arrayBuffer()) break default: encoded = new TextEncoder().encode(input) } await this.writer.write(encoded) return this } async close(): Promise { await this.writer.close() return this } // -- pipe(into: Troopa): typeof into pipe(into: WritableStream): typeof this pipe(into: Troopa | WritableStream): Troopa { if (into instanceof Troopa) { this.process.stdout.pipeTo(into.process.stdin) return into } this.process.stdout.pipeTo(into) return this } then( onfulfilled?: ((a: Deno.CommandStatus) => Resolved | PromiseLike), onrejected?: ((a: unknown) => Rejected | PromiseLike) ): PromiseLike { return this.process.status.then(onfulfilled, onrejected) } } /** * Run a shell script. * Factory facade to easily interact and run shell scripts * @since 2023-11-25 */ export default function koopa(opts: Partial = {}): ShellSpawner { const actualOpts: ShellSpawnerOpts = { shell: 'bash', shellArgs: cmd => ['-c', `${opts.prefix || 'set -euo pipefail'}; ${cmd}`], autospawn: true, ...opts } const factory: ShellSpawner = function ( templ: TemplateStringsArray | string, ...values: unknown[] ) { // FIXME: Implement actual escaping const escape = (a: string) => a let execStr = '' if (typeof templ === 'string') { execStr += templ } else { for (let i = 0; i < templ.length; i++) { execStr += templ[i] execStr += typeof values[i] !== 'undefined' ? escape(`${values[i]}`) : '' } } return new Troopa(execStr, actualOpts) } return factory } if (import.meta.main) { const $ = koopa({ prefix: 'set -euox pipefail', }) const stdout = Deno.stdout.writable const x = await $`echo 1; sleep 1; echo 2; sleep 1; echo 3;` .pipe(stdout) console.log(`Exit Status: ${x.code}`) }