Skip to content

Commit

Permalink
fix: escape spaces in cmd scripts too (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
nlf authored Jun 23, 2022
1 parent b9752e3 commit 0bca5be
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 58 deletions.
4 changes: 2 additions & 2 deletions lib/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ const cmd = (input, doubleEscape) => {
}

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

// except for % which is escaped with another %, and only once
Expand Down
4 changes: 3 additions & 1 deletion lib/make-spawn-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ const makeSpawnArgs = options => {
if (!isCmd) {
chmod(scriptFile, '0775')
}
const spawnArgs = isCmd ? ['/d', '/s', '/c', scriptFile] : ['-c', scriptFile]
const spawnArgs = isCmd
? ['/d', '/s', '/c', escape.cmd(scriptFile)]
: ['-c', escape.sh(scriptFile)]

const spawnOpts = {
env: spawnEnv,
Expand Down
38 changes: 20 additions & 18 deletions test/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,24 @@ t.test('cmd', (t) => {
['\\%PATH%', '\\%%PATH%%', true],
['--arg="%PATH%"', '^"--arg=\\^"%%PATH%%\\^"^"'],
['--arg="%PATH%"', '^^^"--arg=\\^^^"%%PATH%%\\^^^"^^^"', true],
['--arg=npm exec -c "%PATH%"', '^"--arg=npm exec -c \\^"%%PATH%%\\^"^"'],
['--arg=npm exec -c "%PATH%"', '^^^"--arg=npm exec -c \\^^^"%%PATH%%\\^^^"^^^"', true],
[`--arg=npm exec -c '%PATH%'`, `^"--arg=npm exec -c '%%PATH%%'^"`],
[`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm exec -c '%%PATH%%'^^^"`, true],
[`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm exec -c \\^"%%PATH%%\\^"'^"`],
[`'--arg=npm exec -c "%PATH%"'`, `^^^"'--arg=npm exec -c \\^^^"%%PATH%%\\^^^"'^^^"`, true],
['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program Files\\test.bat\\^"^"'],
['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program Files\\test.bat\\^^^"^^^"', true],
['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program Files\\test%%.bat\\^"^"'],
['"C:\\Program Files\\test%.bat"', '^^^"\\^^^"C:\\Program Files\\test%%.bat\\^^^"^^^"', true],
['% % %', '^"%% %% %%^"'],
['% % %', '^^^"%% %% %%^^^"', true],
['--arg=npm exec -c "%PATH%"', '^"--arg=npm^ exec^ -c^ \\^"%%PATH%%\\^"^"'],
['--arg=npm exec -c "%PATH%"', '^^^"--arg=npm^^^ exec^^^ -c^^^ \\^^^"%%PATH%%\\^^^"^^^"', true],
[`--arg=npm exec -c '%PATH%'`, `^"--arg=npm^ exec^ -c^ '%%PATH%%'^"`],
[`--arg=npm exec -c '%PATH%'`, `^^^"--arg=npm^^^ exec^^^ -c^^^ '%%PATH%%'^^^"`, true],
[`'--arg=npm exec -c "%PATH%"'`, `^"'--arg=npm^ exec^ -c^ \\^"%%PATH%%\\^"'^"`],
[`'--arg=npm exec -c "%PATH%"'`,
`^^^"'--arg=npm^^^ exec^^^ -c^^^ \\^^^"%%PATH%%\\^^^"'^^^"`, true],
['"C:\\Program Files\\test.bat"', '^"\\^"C:\\Program^ Files\\test.bat\\^"^"'],
['"C:\\Program Files\\test.bat"', '^^^"\\^^^"C:\\Program^^^ Files\\test.bat\\^^^"^^^"', true],
['"C:\\Program Files\\test%.bat"', '^"\\^"C:\\Program^ Files\\test%%.bat\\^"^"'],
['"C:\\Program Files\\test%.bat"',
'^^^"\\^^^"C:\\Program^^^ Files\\test%%.bat\\^^^"^^^"', true],
['% % %', '^"%%^ %%^ %%^"'],
['% % %', '^^^"%%^^^ %%^^^ %%^^^"', true],
['hello^^^^^^', 'hello^^^^^^^^^^^^'],
['hello^^^^^^', 'hello^^^^^^^^^^^^^^^^^^^^^^^^', true],
['hello world', '^"hello world^"'],
['hello world', '^^^"hello world^^^"', true],
['hello world', '^"hello^ world^"'],
['hello world', '^^^"hello^^^ world^^^"', true],
['hello"world', '^"hello\\^"world^"'],
['hello"world', '^^^"hello\\^^^"world^^^"', true],
['hello""world', '^"hello\\^"\\^"world^"'],
Expand All @@ -90,10 +92,10 @@ t.test('cmd', (t) => {
['hello\\"world', '^^^"hello\\\\\\^^^"world^^^"', true],
['hello\\\\"world', '^"hello\\\\\\\\\\^"world^"'],
['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', true],
['hello world\\', '^"hello world\\\\^"'],
['hello world\\', '^^^"hello world\\\\^^^"', true],
['hello %PATH%', '^"hello %%PATH%%^"'],
['hello %PATH%', '^^^"hello %%PATH%%^^^"', true],
['hello world\\', '^"hello^ world\\\\^"'],
['hello world\\', '^^^"hello^^^ world\\\\^^^"', true],
['hello %PATH%', '^"hello^ %%PATH%%^"'],
['hello %PATH%', '^^^"hello^^^ %%PATH%%^^^"', true],
]

for (const [input, expectation, double] of expectations) {
Expand Down
94 changes: 57 additions & 37 deletions test/make-spawn-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ const which = {
}

const path = require('path')
const tmpdir = path.resolve(t.testdir())
// we make our fake temp dir contain spaces for extra safety in paths with spaces
const tmpdir = path.resolve(t.testdir({ 'with spaces': {} }), 'with spaces')

// used for unescaping windows path to script file
const unescapeCmd = (input) => input
.replace(/^\^"/, '')
.replace(/\^"$/, '')
.replace(/\^(.)/g, '$1')

const unescapeSh = (input) => input
.replace(/^'/, '')
.replace(/'$/, '')

const makeSpawnArgs = requireInject('../lib/make-spawn-args.js', {
fs: {
Expand Down Expand Up @@ -68,7 +79,7 @@ if (isWindows) {
cmd: 'script "quoted parameter"; second command',
})
t.equal(shell, 'cmd', 'default shell applies')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -81,11 +92,12 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

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

t.end()
})
Expand All @@ -103,7 +115,7 @@ if (isWindows) {
cmd: 'script "quoted parameter"; second command',
})
t.equal(shell, 'blrorp', 'used ComSpec as default shell')
t.match(args, ['-c', /\.sh$/], 'got expected args')
t.match(args, ['-c', /\.sh'$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -115,9 +127,10 @@ if (isWindows) {
windowsVerbatimArguments: undefined,
}, 'got expected options')

t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
const filename = unescapeSh(args[args.length - 1])
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand All @@ -131,7 +144,7 @@ if (isWindows) {
scriptShell: 'cmd.exe',
})
t.equal(shell, 'cmd.exe', 'kept cmd.exe')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -143,9 +156,10 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
const filename = unescapeCmd(args[args.length - 1])
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand All @@ -161,7 +175,7 @@ if (isWindows) {
args: ['"quoted parameter";', 'second command'],
})
t.equal(shell, 'cmd', 'default shell applies')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -174,11 +188,12 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
t.equal(contents, `@echo off\nscript ^"\\^"quoted parameter\\^";^" ^"second command^"`)
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
const filename = unescapeCmd(args[args.length - 1])
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
t.equal(contents, `@echo off\nscript ^"\\^"quoted^ parameter\\^";^" ^"second^ command^"`)
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand All @@ -194,7 +209,7 @@ if (isWindows) {
args: ['"quoted parameter";', 'second command'],
})
t.equal(shell, 'cmd', 'default shell applies')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -207,14 +222,15 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
const filename = unescapeCmd(args[args.length - 1])
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
t.equal(contents, [
'@echo off',
`script ^^^"\\^^^"quoted parameter\\^^^";^^^" ^^^"second command^^^"`,
`script ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"`,
].join('\n'))
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand All @@ -232,7 +248,7 @@ if (isWindows) {
args: ['"quoted parameter";', 'second command'],
})
t.equal(shell, 'cmd', 'default shell applies')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -245,15 +261,16 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

const contents = fs.readFileSync(args[args.length - 1], { encoding: 'utf8' })
const filename = unescapeCmd(args[args.length - 1])
const contents = fs.readFileSync(filename, { encoding: 'utf8' })
t.equal(contents, [
'@echo off',
// eslint-disable-next-line max-len
`"my script" ^^^"\\^^^"quoted parameter\\^^^";^^^" ^^^"second command^^^"`,
`"my script" ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"`,
].join('\n'))
t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand All @@ -275,7 +292,7 @@ if (isWindows) {
args: ['"quoted parameter";', 'second command'],
})
t.equal(shell, 'sh', 'defaults to sh')
t.match(args, ['-c', /\.sh$/], 'got expected args')
t.match(args, ['-c', /\.sh'$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -287,11 +304,12 @@ if (isWindows) {
windowsVerbatimArguments: undefined,
}, 'got expected options')

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

t.end()
})
Expand All @@ -305,7 +323,7 @@ if (isWindows) {
scriptShell: '/bin/sh',
})
t.equal(shell, '/bin/sh', 'kept provided setting')
t.match(args, ['-c', /\.sh$/], 'got expected args')
t.match(args, ['-c', /\.sh'$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -317,11 +335,12 @@ if (isWindows) {
windowsVerbatimArguments: undefined,
}, 'got expected options')

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

t.end()
})
Expand All @@ -336,7 +355,7 @@ if (isWindows) {
scriptShell: 'cmd.exe',
})
t.equal(shell, 'cmd.exe', 'kept cmd.exe')
t.match(args, ['/d', '/s', '/c', /\.cmd$/], 'got expected args')
t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args')
t.match(opts, {
env: {
npm_package_json: /package\.json$/,
Expand All @@ -348,9 +367,10 @@ if (isWindows) {
windowsVerbatimArguments: true,
}, 'got expected options')

t.ok(fs.existsSync(args[args.length - 1]), 'script file was written')
const filename = unescapeCmd(args[args.length - 1])
t.ok(fs.existsSync(filename), 'script file was written')
cleanup()
t.not(fs.existsSync(args[args.length - 1]), 'cleanup removes script file')
t.not(fs.existsSync(filename), 'cleanup removes script file')

t.end()
})
Expand Down

0 comments on commit 0bca5be

Please sign in to comment.