Skip to content

Commit

Permalink
feat: ability to provide JavaScript objects and streams as file descr…
Browse files Browse the repository at this point in the history
…iptors for redirects (#230)
  • Loading branch information
dsherret authored Jan 28, 2024
1 parent 962acdf commit 4d39bf3
Show file tree
Hide file tree
Showing 14 changed files with 3,905 additions and 3,300 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 125 additions & 2 deletions mod.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readAll } from "./src/deps.ts";
import { readAll, readerFromStreamReader } from "./src/deps.ts";
import $, { build$, CommandBuilder, CommandContext, CommandHandler, KillSignal, KillSignalController } from "./mod.ts";
import {
assert,
Expand All @@ -7,7 +7,7 @@ import {
assertRejects,
assertStringIncludes,
assertThrows,
readerFromStreamReader,
toWritableStream,
withTempDir,
} from "./src/deps.test.ts";
import { Buffer, colors, path } from "./src/deps.ts";
Expand Down Expand Up @@ -1148,6 +1148,14 @@ Deno.test("output redirects", async () => {
assertEquals(result.code, 1);
assert(result.stderr.startsWith("failed opening file for redirect"));
}

{
assertThrows(
() => $`echo 1 > ${new TextEncoder()}`,
Error,
"Failed resolving expression in command. Unsupported object provided to output redirect.",
);
}
});
});

Expand All @@ -1159,6 +1167,121 @@ Deno.test("input redirects", async () => {
});
});

Deno.test("input redirects with provided object", async () => {
{
assertThrows(
() => $`cat - < ${new TextEncoder()} && echo ${"test"}`,
Error,
"Failed resolving expression 1/2 in command. Unsupported object provided to input redirect.",
);
}
// stream
{
const text = "testing".repeat(1000);
const bytes = new TextEncoder().encode(text);
const stream = new ReadableStream({
start(controller) {
controller.enqueue(bytes);
controller.close();
},
});
const output = await $`cat - < ${stream}`.text();
assertEquals(output, text);
}
// bytes
{
const text = "testing".repeat(1000);
const bytes = new TextEncoder().encode(text);
const output = await $`cat - < ${bytes}`.text();
assertEquals(output, text);
}
// response
{
const text = "testing".repeat(1000);
const response = new Response(text);
const output = await $`cat - < ${response}`.text();
assertEquals(output, text);
}
// file
await withTempDir(async (tempDir) => {
const text = "testing".repeat(1000);
const filePath = tempDir.join("file.txt");
filePath.writeTextSync(text);
const file = filePath.openSync({ read: true });
const output = await $`cat - < ${file}`.text();
assertEquals(output, text);
});
// function
{
const text = "testing".repeat(1000);
const response = new Response(text);
const output = await $`cat - < ${() => response.body!}`.text();
assertEquals(output, text);
}
});

Deno.test("output redirect with provided object", async () => {
await withTempDir(async (tempDir) => {
const buffer = new Buffer();
const pipedText = "testing\nthis\nout".repeat(1_000);
tempDir.join("data.txt").writeTextSync(pipedText);
await $`cat data.txt > ${toWritableStream(buffer)}`.cwd(tempDir);
assertEquals(new TextDecoder().decode(buffer.bytes()), pipedText);
});
{
const chunks = [];
let wasClosed = false;
const writableStream = new WritableStream({
write(chunk) {
chunks.push(chunk);
},
close() {
wasClosed = true;
},
});
let didThrow = false;
try {
await $`echo 1 > ${writableStream} ; exit 1`;
} catch {
didThrow = true;
}
assert(didThrow);
assertEquals(chunks.length, 1);
assert(wasClosed);
}
{
const bytes = new Uint8Array(2);
await $`echo 1 > ${bytes}`;
assertEquals(new TextDecoder().decode(bytes), "1\n");
}
// overflow
{
const bytes = new Uint8Array(1);
const result = await $`echo 1 > ${bytes}`.noThrow().stderr("piped");
assertEquals(result.stderr, "echo: Overflow writing 2 bytes to Uint8Array (length: 1).\n");
assertEquals(result.code, 1);
assertEquals(bytes[0], 49);
}
// file
await withTempDir(async (tempDir) => {
const filePath = tempDir.join("file.txt");
const file = filePath.openSync({ write: true, create: true, truncate: true });
await $`echo testing > ${file}`;
assertEquals(filePath.readTextSync(), "testing\n");
});
// function
{
const chunks: Uint8Array[] = [];
const writableStream = new WritableStream({
write(chunk) {
chunks.push(chunk);
},
});
await $`echo 1 > ${() => writableStream}`;
assertEquals(chunks, [new Uint8Array([49, 10])]);
}
});

Deno.test("shebang support", async (t) => {
await withTempDir(async (dir) => {
const steps: Promise<boolean>[] = [];
Expand Down
19 changes: 16 additions & 3 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CommandBuilder, escapeArg, getRegisteredCommandNamesSymbol, template, templateRaw } from "./src/command.ts";
import {
CommandBuilder,
escapeArg,
getRegisteredCommandNamesSymbol,
setCommandTextAndFdsSymbol,
template,
templateRaw,
} from "./src/command.ts";
import {
Box,
Delay,
Expand All @@ -7,6 +14,7 @@ import {
delayToMs,
formatMillis,
LoggerTreeBox,
symbols,
TreeBox,
} from "./src/common.ts";
import {
Expand Down Expand Up @@ -468,6 +476,8 @@ export interface $BuiltInProperties<TExtras extends ExtrasObject = {}> {
* ```
*/
sleep(delay: Delay): Promise<void>;
/** Symbols that can be attached to objects for better integration with Dax. */
symbols: typeof symbols;
/**
* Executes the command as raw text without escaping expressions.
*
Expand Down Expand Up @@ -617,7 +627,8 @@ function build$FromState<TExtras extends ExtrasObject = {}>(state: $State<TExtra
};
const result = Object.assign(
(strings: TemplateStringsArray, ...exprs: any[]) => {
return state.commandBuilder.getValue().command(template(strings, exprs));
const { text, streams } = template(strings, exprs);
return state.commandBuilder.getValue()[setCommandTextAndFdsSymbol](text, streams);
},
helperObject,
logDepthObj,
Expand Down Expand Up @@ -741,11 +752,13 @@ function build$FromState<TExtras extends ExtrasObject = {}>(state: $State<TExtra
const commandBuilder = state.commandBuilder.getValue().printCommand(value);
state.commandBuilder.setValue(commandBuilder);
},
symbols,
request(url: string | URL) {
return state.requestBuilder.url(url);
},
raw(strings: TemplateStringsArray, ...exprs: any[]) {
return state.commandBuilder.getValue().command(templateRaw(strings, exprs));
const { text, streams } = templateRaw(strings, exprs);
return state.commandBuilder.getValue()[setCommandTextAndFdsSymbol](text, streams);
},
withRetries<TReturn>(opts: RetryOptions<TReturn>): Promise<TReturn> {
return withRetries(result, state.errorLogger.getValue(), opts);
Expand Down
Loading

0 comments on commit 4d39bf3

Please sign in to comment.