diff --git a/lib/escape.js b/lib/escape.js index 303100d..9aca8bd 100644 --- a/lib/escape.js +++ b/lib/escape.js @@ -36,14 +36,11 @@ 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 - result = result.replace(/%/g, '%%') - return result } @@ -65,13 +62,7 @@ const sh = (input) => { return result } -// disabling the no-control-regex rule for this line as we very specifically _do_ want to -// replace those characters if they somehow exist at this point, which is highly unlikely -// eslint-disable-next-line no-control-regex -const filename = (input) => input.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') - module.exports = { cmd, sh, - filename, } diff --git a/lib/make-spawn-args.js b/lib/make-spawn-args.js index 7725fd9..5b06db3 100644 --- a/lib/make-spawn-args.js +++ b/lib/make-spawn-args.js @@ -1,19 +1,10 @@ /* eslint camelcase: "off" */ const isWindows = require('./is-windows.js') const setPATH = require('./set-path.js') -const { unlinkSync: unlink, writeFileSync: writeFile } = require('fs') -const { tmpdir } = require('os') const { resolve } = require('path') const which = require('which') const npm_config_node_gyp = require.resolve('node-gyp/bin/node-gyp.js') const escape = require('./escape.js') -const { randomBytes } = require('crypto') - -const translateWinPathToPosix = (path) => { - return path - .replace(/^([A-z]):/, '/$1') - .replace(/\\/g, '/') -} const makeSpawnArgs = options => { const { @@ -38,10 +29,7 @@ const makeSpawnArgs = options => { npm_config_node_gyp, }) - const fileName = escape.filename(`${event}-${randomBytes(4).toString('hex')}`) - let scriptFile - let script = '' - + let doubleEscape = false const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(scriptShell) if (isCmd) { let initialCmd = '' @@ -68,26 +56,18 @@ const makeSpawnArgs = options => { pathToInitial = initialCmd.toLowerCase() } - const doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat') - - scriptFile = resolve(tmpdir(), `${fileName}.cmd`) - script += '@echo off\n' - script += cmd - if (args.length) { - script += ` ${args.map((arg) => escape.cmd(arg, doubleEscape)).join(' ')}` - } - } else { - scriptFile = resolve(tmpdir(), `${fileName}.sh`) - script = cmd - if (args.length) { - script += ` ${args.map((arg) => escape.sh(arg)).join(' ')}` - } + doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat') } - writeFile(scriptFile, script) + let script = cmd + for (const arg of args) { + script += isCmd + ? ` ${escape.cmd(arg, doubleEscape)}` + : ` ${escape.sh(arg)}` + } const spawnArgs = isCmd - ? ['/d', '/s', '/c', escape.cmd(scriptFile)] - : [isWindows ? translateWinPathToPosix(scriptFile) : scriptFile] + ? ['/d', '/s', '/c', script] + : ['-c', '--', script] const spawnOpts = { env: spawnEnv, @@ -97,16 +77,7 @@ const makeSpawnArgs = options => { ...(isCmd ? { windowsVerbatimArguments: true } : {}), } - const cleanup = () => { - // delete the script, this is just a best effort - try { - unlink(scriptFile) - } catch (err) { - // ignore errors - } - } - - return [scriptShell, spawnArgs, spawnOpts, cleanup] + return [scriptShell, spawnArgs, spawnOpts] } module.exports = makeSpawnArgs diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index ec6ef31..f2808ac 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -55,7 +55,7 @@ const runScriptPkg = async options => { console.log(bruce(pkg._id, event, cmd)) } - const [spawnShell, spawnArgs, spawnOpts, cleanup] = makeSpawnArgs({ + const [spawnShell, spawnArgs, spawnOpts] = makeSpawnArgs({ event, path, scriptShell, @@ -93,7 +93,7 @@ const runScriptPkg = async options => { } else { throw er } - }).finally(cleanup) + }) } module.exports = runScriptPkg diff --git a/test/escape.js b/test/escape.js index fc27fb4..ca4c818 100644 --- a/test/escape.js +++ b/test/escape.js @@ -1,6 +1,6 @@ 'use strict' -const { writeFileSync: writeFile, unlinkSync: unlink, chmodSync: chmod } = require('fs') +const { writeFileSync: writeFile } = require('fs') const { join } = require('path') const t = require('tap') const promiseSpawn = require('@npmcli/promise-spawn') @@ -29,17 +29,11 @@ t.test('sh', (t) => { } t.test('integration', { skip: isWindows && 'posix only' }, async (t) => { - const dir = t.testdir() - for (const [input] of expectations) { - const filename = join(dir, 'posix.sh') - const script = `#!/usr/bin/env sh\nnode -p process.argv[1] -- ${escape.sh(input)}` - writeFile(filename, script) - chmod(filename, '0755') - const p = await promiseSpawn('sh', ['-c', filename], { stdioString: true }) + const script = `node -p process.argv[1] -- ${escape.sh(input)}` + const p = await promiseSpawn('sh', ['-c', '--', script], { stdioString: true }) const stdout = p.stdout.trim() - t.equal(input, stdout, 'actual output matches input') - unlink(filename) + t.equal(stdout, input, `expected \`${stdout}\` to equal \`${input}\``) } t.end() @@ -52,30 +46,31 @@ t.test('cmd', (t) => { const expectations = [ ['', '""'], ['test', 'test'], - ['%PATH%', '%%PATH%%'], - ['%PATH%', '%%PATH%%', true], - ['"%PATH%"', '^"\\^"%%PATH%%\\^"^"'], - ['"%PATH%"', '^^^"\\^^^"%%PATH%%\\^^^"^^^"', true], - [`'%PATH%'`, `'%%PATH%%'`], - [`'%PATH%'`, `'%%PATH%%'`, true], - ['\\%PATH%', '\\%%PATH%%'], - ['\\%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%%\\^"'^"`], + ['%PATH%', '^%PATH^%'], + ['%PATH%', '^^^%PATH^^^%', true], + ['"%PATH%"', '^"\\^"^%PATH^%\\^"^"'], + ['"%PATH%"', '^^^"\\^^^"^^^%PATH^^^%\\^^^"^^^"', true], + [`'%PATH%'`, `'^%PATH^%'`], + [`'%PATH%'`, `'^^^%PATH^^^%'`, true], + ['\\%PATH%', '\\^%PATH^%'], + ['\\%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], + `^^^"'--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\\^"^"'], ['"C:\\Program Files\\test%.bat"', - '^^^"\\^^^"C:\\Program^^^ Files\\test%%.bat\\^^^"^^^"', true], - ['% % %', '^"%%^ %%^ %%^"'], - ['% % %', '^^^"%%^^^ %%^^^ %%^^^"', true], + '^^^"\\^^^"C:\\Program^^^ Files\\test^^^%.bat\\^^^"^^^"', true], + ['% % %', '^"^%^ ^%^ ^%^"'], + ['% % %', '^^^"^^^%^^^ ^^^%^^^ ^^^%^^^"', true], ['hello^^^^^^', 'hello^^^^^^^^^^^^'], ['hello^^^^^^', 'hello^^^^^^^^^^^^^^^^^^^^^^^^', true], ['hello world', '^"hello^ world^"'], @@ -94,8 +89,8 @@ t.test('cmd', (t) => { ['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', true], ['hello world\\', '^"hello^ world\\\\^"'], ['hello world\\', '^^^"hello^^^ world\\\\^^^"', true], - ['hello %PATH%', '^"hello^ %%PATH%%^"'], - ['hello %PATH%', '^^^"hello^^^ %%PATH%%^^^"', true], + ['hello %PATH%', '^"hello^ ^%PATH^%^"'], + ['hello %PATH%', '^^^"hello^^^ ^^^%PATH^^^%^^^"', true], ] for (const [input, expectation, double] of expectations) { @@ -105,23 +100,20 @@ t.test('cmd', (t) => { t.test('integration', { skip: !isWindows && 'Windows only' }, async (t) => { const dir = t.testdir() + const shimFile = join(dir, 'shim.cmd') + const shim = `@echo off\nnode -p process.argv[1] -- %*` + writeFile(shimFile, shim) for (const [input,, double] of expectations) { - const filename = join(dir, 'win.cmd') - if (double) { - const shimFile = join(dir, 'shim.cmd') - const shim = `@echo off\nnode -p process.argv[1] -- %*` - writeFile(shimFile, shim) - const script = `@echo off\n"${shimFile}" ${escape.cmd(input, double)}` - writeFile(filename, script) - } else { - const script = `@echo off\nnode -p process.argv[1] -- ${escape.cmd(input)}` - writeFile(filename, script) - } - const p = await promiseSpawn('cmd', ['/d', '/s', '/c', filename], { stdioString: true }) + const script = double + ? `${escape.cmd(shimFile)} ${escape.cmd(input, double)}` + : `node -p process.argv[1] -- ${escape.cmd(input)}` + const p = await promiseSpawn('cmd', ['/d', '/s', '/c', script], { + stdioString: true, + windowsVerbatimArguments: true, + }) const stdout = p.stdout.trim() - t.equal(input, stdout, 'actual output matches input') - unlink(filename) + t.equal(stdout, input, `expected \`${stdout}\` to equal \`${input}\``) } t.end() diff --git a/test/make-spawn-args.js b/test/make-spawn-args.js index 66573bb..1512be4 100644 --- a/test/make-spawn-args.js +++ b/test/make-spawn-args.js @@ -1,5 +1,4 @@ const t = require('tap') -const fs = require('fs') const requireInject = require('require-inject') const isWindows = require('../lib/is-windows.js') @@ -22,44 +21,17 @@ const which = { }, } -const path = require('path') -// 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 { dirname } = require('path') +const resolve = (...args) => { + const root = isWindows ? 'C:\\Temp' : '/tmp' + return [root, ...args].join(isWindows ? '\\' : '/') +} const makeSpawnArgs = requireInject('../lib/make-spawn-args.js', { - fs: { - ...fs, - chmodSync (_path, mode) { - if (process.platform === 'win32') { - _path = _path.replace(/\//g, '\\') - } else { - _path = _path.replace(/\\/g, '/') - } - return fs.chmodSync(_path, mode) - }, - writeFileSync (_path, content) { - if (process.platform === 'win32') { - _path = _path.replace(/\//g, '\\') - } else { - _path = _path.replace(/\\/g, '/') - } - return fs.writeFileSync(_path, content) - }, - }, which, - os: { - ...require('os'), - tmpdir: () => tmpdir, + path: { + dirname, + resolve, }, }) @@ -73,18 +45,20 @@ if (isWindows) { }) t.test('simple script', (t) => { - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', 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(opts, { + t.strictSame(args, ['/d', '/s', '/c', + 'script "quoted parameter"; second command', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', + npm_lifecycle_script: 'script "quoted parameter"; second command', npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), }, stdio: undefined, @@ -92,32 +66,24 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - 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(filename), 'cleanup removes script file') - t.end() }) t.test('event with invalid characters runs', (t) => { - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event<:>\x03', // everything after the word "event" is invalid path: 'path', cmd: 'script "quoted parameter"; second command', }) t.equal(shell, 'cmd', 'default shell applies') - // disabling no-control-regex because we are testing specifically if the control - // character gets removed - // eslint-disable-next-line no-control-regex - t.match(args, ['/d', '/s', '/c', /(?:\\|\/)[^<:>\x03]+.cmd\^"$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['/d', '/s', '/c', + 'script "quoted parameter"; second command', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, - npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', + npm_package_json: 'C:\\Temp\\path\\package.json', + npm_lifecycle_event: 'event<:>\x03', + npm_lifecycle_script: 'script "quoted parameter"; second command', npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), }, stdio: undefined, @@ -125,13 +91,6 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - 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(filename), 'cleanup removes script file') - t.end() }) @@ -142,37 +101,30 @@ if (isWindows) { whichPaths.delete('blrorp') delete process.env.ComSpec }) - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script "quoted parameter"; second command', }) t.equal(shell, 'blrorp', 'used ComSpec as default shell') - t.match(args, [/\.sh$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['-c', '--', 'script "quoted parameter"; second command'], + 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', + npm_lifecycle_script: 'script "quoted parameter"; second command', }, stdio: undefined, cwd: 'path', windowsVerbatimArguments: undefined, }, 'got expected options') - let filename = unescapeSh(args[args.length - 1]) - if (process.platform === 'win32') { - filename = filename.replace(/^\/([A-z])/, '$1:') - } - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) t.test('with cmd.exe as scriptShell', (t) => { - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script', @@ -180,10 +132,12 @@ 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(opts, { + t.strictSame(args, ['/d', '/s', '/c', + 'script ^"\\^"quoted^ parameter\\^";^" ^"second^ command^"', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', npm_lifecycle_script: 'script', }, @@ -192,11 +146,6 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - const filename = unescapeCmd(args[args.length - 1]) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) @@ -204,17 +153,19 @@ if (isWindows) { whichPaths.set('script', '/path/script.exe') t.teardown(() => whichPaths.delete('script')) - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script', args: ['"quoted parameter";', 'second command'], }) t.equal(shell, 'cmd', 'default shell applies') - t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['/d', '/s', '/c', + 'script ^"\\^"quoted^ parameter\\^";^" ^"second^ command^"', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', npm_lifecycle_script: 'script', npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), @@ -224,13 +175,6 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - 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(filename), 'cleanup removes script file') - t.end() }) @@ -238,17 +182,19 @@ if (isWindows) { whichPaths.set('script', '/path/script.cmd') t.teardown(() => whichPaths.delete('script')) - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script', args: ['"quoted parameter";', 'second command'], }) t.equal(shell, 'cmd', 'default shell applies') - t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['/d', '/s', '/c', + 'script ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', npm_lifecycle_script: 'script', npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), @@ -258,16 +204,6 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - const filename = unescapeCmd(args[args.length - 1]) - const contents = fs.readFileSync(filename, { encoding: 'utf8' }) - t.equal(contents, [ - '@echo off', - `script ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"`, - ].join('\n')) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) @@ -277,19 +213,21 @@ if (isWindows) { whichPaths.set('"my script"', '/path/script.cmd') t.teardown(() => whichPaths.delete('my script')) - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: '"my script"', args: ['"quoted parameter";', 'second command'], }) t.equal(shell, 'cmd', 'default shell applies') - t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['/d', '/s', '/c', + '"my script" ^^^"\\^^^"quoted^^^ parameter\\^^^";^^^" ^^^"second^^^ command^^^"', + ], 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: 'C:\\Temp\\path\\package.json', npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', + npm_lifecycle_script: '"my script"', npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), }, stdio: undefined, @@ -297,17 +235,6 @@ if (isWindows) { windowsVerbatimArguments: true, }, 'got expected options') - 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^^^"`, - ].join('\n')) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) @@ -321,17 +248,18 @@ if (isWindows) { }) t.test('simple script', (t) => { - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script', args: ['"quoted parameter";', 'second command'], }) t.equal(shell, 'sh', 'defaults to sh') - t.match(args, [/\.sh$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['-c', '--', `script '"quoted parameter";' 'second command'`], + 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: '/tmp/path/package.json', npm_lifecycle_event: 'event', npm_lifecycle_script: 'script', }, @@ -340,31 +268,23 @@ if (isWindows) { windowsVerbatimArguments: undefined, }, 'got expected options') - const filename = unescapeSh(args[args.length - 1]) - const contents = fs.readFileSync(filename, { encoding: 'utf8' }) - t.equal(contents, `script '"quoted parameter";' 'second command'`) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) t.test('event with invalid characters runs', (t) => { - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event<:>/\x04', path: 'path', cmd: 'script', args: ['"quoted parameter";', 'second command'], }) t.equal(shell, 'sh', 'defaults to sh') - // no-control-regex disabled because we're specifically testing control chars - // eslint-disable-next-line no-control-regex - t.match(args, [/(?:\\|\/)[^<:>/\x04]+\.sh$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['-c', '--', `script '"quoted parameter";' 'second command'`], + 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, - npm_lifecycle_event: 'event', + npm_package_json: '/tmp/path/package.json', + npm_lifecycle_event: 'event<:>/\x04', npm_lifecycle_script: 'script', }, stdio: undefined, @@ -372,43 +292,32 @@ if (isWindows) { windowsVerbatimArguments: undefined, }, 'got expected options') - const filename = unescapeSh(args[args.length - 1]) - const contents = fs.readFileSync(filename, { encoding: 'utf8' }) - t.equal(contents, `script '"quoted parameter";' 'second command'`) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() }) t.test('can use cmd.exe', (t) => { // test that we can explicitly run in cmd.exe, even on posix systems // relevant for running under WSL - const [shell, args, opts, cleanup] = makeSpawnArgs({ + const [shell, args, opts] = makeSpawnArgs({ event: 'event', path: 'path', cmd: 'script "quoted parameter"; second command', scriptShell: 'cmd.exe', }) t.equal(shell, 'cmd.exe', 'kept cmd.exe') - t.match(args, ['/d', '/s', '/c', /\.cmd\^"$/], 'got expected args') - t.match(opts, { + t.strictSame(args, ['/d', '/s', '/c', 'script "quoted parameter"; second command'], + 'got expected args') + t.hasStrict(opts, { env: { - npm_package_json: /package\.json$/, + npm_package_json: '/tmp/path/package.json', npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', + npm_lifecycle_script: 'script "quoted parameter"; second command', }, stdio: undefined, cwd: 'path', windowsVerbatimArguments: true, }, 'got expected options') - const filename = unescapeCmd(args[args.length - 1]) - t.ok(fs.existsSync(filename), 'script file was written') - cleanup() - t.not(fs.existsSync(filename), 'cleanup removes script file') - t.end() })