Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ability to provide JavaScript objects and streams as file descriptors for redirects #230

Merged
merged 18 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading