@@ -2,6 +2,12 @@ import { isString } from "@/remeda";
22import { Result } from "@/result" ;
33
44import { 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
612export interface ShellExecResult {
713 stdout : string ;
@@ -12,26 +18,64 @@ const REGEXP_NULL_CHAR = /\x00+/g;
1218const REGEXP_SAFE_CHARS = / ^ [ A - Z a - z 0 - 9 , : = _ . / - ] + $ / ;
1319const 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 > > ;
1629export async function $ (
1730 cmd : TemplateStringsArray ,
1831 ...values : any [ ]
1932) : Promise < Result < ShellExecResult , Error > > ;
2033export 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