From 3fefbdc3eca3e66b0af7926d5bac7a65173243f2 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 26 Jan 2024 13:20:41 -0500 Subject: [PATCH] feat: add `.pipe(...)` for piping stdout of a command to another command (#218) --- mod.test.ts | 14 ++++++++++++- mod.ts | 37 +++------------------------------ src/command.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/mod.test.ts b/mod.test.ts index 49ca397..a607949 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -797,7 +797,7 @@ Deno.test("piping to stdin", async () => { assertEquals(result, "1\n2"); } - // command that exists via stdin + // command that exits via stdin { const child = $`echo 1 && echo 2 && exit 1`; const result = await $`deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable);'` @@ -809,6 +809,18 @@ Deno.test("piping to stdin", async () => { } }); +Deno.test("pipe", async () => { + { + const result = await $`echo 1 && echo 2` + .pipe($`deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable);'`) + .stderr("piped") + .stdout("piped") + .spawn(); + assertEquals(result.stdout, ""); + assertEquals(result.stderr, "1\n2\n"); + } +}); + Deno.test("piping to a writable and the command fails", async () => { const chunks = []; let wasClosed = false; diff --git a/mod.ts b/mod.ts index f24117e..f3b3941 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1,4 @@ -import { CommandBuilder, CommandResult, escapeArg, getRegisteredCommandNamesSymbol } from "./src/command.ts"; +import { CommandBuilder, escapeArg, getRegisteredCommandNamesSymbol, template, templateRaw } from "./src/command.ts"; import { Box, Delay, @@ -610,16 +610,7 @@ function build$FromState(state: $State { - let result = ""; - for (let i = 0; i < Math.max(strings.length, exprs.length); i++) { - if (strings.length > i) { - result += strings[i]; - } - if (exprs.length > i) { - result += templateLiteralExprToString(exprs[i], escapeArg); - } - } - return state.commandBuilder.getValue().command(result); + return state.commandBuilder.getValue().command(template(strings, exprs)); }, helperObject, logDepthObj, @@ -747,16 +738,7 @@ function build$FromState(state: $State i) { - result += strings[i]; - } - if (exprs.length > i) { - result += templateLiteralExprToString(exprs[i]); - } - } - return state.commandBuilder.getValue().command(result); + return state.commandBuilder.getValue().command(templateRaw(strings, exprs)); }, withRetries(opts: RetryOptions): Promise { return withRetries(result, state.errorLogger.getValue(), opts); @@ -864,19 +846,6 @@ export function build$( })); } -function templateLiteralExprToString(expr: any, escape?: (arg: string) => string): string { - let result: string; - if (expr instanceof Array) { - return expr.map((e) => templateLiteralExprToString(e, escape)).join(" "); - } else if (expr instanceof CommandResult) { - // remove last newline - result = expr.stdout.replace(/\r?\n$/, ""); - } else { - result = `${expr}`; - } - return escape ? escape(result) : result; -} - /** * Default `$` instance where commands may be executed. */ diff --git a/src/command.ts b/src/command.ts index 409edfd..477d0b3 100644 --- a/src/command.ts +++ b/src/command.ts @@ -363,6 +363,22 @@ export class CommandBuilder implements PromiseLike { }); } + /** Pipes the current command to the provided command returning the + * provided command builder. When chaining, it's important to call this + * after you are done configuring the current command or else you will + * start modifying the provided command instead. + * + * @example + * ```ts + * const lineCount = await $`echo 1 && echo 2` + * .pipe($`wc -l`) + * .text(); + * ``` + */ + pipe(builder: CommandBuilder): CommandBuilder { + return builder.stdin(this.stdout("piped")); + } + /** Sets multiple environment variables to use at the same time via an object literal. */ env(items: Record): CommandBuilder; /** Sets a single environment variable to use. */ @@ -1166,3 +1182,42 @@ function signalCausesAbort(signal: Deno.Signal) { return false; } } + +export function template(strings: TemplateStringsArray, exprs: any[]) { + let result = ""; + for (let i = 0; i < Math.max(strings.length, exprs.length); i++) { + if (strings.length > i) { + result += strings[i]; + } + if (exprs.length > i) { + result += templateLiteralExprToString(exprs[i], escapeArg); + } + } + return result; +} + +export function templateRaw(strings: TemplateStringsArray, exprs: any[]) { + let result = ""; + for (let i = 0; i < Math.max(strings.length, exprs.length); i++) { + if (strings.length > i) { + result += strings[i]; + } + if (exprs.length > i) { + result += templateLiteralExprToString(exprs[i]); + } + } + return result; +} + +function templateLiteralExprToString(expr: any, escape?: (arg: string) => string): string { + let result: string; + if (expr instanceof Array) { + return expr.map((e) => templateLiteralExprToString(e, escape)).join(" "); + } else if (expr instanceof CommandResult) { + // remove last newline + result = expr.stdout.replace(/\r?\n$/, ""); + } else { + result = `${expr}`; + } + return escape ? escape(result) : result; +}