Skip to content

Commit 0bca5be

Browse files
authored
fix: escape spaces in cmd scripts too (#84)
1 parent b9752e3 commit 0bca5be

File tree

4 files changed

+82
-58
lines changed

4 files changed

+82
-58
lines changed

Diff for: lib/escape.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ const cmd = (input, doubleEscape) => {
3636
}
3737

3838
// and finally, prefix shell meta chars with a ^
39-
result = result.replace(/[!^&()<>|"]/g, '^$&')
39+
result = result.replace(/[ !^&()<>|"]/g, '^$&')
4040
if (doubleEscape) {
41-
result = result.replace(/[!^&()<>|"]/g, '^$&')
41+
result = result.replace(/[ !^&()<>|"]/g, '^$&')
4242
}
4343

4444
// except for % which is escaped with another %, and only once

Diff for: lib/make-spawn-args.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ const makeSpawnArgs = options => {
8383
if (!isCmd) {
8484
chmod(scriptFile, '0775')
8585
}
86-
const spawnArgs = isCmd ? ['/d', '/s', '/c', scriptFile] : ['-c', scriptFile]
86+
const spawnArgs = isCmd
87+
? ['/d', '/s', '/c', escape.cmd(scriptFile)]
88+
: ['-c', escape.sh(scriptFile)]
8789

8890
const spawnOpts = {
8991
env: spawnEnv,

Diff for: test/escape.js

+20-18
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,24 @@ t.test('cmd', (t) => {
6262
['\\%PATH%', '\\%%PATH%%', true],
6363
['--arg="%PATH%"', '^"--arg=\\^"%%PATH%%\\^"^"'],
6464
['--arg="%PATH%"', '^^^"--arg=\\^^^"%%PATH%%\\^^^"^^^"', true],
65-
['--arg=npm exec -c "%PATH%"', '^"--arg=npm exec -c \\^"%%PATH%%\\^"^"'],
66-
['--arg=npm exec -c "%PATH%"', '^^^"--arg=npm exec -c \\^^^"%%PATH%%\\^^^"^^^"', true],
67-
[`--arg=npm exec -c '%PATH%'`, `^"--arg=npm exec -c '%%PATH%%'^"`],
68-
[`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm exec -c '%%PATH%%'^^^"`, true],
69-
[`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm exec -c \\^"%%PATH%%\\^"'^"`],
70-
[`'--arg=npm exec -c "%PATH%"'`, `^^^"'--arg=npm exec -c \\^^^"%%PATH%%\\^^^"'^^^"`, true],
71-
['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program Files\\test.bat\\^"^"'],
72-
['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program Files\\test.bat\\^^^"^^^"', true],
73-
['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program Files\\test%%.bat\\^"^"'],
74-
['"C:\\Program Files\\test%.bat"', '^^^"\\^^^"C:\\Program Files\\test%%.bat\\^^^"^^^"', true],
75-
['% % %', '^"%% %% %%^"'],
76-
['% % %', '^^^"%% %% %%^^^"', true],
65+
['--arg=npm exec -c "%PATH%"', '^"--arg=npm^ exec^ -c^ \\^"%%PATH%%\\^"^"'],
66+
['--arg=npm exec -c "%PATH%"', '^^^"--arg=npm^^^ exec^^^ -c^^^ \\^^^"%%PATH%%\\^^^"^^^"', true],
67+
[`--arg=npm exec -c '%PATH%'`, `^"--arg=npm^ exec^ -c^ '%%PATH%%'^"`],
68+
[`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm^^^ exec^^^ -c^^^ '%%PATH%%'^^^"`, true],
69+
[`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm^ exec^ -c^ \\^"%%PATH%%\\^"'^"`],
70+
[`'--arg=npm exec -c "%PATH%"'`,
71+
`^^^"'--arg=npm^^^ exec^^^ -c^^^ \\^^^"%%PATH%%\\^^^"'^^^"`, true],
72+
['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program^ Files\\test.bat\\^"^"'],
73+
['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program^^^ Files\\test.bat\\^^^"^^^"', true],
74+
['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program^ Files\\test%%.bat\\^"^"'],
75+
['"C:\\Program Files\\test%.bat"',
76+
'^^^"\\^^^"C:\\Program^^^ Files\\test%%.bat\\^^^"^^^"', true],
77+
['% % %', '^"%%^ %%^ %%^"'],
78+
['% % %', '^^^"%%^^^ %%^^^ %%^^^"', true],
7779
['hello^^^^^^', 'hello^^^^^^^^^^^^'],
7880
['hello^^^^^^', 'hello^^^^^^^^^^^^^^^^^^^^^^^^', true],
79-
['hello world', '^"hello world^"'],
80-
['hello world', '^^^"hello world^^^"', true],
81+
['hello world', '^"hello^ world^"'],
82+
['hello world', '^^^"hello^^^ world^^^"', true],
8183
['hello"world', '^"hello\\^"world^"'],
8284
['hello"world', '^^^"hello\\^^^"world^^^"', true],
8385
['hello""world', '^"hello\\^"\\^"world^"'],
@@ -90,10 +92,10 @@ t.test('cmd', (t) => {
9092
['hello\\"world', '^^^"hello\\\\\\^^^"world^^^"', true],
9193
['hello\\\\"world', '^"hello\\\\\\\\\\^"world^"'],
9294
['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', true],
93-
['hello world\\', '^"hello world\\\\^"'],
94-
['hello world\\', '^^^"hello world\\\\^^^"', true],
95-
['hello %PATH%', '^"hello %%PATH%%^"'],
96-
['hello %PATH%', '^^^"hello %%PATH%%^^^"', true],
95+
['hello world\\', '^"hello^ world\\\\^"'],
96+
['hello world\\', '^^^"hello^^^ world\\\\^^^"', true],
97+
['hello %PATH%', '^"hello^ %%PATH%%^"'],
98+
['hello %PATH%', '^^^"hello^^^ %%PATH%%^^^"', true],
9799
]
98100

99101
for (const [input, expectation, double] of expectations) {

Diff for: test/make-spawn-args.js

+57-37
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,18 @@ const which = {
2323
}
2424

2525
const path = require('path')
26-
const tmpdir = path.resolve(t.testdir())
26+
// we make our fake temp dir contain spaces for extra safety in paths with spaces
27+
const tmpdir = path.resolve(t.testdir({ 'with spaces': {} }), 'with spaces')
28+
29+
// used for unescaping windows path to script file
30+
const unescapeCmd = (input) => input
31+
.replace(/^\^"/, '')
32+
.replace(/\^"$/, '')
33+
.replace(/\^(.)/g, '$1')
34+
35+
const unescapeSh = (input) => input
36+
.replace(/^'/, '')
37+
.replace(/'$/, '')
2738

2839
const makeSpawnArgs = requireInject('../lib/make-spawn-args.js', {
2940
fs: {
@@ -68,7 +79,7 @@ if (isWindows) {
6879
cmd: 'script "quoted parameter"; second command',
6980
})
7081
t.equal(shell, 'cmd', 'default shell applies')
71-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
82+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
7283
t.match(opts, {
7384
env: {
7485
npm_package_json: /package\.json$/,
@@ -81,11 +92,12 @@ if (isWindows) {
8192
windowsVerbatimArguments: true,
8293
}, 'got expected options')
8394

84-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
95+
const filename = unescapeCmd(args[args.length - 1])
96+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
8597
t.equal(contents, `@echo off\nscript "quoted parameter"; second command`)
86-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
98+
t.ok(fs.existsSync(filename), 'script file was written')
8799
cleanup()
88-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
100+
t.not(fs.existsSync(filename), 'cleanup removes script file')
89101

90102
t.end()
91103
})
@@ -103,7 +115,7 @@ if (isWindows) {
103115
cmd: 'script "quoted parameter"; second command',
104116
})
105117
t.equal(shell, 'blrorp', 'used ComSpec as default shell')
106-
t.match(args, ['-c', /\.sh$/], 'got expected args')
118+
t.match(args, ['-c', /\.sh'$/], 'got expected args')
107119
t.match(opts, {
108120
env: {
109121
npm_package_json: /package\.json$/,
@@ -115,9 +127,10 @@ if (isWindows) {
115127
windowsVerbatimArguments: undefined,
116128
}, 'got expected options')
117129

118-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
130+
const filename = unescapeSh(args[args.length - 1])
131+
t.ok(fs.existsSync(filename), 'script file was written')
119132
cleanup()
120-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
133+
t.not(fs.existsSync(filename), 'cleanup removes script file')
121134

122135
t.end()
123136
})
@@ -131,7 +144,7 @@ if (isWindows) {
131144
scriptShell: 'cmd.exe',
132145
})
133146
t.equal(shell, 'cmd.exe', 'kept cmd.exe')
134-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
147+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
135148
t.match(opts, {
136149
env: {
137150
npm_package_json: /package\.json$/,
@@ -143,9 +156,10 @@ if (isWindows) {
143156
windowsVerbatimArguments: true,
144157
}, 'got expected options')
145158

146-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
159+
const filename = unescapeCmd(args[args.length - 1])
160+
t.ok(fs.existsSync(filename), 'script file was written')
147161
cleanup()
148-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
162+
t.not(fs.existsSync(filename), 'cleanup removes script file')
149163

150164
t.end()
151165
})
@@ -161,7 +175,7 @@ if (isWindows) {
161175
args: ['"quoted parameter";', 'second command'],
162176
})
163177
t.equal(shell, 'cmd', 'default shell applies')
164-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
178+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
165179
t.match(opts, {
166180
env: {
167181
npm_package_json: /package\.json$/,
@@ -174,11 +188,12 @@ if (isWindows) {
174188
windowsVerbatimArguments: true,
175189
}, 'got expected options')
176190

177-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
178-
t.equal(contents, `@echo off\nscript ^"\\^"quoted parameter\\^";^" ^"second command^"`)
179-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
191+
const filename = unescapeCmd(args[args.length - 1])
192+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
193+
t.equal(contents, `@echo off\nscript ^"\\^"quoted^ parameter\\^";^" ^"second^ command^"`)
194+
t.ok(fs.existsSync(filename), 'script file was written')
180195
cleanup()
181-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
196+
t.not(fs.existsSync(filename), 'cleanup removes script file')
182197

183198
t.end()
184199
})
@@ -194,7 +209,7 @@ if (isWindows) {
194209
args: ['"quoted parameter";', 'second command'],
195210
})
196211
t.equal(shell, 'cmd', 'default shell applies')
197-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
212+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
198213
t.match(opts, {
199214
env: {
200215
npm_package_json: /package\.json$/,
@@ -207,14 +222,15 @@ if (isWindows) {
207222
windowsVerbatimArguments: true,
208223
}, 'got expected options')
209224

210-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
225+
const filename = unescapeCmd(args[args.length - 1])
226+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
211227
t.equal(contents, [
212228
'@echo off',
213-
`script ^^^"\\^^^"quoted parameter\\^^^";^^^" ^^^"second command^^^"`,
229+
`script ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"`,
214230
].join('\n'))
215-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
231+
t.ok(fs.existsSync(filename), 'script file was written')
216232
cleanup()
217-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
233+
t.not(fs.existsSync(filename), 'cleanup removes script file')
218234

219235
t.end()
220236
})
@@ -232,7 +248,7 @@ if (isWindows) {
232248
args: ['"quoted parameter";', 'second command'],
233249
})
234250
t.equal(shell, 'cmd', 'default shell applies')
235-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
251+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
236252
t.match(opts, {
237253
env: {
238254
npm_package_json: /package\.json$/,
@@ -245,15 +261,16 @@ if (isWindows) {
245261
windowsVerbatimArguments: true,
246262
}, 'got expected options')
247263

248-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
264+
const filename = unescapeCmd(args[args.length - 1])
265+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
249266
t.equal(contents, [
250267
'@echo off',
251268
// eslint-disable-next-line max-len
252-
`"my script" ^^^"\\^^^"quoted parameter\\^^^";^^^" ^^^"second command^^^"`,
269+
`"my script" ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"`,
253270
].join('\n'))
254-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
271+
t.ok(fs.existsSync(filename), 'script file was written')
255272
cleanup()
256-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
273+
t.not(fs.existsSync(filename), 'cleanup removes script file')
257274

258275
t.end()
259276
})
@@ -275,7 +292,7 @@ if (isWindows) {
275292
args: ['"quoted parameter";', 'second command'],
276293
})
277294
t.equal(shell, 'sh', 'defaults to sh')
278-
t.match(args, ['-c', /\.sh$/], 'got expected args')
295+
t.match(args, ['-c', /\.sh'$/], 'got expected args')
279296
t.match(opts, {
280297
env: {
281298
npm_package_json: /package\.json$/,
@@ -287,11 +304,12 @@ if (isWindows) {
287304
windowsVerbatimArguments: undefined,
288305
}, 'got expected options')
289306

290-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
307+
const filename = unescapeSh(args[args.length - 1])
308+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
291309
t.equal(contents, `#!/usr/bin/env sh\nscript '"quoted parameter";' 'second command'`)
292-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
310+
t.ok(fs.existsSync(filename), 'script file was written')
293311
cleanup()
294-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
312+
t.not(fs.existsSync(filename), 'cleanup removes script file')
295313

296314
t.end()
297315
})
@@ -305,7 +323,7 @@ if (isWindows) {
305323
scriptShell: '/bin/sh',
306324
})
307325
t.equal(shell, '/bin/sh', 'kept provided setting')
308-
t.match(args, ['-c', /\.sh$/], 'got expected args')
326+
t.match(args, ['-c', /\.sh'$/], 'got expected args')
309327
t.match(opts, {
310328
env: {
311329
npm_package_json: /package\.json$/,
@@ -317,11 +335,12 @@ if (isWindows) {
317335
windowsVerbatimArguments: undefined,
318336
}, 'got expected options')
319337

320-
const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
338+
const filename = unescapeSh(args[args.length - 1])
339+
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
321340
t.equal(contents, `#!/bin/sh\nscript '"quoted parameter";' 'second command'`)
322-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
341+
t.ok(fs.existsSync(filename), 'script file was written')
323342
cleanup()
324-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
343+
t.not(fs.existsSync(filename), 'cleanup removes script file')
325344

326345
t.end()
327346
})
@@ -336,7 +355,7 @@ if (isWindows) {
336355
scriptShell: 'cmd.exe',
337356
})
338357
t.equal(shell, 'cmd.exe', 'kept cmd.exe')
339-
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
358+
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
340359
t.match(opts, {
341360
env: {
342361
npm_package_json: /package\.json$/,
@@ -348,9 +367,10 @@ if (isWindows) {
348367
windowsVerbatimArguments: true,
349368
}, 'got expected options')
350369

351-
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
370+
const filename = unescapeCmd(args[args.length - 1])
371+
t.ok(fs.existsSync(filename), 'script file was written')
352372
cleanup()
353-
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
373+
t.not(fs.existsSync(filename), 'cleanup removes script file')
354374

355375
t.end()
356376
})

0 commit comments

Comments
 (0)