1+ 'use strict'
2+
13const { spawn } = require ( 'child_process' )
4+ const os = require ( 'os' )
5+ const which = require ( 'which' )
26
3- const isPipe = ( stdio = 'pipe' , fd ) =>
4- stdio === 'pipe' || stdio === null ? true
5- : Array . isArray ( stdio ) ? isPipe ( stdio [ fd ] , fd )
6- : false
7+ const escape = require ( './escape.js' )
78
89// 'extra' object is for decorating the error a bit more
910const promiseSpawn = ( cmd , args , opts = { } , extra = { } ) => {
11+ if ( opts . shell ) {
12+ return spawnWithShell ( cmd , args , opts , extra )
13+ }
14+
1015 let proc
16+
1117 const p = new Promise ( ( res , rej ) => {
1218 proc = spawn ( cmd , args , opts )
19+
1320 const stdout = [ ]
1421 const stderr = [ ]
22+
1523 const reject = er => rej ( Object . assign ( er , {
1624 cmd,
1725 args,
1826 ...stdioResult ( stdout , stderr , opts ) ,
1927 ...extra ,
2028 } ) )
29+
2130 proc . on ( 'error' , reject )
31+
2232 if ( proc . stdout ) {
2333 proc . stdout . on ( 'data' , c => stdout . push ( c ) ) . on ( 'error' , reject )
2434 proc . stdout . on ( 'error' , er => reject ( er ) )
2535 }
36+
2637 if ( proc . stderr ) {
2738 proc . stderr . on ( 'data' , c => stderr . push ( c ) ) . on ( 'error' , reject )
2839 proc . stderr . on ( 'error' , er => reject ( er ) )
2940 }
41+
3042 proc . on ( 'close' , ( code , signal ) => {
3143 const result = {
3244 cmd,
@@ -36,6 +48,7 @@ const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
3648 ...stdioResult ( stdout , stderr , opts ) ,
3749 ...extra ,
3850 }
51+
3952 if ( code || signal ) {
4053 rej ( Object . assign ( new Error ( 'command failed' ) , result ) )
4154 } else {
@@ -49,14 +62,134 @@ const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
4962 return p
5063}
5164
52- const stdioResult = ( stdout , stderr , { stdioString, stdio } ) =>
53- stdioString ? {
54- stdout : isPipe ( stdio , 1 ) ? Buffer . concat ( stdout ) . toString ( ) . trim ( ) : null ,
55- stderr : isPipe ( stdio , 2 ) ? Buffer . concat ( stderr ) . toString ( ) . trim ( ) : null ,
65+ const spawnWithShell = ( cmd , args , opts , extra ) => {
66+ let command = opts . shell
67+ // if shell is set to true, we use a platform default. we can't let the core
68+ // spawn method decide this for us because we need to know what shell is in use
69+ // ahead of time so that we can escape arguments properly. we don't need coverage here.
70+ if ( command === true ) {
71+ // istanbul ignore next
72+ command = process . platform === 'win32' ? process . env . ComSpec : 'sh'
5673 }
57- : {
58- stdout : isPipe ( stdio , 1 ) ? Buffer . concat ( stdout ) : null ,
59- stderr : isPipe ( stdio , 2 ) ? Buffer . concat ( stderr ) : null ,
74+
75+ const options = { ...opts , shell : false }
76+ const realArgs = [ ]
77+ let script = cmd
78+
79+ // first, determine if we're in windows because if we are we need to know if we're
80+ // running an .exe or a .cmd/.bat since the latter requires extra escaping
81+ const isCmd = / (?: ^ | \\ ) c m d (?: \. e x e ) ? $ / i. test ( command )
82+ if ( isCmd ) {
83+ let doubleEscape = false
84+
85+ // find the actual command we're running
86+ let initialCmd = ''
87+ let insideQuotes = false
88+ for ( let i = 0 ; i < cmd . length ; ++ i ) {
89+ const char = cmd . charAt ( i )
90+ if ( char === ' ' && ! insideQuotes ) {
91+ break
92+ }
93+
94+ initialCmd += char
95+ if ( char === '"' || char === "'" ) {
96+ insideQuotes = ! insideQuotes
97+ }
98+ }
99+
100+ let pathToInitial
101+ try {
102+ pathToInitial = which . sync ( initialCmd , {
103+ path : ( options . env && options . env . PATH ) || process . env . PATH ,
104+ pathext : ( options . env && options . env . PATHEXT ) || process . env . PATHEXT ,
105+ } ) . toLowerCase ( )
106+ } catch ( err ) {
107+ pathToInitial = initialCmd . toLowerCase ( )
108+ }
109+
110+ doubleEscape = pathToInitial . endsWith ( '.cmd' ) || pathToInitial . endsWith ( '.bat' )
111+ for ( const arg of args ) {
112+ script += ` ${ escape . cmd ( arg , doubleEscape ) } `
113+ }
114+ realArgs . push ( '/d' , '/s' , '/c' , script )
115+ options . windowsVerbatimArguments = true
116+ } else {
117+ for ( const arg of args ) {
118+ script += ` ${ escape . sh ( arg ) } `
119+ }
120+ realArgs . push ( '-c' , script )
60121 }
61122
123+ return promiseSpawn ( command , realArgs , options , extra )
124+ }
125+
126+ // open a file with the default application as defined by the user's OS
127+ const open = ( _args , opts = { } , extra = { } ) => {
128+ const options = { ...opts , shell : true }
129+ const args = [ ] . concat ( _args )
130+
131+ let platform = process . platform
132+ // process.platform === 'linux' may actually indicate WSL, if that's the case
133+ // we want to treat things as win32 anyway so the host can open the argument
134+ if ( platform === 'linux' && os . release ( ) . includes ( 'Microsoft' ) ) {
135+ platform = 'win32'
136+ }
137+
138+ let command = options . command
139+ if ( ! command ) {
140+ if ( platform === 'win32' ) {
141+ // spawnWithShell does not do the additional os.release() check, so we
142+ // have to force the shell here to make sure we treat WSL as windows.
143+ options . shell = process . env . ComSpec
144+ // also, the start command accepts a title so to make sure that we don't
145+ // accidentally interpret the first arg as the title, we stick an empty
146+ // string immediately after the start command
147+ command = 'start ""'
148+ } else if ( platform === 'darwin' ) {
149+ command = 'open'
150+ } else {
151+ command = 'xdg-open'
152+ }
153+ }
154+
155+ return spawnWithShell ( command , args , options , extra )
156+ }
157+ promiseSpawn . open = open
158+
159+ const isPipe = ( stdio = 'pipe' , fd ) => {
160+ if ( stdio === 'pipe' || stdio === null ) {
161+ return true
162+ }
163+
164+ if ( Array . isArray ( stdio ) ) {
165+ return isPipe ( stdio [ fd ] , fd )
166+ }
167+
168+ return false
169+ }
170+
171+ const stdioResult = ( stdout , stderr , { stdioString = true , stdio } ) => {
172+ const result = {
173+ stdout : null ,
174+ stderr : null ,
175+ }
176+
177+ // stdio is [stdin, stdout, stderr]
178+ if ( isPipe ( stdio , 1 ) ) {
179+ result . stdout = Buffer . concat ( stdout )
180+ if ( stdioString ) {
181+ result . stdout = result . stdout . toString ( ) . trim ( )
182+ }
183+ }
184+
185+ if ( isPipe ( stdio , 2 ) ) {
186+ result . stderr = Buffer . concat ( stderr )
187+ if ( stdioString ) {
188+ result . stderr = result . stderr . toString ( ) . trim ( )
189+ }
190+ }
191+
192+ return result
193+ }
194+
62195module . exports = promiseSpawn
0 commit comments