diff --git a/mod.test.ts b/mod.test.ts index 09f669f..38b219b 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -192,6 +192,12 @@ Deno.test("should throw when exit code is non-zero", async () => { ); }); +Deno.test("should error in the shell when the command can't be found", async () => { + const output = await $`nonexistentcommanddaxtest`.noThrow().stderr("piped"); + assertEquals(output.code, 127); + assertEquals(output.stderr, "dax: nonexistentcommanddaxtest: command not found\n"); +}); + Deno.test("throws when providing an object that doesn't override toString", async () => { { const obj1 = {}; @@ -263,6 +269,11 @@ Deno.test("CommandBuilder#json()", async () => { assertEquals(output, { test: 5 }); }); +Deno.test("CommandBuilder#json('stderr')", async () => { + const output = await $`deno eval "console.error(JSON.stringify({ test: 5 }));"`.json("stderr"); + assertEquals(output, { test: 5 }); +}); + Deno.test("stderrJson", async () => { const output = await $`deno eval "console.error(JSON.stringify({ test: 5 }));"`.stderr("piped"); assertEquals(output.stderrJson, { test: 5 }); @@ -490,11 +501,9 @@ Deno.test("should not allow invalid command names", () => { Deno.test("should unregister commands", async () => { const builder = new CommandBuilder().unregisterCommand("export").noThrow(); - await assertRejects( - async () => await builder.command("export somewhere"), - Error, - "Command not found: export", - ); + const output = await builder.command("export somewhere").stderr("piped"); + assertEquals(output.code, 127); + assertEquals(output.stderr, "dax: export: command not found\n"); }); Deno.test("sleep command", async () => { @@ -1124,6 +1133,16 @@ Deno.test("command .lines()", async () => { assertEquals(result, ["1", "2"]); }); +Deno.test("command .lines('stderr')", async () => { + const result = await $`deno eval "console.error(1); console.error(2)"`.lines("stderr"); + assertEquals(result, ["1", "2"]); +}); + +Deno.test("command .lines('combined')", async () => { + const result = await $`deno eval "console.log(1); console.error(2)"`.lines("combined"); + assertEquals(result, ["1", "2"]); +}); + Deno.test("piping in command", async () => { await withTempDir(async (tempDir) => { const result = await $`echo 1 | cat - > output.txt`.cwd(tempDir).text(); @@ -1438,7 +1457,7 @@ Deno.test("shebang support", async (t) => { .text(); }, Error, - "Command not found: deno run", + "Exited with code: 127", ); }); diff --git a/src/command.ts b/src/command.ts index e066b5b..de1c9a2 100644 --- a/src/command.ts +++ b/src/command.ts @@ -38,6 +38,7 @@ import { symbols } from "./common.ts"; import { whichCommand } from "./commands/which.ts"; type BufferStdio = "inherit" | "null" | "streamed" | Buffer; +type StreamKind = "stdout" | "stderr" | "combined"; class Deferred { #create: () => T | Promise; @@ -492,12 +493,13 @@ export class CommandBuilder implements PromiseLike { * await $`echo 1`.quiet("stderr"); * ``` */ - quiet(kind: "stdout" | "stderr" | "both" = "both"): CommandBuilder { + quiet(kind: StreamKind | "both" = "combined"): CommandBuilder { + kind = kind === "both" ? "combined" : kind; return this.#newWithState((state) => { - if (kind === "both" || kind === "stdout") { + if (kind === "combined" || kind === "stdout") { state.stdout.kind = getQuietKind(state.stdout.kind); } - if (kind === "both" || kind === "stderr") { + if (kind === "combined" || kind === "stderr") { state.stderr.kind = getQuietKind(state.stderr.kind); } }); @@ -543,12 +545,14 @@ export class CommandBuilder implements PromiseLike { * const data = (await $`command`.quiet("stdout")).stdoutBytes; * ``` */ - async bytes(): Promise { - return (await this.quiet("stdout")).stdoutBytes; + async bytes(kind: StreamKind): Promise { + const command = kind === "combined" ? this.quiet(kind).captureCombined() : this.quiet(kind); + return (await command)[`${kind}Bytes`]; } /** - * Sets stdout as quiet, spawns the command, and gets stdout as a string without the last newline. + * Sets the provided stream (stdout by default) as quiet, spawns the command, and gets the stream as a string without the last newline. + * Can be used to get stdout, stderr, or both. * * Shorthand for: * @@ -556,18 +560,19 @@ export class CommandBuilder implements PromiseLike { * const data = (await $`command`.quiet("stdout")).stdout.replace(/\r?\n$/, ""); * ``` */ - async text(): Promise { - return (await this.quiet("stdout")).stdout.replace(/\r?\n$/, ""); + async text(kind: StreamKind = "stdout"): Promise { + const command = kind === "combined" ? this.quiet(kind).captureCombined() : this.quiet(kind); + return (await command)[kind].replace(/\r?\n$/, ""); } /** Gets the text as an array of lines. */ - async lines(): Promise { - const text = await this.text(); + async lines(kind: StreamKind = "stdout"): Promise { + const text = await this.text(kind); return text.split(/\r?\n/g); } /** - * Sets stdout as quiet, spawns the command, and gets stdout as JSON. + * Sets stream (stdout by default) as quiet, spawns the command, and gets stream as JSON. * * Shorthand for: * @@ -575,8 +580,8 @@ export class CommandBuilder implements PromiseLike { * const data = (await $`command`.quiet("stdout")).stdoutJson; * ``` */ - async json(): Promise { - return (await this.quiet("stdout")).stdoutJson; + async json(kind: Exclude = "stdout"): Promise { + return (await this.quiet(kind))[`${kind}Json`]; } /** @internal */ diff --git a/src/shell.ts b/src/shell.ts index ffa72fc..ff91a26 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -928,6 +928,10 @@ async function executeUnresolvedCommand( context: Context, ): Promise { const resolvedCommand = await resolveCommand(unresolvedCommand, context); + if (resolvedCommand === false) { + context.stderr.writeLine(`dax: ${unresolvedCommand.name}: command not found`); + return { code: 127 }; + } if (resolvedCommand.kind === "shebang") { return executeUnresolvedCommand(resolvedCommand.command, [...resolvedCommand.args, ...commandArgs], context); } @@ -1113,7 +1117,7 @@ function pipeCommandPipeReaderToWriterSync( } } -type ResolvedCommand = ResolvedPathCommand | ResolvedShebangCommand; +type ResolvedCommand = ResolvedPathCommand | ResolvedShebangCommand | false; interface ResolvedPathCommand { kind: "path"; @@ -1141,7 +1145,7 @@ async function resolveCommand(unresolvedCommand: UnresolvedCommand, context: Con // won't have a script with a shebang in it on Windows const result = await getExecutableShebangFromPath(commandPath); if (result === false) { - throw new Error(`Command not found: ${unresolvedCommand.name}`); + return false; } else if (result != null) { const args = await parseShebangArgs(result, context); const name = args.shift()!; @@ -1165,7 +1169,7 @@ async function resolveCommand(unresolvedCommand: UnresolvedCommand, context: Con const commandPath = await whichFromContext(unresolvedCommand.name, context); if (commandPath == null) { - throw new Error(`Command not found: ${unresolvedCommand.name}`); + return false; } return { kind: "path",