1
+ 'use strict'
2
+
1
3
const { spawn } = require ( 'child_process' )
4
+ const os = require ( 'os' )
5
+ const which = require ( 'which' )
2
6
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' )
7
8
8
9
// 'extra' object is for decorating the error a bit more
9
10
const promiseSpawn = ( cmd , args , opts = { } , extra = { } ) => {
11
+ if ( opts . shell ) {
12
+ return spawnWithShell ( cmd , args , opts , extra )
13
+ }
14
+
10
15
let proc
16
+
11
17
const p = new Promise ( ( res , rej ) => {
12
18
proc = spawn ( cmd , args , opts )
19
+
13
20
const stdout = [ ]
14
21
const stderr = [ ]
22
+
15
23
const reject = er => rej ( Object . assign ( er , {
16
24
cmd,
17
25
args,
18
26
...stdioResult ( stdout , stderr , opts ) ,
19
27
...extra ,
20
28
} ) )
29
+
21
30
proc . on ( 'error' , reject )
31
+
22
32
if ( proc . stdout ) {
23
33
proc . stdout . on ( 'data' , c => stdout . push ( c ) ) . on ( 'error' , reject )
24
34
proc . stdout . on ( 'error' , er => reject ( er ) )
25
35
}
36
+
26
37
if ( proc . stderr ) {
27
38
proc . stderr . on ( 'data' , c => stderr . push ( c ) ) . on ( 'error' , reject )
28
39
proc . stderr . on ( 'error' , er => reject ( er ) )
29
40
}
41
+
30
42
proc . on ( 'close' , ( code , signal ) => {
31
43
const result = {
32
44
cmd,
@@ -36,6 +48,7 @@ const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
36
48
...stdioResult ( stdout , stderr , opts ) ,
37
49
...extra ,
38
50
}
51
+
39
52
if ( code || signal ) {
40
53
rej ( Object . assign ( new Error ( 'command failed' ) , result ) )
41
54
} else {
@@ -49,14 +62,134 @@ const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
49
62
return p
50
63
}
51
64
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'
56
73
}
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 )
60
121
}
61
122
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
+
62
195
module . exports = promiseSpawn
0 commit comments