Skip to content

Commit 8dd93fb

Browse files
committed
feat: enhance shell command execution with options for stdout and stderr handling
1 parent 46af36f commit 8dd93fb

File tree

1 file changed

+53
-9
lines changed

1 file changed

+53
-9
lines changed

src/common/shell.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { isString } from "@/remeda";
22
import { Result } from "@/result";
33

44
import { PromiseWithResolvers } from "./promise";
5+
import { concatTemplateStrings } from "./string";
6+
7+
export interface ShellExecOptions {
8+
onStdout?: "ignore" | "print" | ((chunk: string) => void);
9+
onStderr?: "ignore" | "print" | ((chunk: string) => void);
10+
}
511

612
export interface ShellExecResult {
713
stdout: string;
@@ -12,26 +18,64 @@ const REGEXP_NULL_CHAR = /\x00+/g;
1218
const REGEXP_SAFE_CHARS = /^[A-Za-z0-9,:=_./-]+$/;
1319
const REGEXP_SINGLE_QUOTES = /'+/g;
1420

15-
export async function $(cmd: string): Promise<Result<ShellExecResult, Error>>;
21+
const noop = () => {};
22+
const pipeToStdout = (chunk: string) => process.stdout.write(chunk);
23+
const pipeToStderr = (chunk: string) => process.stderr.write(chunk);
24+
25+
export async function $(
26+
cmd: string,
27+
options?: ShellExecOptions,
28+
): Promise<Result<ShellExecResult, Error>>;
1629
export async function $(
1730
cmd: TemplateStringsArray,
1831
...values: any[]
1932
): Promise<Result<ShellExecResult, Error>>;
2033
export async function $(cmd: string | TemplateStringsArray, ...values: any[]) {
21-
const { exec } = await import("node:child_process");
34+
const { spawn } = await import("node:child_process");
2235

23-
const command = isString(cmd)
24-
? cmd
25-
: cmd.reduce((acc, part, index) => acc + part + (values[index] ?? ""), "");
36+
const [command, options] = isString(cmd)
37+
? [cmd, (values[0] || {}) as ShellExecOptions]
38+
: [concatTemplateStrings(cmd, values), {}];
39+
const onStdout =
40+
options.onStdout === "ignore"
41+
? noop
42+
: options.onStdout === "print"
43+
? pipeToStdout
44+
: options.onStdout || noop;
45+
const onStderr =
46+
options.onStderr === "ignore"
47+
? noop
48+
: options.onStderr === "print"
49+
? pipeToStderr
50+
: options.onStderr || noop;
2651

2752
const fn = async () => {
2853
const { promise, reject, resolve } = PromiseWithResolvers<ShellExecResult>();
2954

30-
exec(command, (error, stdout, stderr) => {
31-
if (error) {
32-
reject(error);
33-
} else {
55+
const child = spawn(command, {
56+
shell: true,
57+
stdio: ["inherit", "pipe", "pipe"],
58+
});
59+
60+
let stdout = "";
61+
let stderr = "";
62+
child.stdout?.on("data", data => {
63+
const chunk = data.toString();
64+
stdout += chunk;
65+
onStdout(chunk);
66+
});
67+
child.stderr?.on("data", data => {
68+
const chunk = data.toString();
69+
stderr += chunk;
70+
onStderr(chunk);
71+
});
72+
73+
child.on("error", reject);
74+
child.on("close", code => {
75+
if (code === 0) {
3476
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
77+
} else {
78+
reject(new Error(`Command exited with code ${code}`));
3579
}
3680
});
3781

0 commit comments

Comments
 (0)