Skip to content

Commit 24c5165

Browse files
committed
feat: write scripts to a file and run that instead of passing scripts as a single string
1 parent 06511fb commit 24c5165

File tree

6 files changed

+229
-18
lines changed

6 files changed

+229
-18
lines changed

lib/escape.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict'
2+
3+
const cmd = (input) => {
4+
if (!input.length) {
5+
return '""'
6+
}
7+
8+
let result
9+
if (!/[ \t\n\v"]/.test(input)) {
10+
result = input
11+
} else {
12+
result = '"'
13+
for (let i = 0; i <= input.length; ++i) {
14+
let slashCount = 0
15+
while (input[i] === '\\') {
16+
++i
17+
++slashCount
18+
}
19+
20+
if (i === input.length) {
21+
result += '\\'.repeat(slashCount * 2)
22+
break
23+
}
24+
25+
if (input[i] === '"') {
26+
result += '\\'.repeat(slashCount * 2 + 1)
27+
result += input[i]
28+
} else {
29+
result += '\\'.repeat(slashCount)
30+
result += input[i]
31+
}
32+
}
33+
result += '"'
34+
}
35+
36+
// and finally, prefix shell meta chars with a ^
37+
result = result.replace(/[!^&()<>|"]/g, '^$&')
38+
// except for % which is escaped with another %
39+
result = result.replace(/%/g, '%%')
40+
41+
return result
42+
}
43+
44+
const sh = (input) => {
45+
if (!input.length) {
46+
return `''`
47+
}
48+
49+
if (!/[\t\n\r "#$&'()*;<>?\\`|~]/.test(input)) {
50+
return input
51+
}
52+
53+
// replace single quotes with '\'' and wrap the whole result in a fresh set of quotes
54+
const result = `'${input.replace(/'/g, `'\\''`)}'`
55+
// if the input string already had single quotes around it, clean those up
56+
.replace(/^(?:'')+(?!$)/, '')
57+
.replace(/\\'''/g, `\\'`)
58+
59+
return result
60+
}
61+
62+
module.exports = {
63+
cmd,
64+
sh,
65+
}

lib/make-spawn-args.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
/* eslint camelcase: "off" */
22
const isWindows = require('./is-windows.js')
33
const setPATH = require('./set-path.js')
4+
const { chmodSync: chmod, writeFileSync: writeFile } = require('fs')
5+
const { tmpdir } = require('os')
46
const { resolve } = require('path')
7+
const which = require('which')
58
const npm_config_node_gyp = require.resolve('node-gyp/bin/node-gyp.js')
9+
const escape = require('./escape.js')
610

711
const makeSpawnArgs = options => {
812
const {
@@ -12,11 +16,28 @@ const makeSpawnArgs = options => {
1216
env = {},
1317
stdio,
1418
cmd,
19+
args = [],
1520
stdioString = false,
1621
} = options
1722

23+
let scriptFile
24+
let script = ''
1825
const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(scriptShell)
19-
const args = isCmd ? ['/d', '/s', '/c', cmd] : ['-c', cmd]
26+
if (isCmd) {
27+
scriptFile = resolve(tmpdir(), `${event}-${Date.now()}.cmd`)
28+
script += '@echo off\n'
29+
script += `${cmd} ${args.map((arg) => escape.cmd(arg)).join(' ')}`
30+
} else {
31+
const shellPath = which.sync(scriptShell)
32+
scriptFile = resolve(tmpdir(), `${event}-${Date.now()}.sh`)
33+
script += `#!${shellPath}\n`
34+
script += `${cmd} ${args.map((arg) => escape.sh(arg)).join(' ')}`
35+
}
36+
writeFile(scriptFile, script)
37+
if (!isCmd) {
38+
chmod(scriptFile, '0775')
39+
}
40+
const spawnArgs = isCmd ? ['/d', '/s', '/c', scriptFile] : ['-c', scriptFile]
2041

2142
const spawnOpts = {
2243
env: setPATH(path, {
@@ -34,7 +55,7 @@ const makeSpawnArgs = options => {
3455
...(isCmd ? { windowsVerbatimArguments: true } : {}),
3556
}
3657

37-
return [scriptShell, args, spawnOpts]
58+
return [scriptShell, spawnArgs, spawnOpts]
3859
}
3960

4061
module.exports = makeSpawnArgs

lib/run-script-pkg.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const runScriptPkg = async options => {
3131
if (options.cmd) {
3232
cmd = options.cmd
3333
} else if (pkg.scripts && pkg.scripts[event]) {
34-
cmd = pkg.scripts[event] + args.map(a => ` ${JSON.stringify(a)}`).join('')
34+
cmd = pkg.scripts[event]
3535
} else if (
3636
// If there is no preinstall or install script, default to rebuilding node-gyp packages.
3737
event === 'install' &&
@@ -42,7 +42,7 @@ const runScriptPkg = async options => {
4242
) {
4343
cmd = defaultGypInstallScript
4444
} else if (event === 'start' && await isServerPackage(path)) {
45-
cmd = 'node server.js' + args.map(a => ` ${JSON.stringify(a)}`).join('')
45+
cmd = 'node server.js'
4646
}
4747

4848
if (!cmd) {
@@ -61,6 +61,7 @@ const runScriptPkg = async options => {
6161
env: packageEnvs(env, pkg),
6262
stdio,
6363
cmd,
64+
args,
6465
stdioString,
6566
}), {
6667
event,

test/escape.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const t = require('tap')
2+
3+
const escape = require('../lib/escape.js')
4+
5+
t.test('sh', (t) => {
6+
t.test('returns empty quotes when input is empty', async (t) => {
7+
const input = ''
8+
const output = escape.sh(input)
9+
t.equal(output, `''`, 'returned empty single quotes')
10+
})
11+
12+
t.test('returns plain string if quotes are not necessary', async (t) => {
13+
const input = 'test'
14+
const output = escape.sh(input)
15+
t.equal(output, input, 'returned plain string')
16+
})
17+
18+
t.test('wraps in single quotes if special character is present', async (t) => {
19+
const input = 'test words'
20+
const output = escape.sh(input)
21+
t.equal(output, `'test words'`, 'wrapped in single quotes')
22+
})
23+
t.end()
24+
})
25+
26+
t.test('cmd', (t) => {
27+
t.test('returns empty quotes when input is empty', async (t) => {
28+
const input = ''
29+
const output = escape.cmd(input)
30+
t.equal(output, '""', 'returned empty double quotes')
31+
})
32+
33+
t.test('returns plain string if quotes are not necessary', async (t) => {
34+
const input = 'test'
35+
const output = escape.cmd(input)
36+
t.equal(output, input, 'returned plain string')
37+
})
38+
39+
t.test('wraps in double quotes when necessary', async (t) => {
40+
const input = 'test words'
41+
const output = escape.cmd(input)
42+
t.equal(output, '^"test words^"', 'wrapped in double quotes')
43+
})
44+
45+
t.test('doubles up backslashes at end of input', async (t) => {
46+
const input = 'one \\ two \\'
47+
const output = escape.cmd(input)
48+
t.equal(output, '^"one \\ two \\\\^"', 'doubles backslash at end of string')
49+
})
50+
51+
t.test('doubles up backslashes immediately before a double quote', async (t) => {
52+
const input = 'one \\"'
53+
const output = escape.cmd(input)
54+
t.equal(output, '^"one \\\\\\^"^"', 'doubles backslash before double quote')
55+
})
56+
57+
t.test('backslash escapes double quotes', async (t) => {
58+
const input = '"test"'
59+
const output = escape.cmd(input)
60+
t.equal(output, '^"\\^"test\\^"^"', 'escaped double quotes')
61+
})
62+
t.end()
63+
})

test/make-spawn-args.js

+60-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const t = require('tap')
2+
const fs = require('fs')
23
const requireInject = require('require-inject')
34
const isWindows = require('../lib/is-windows.js')
45

@@ -10,21 +11,62 @@ if (!process.env.__FAKE_TESTING_PLATFORM__) {
1011
} })
1112
}
1213

14+
const whichPaths = new Map()
15+
const which = {
16+
sync: (req) => {
17+
if (whichPaths.has(req)) {
18+
return whichPaths.get(req)
19+
}
20+
21+
throw new Error('not found')
22+
},
23+
}
24+
25+
const path = require('path')
26+
const tmpdir = path.resolve(t.testdir())
27+
1328
const makeSpawnArgs = requireInject('../lib/make-spawn-args.js', {
14-
path: require('path')[isWindows ? 'win32' : 'posix'],
29+
fs: {
30+
...fs,
31+
chmodSync (_path, mode) {
32+
if (process.platform === 'win32') {
33+
_path = _path.replace(/\//g, '\\')
34+
} else {
35+
_path = _path.replace(/\\/g, '/')
36+
}
37+
return fs.chmodSync(_path, mode)
38+
},
39+
writeFileSync (_path, content) {
40+
if (process.platform === 'win32') {
41+
_path = _path.replace(/\//g, '\\')
42+
} else {
43+
_path = _path.replace(/\\/g, '/')
44+
}
45+
return fs.writeFileSync(_path, content)
46+
},
47+
},
48+
which,
49+
os: {
50+
...require('os'),
51+
tmpdir: () => tmpdir,
52+
},
1553
})
1654

1755
if (isWindows) {
1856
t.test('windows', t => {
1957
// with no ComSpec
2058
delete process.env.ComSpec
59+
whichPaths.set('cmd', 'C:\\Windows\\System32\\cmd.exe')
60+
t.teardown(() => {
61+
whichPaths.delete('cmd')
62+
})
2163
t.match(makeSpawnArgs({
2264
event: 'event',
2365
path: 'path',
2466
cmd: 'script "quoted parameter"; second command',
2567
}), [
2668
'cmd',
27-
['/d', '/s', '/c', `script "quoted parameter"; second command`],
69+
['/d', '/s', '/c', /\.cmd$/],
2870
{
2971
env: {
3072
npm_package_json: /package\.json$/,
@@ -40,13 +82,17 @@ if (isWindows) {
4082

4183
// with a funky ComSpec
4284
process.env.ComSpec = 'blrorp'
85+
whichPaths.set('blrorp', '/bin/blrorp')
86+
t.teardown(() => {
87+
whichPaths.delete('blrorp')
88+
})
4389
t.match(makeSpawnArgs({
4490
event: 'event',
4591
path: 'path',
4692
cmd: 'script "quoted parameter"; second command',
4793
}), [
4894
'blrorp',
49-
['-c', `script "quoted parameter"; second command`],
95+
['-c', /\.sh$/],
5096
{
5197
env: {
5298
npm_package_json: /package\.json$/,
@@ -62,11 +108,12 @@ if (isWindows) {
62108
t.match(makeSpawnArgs({
63109
event: 'event',
64110
path: 'path',
65-
cmd: 'script "quoted parameter"; second command',
111+
cmd: 'script',
112+
args: ['"quoted parameter";', 'second command'],
66113
scriptShell: 'cmd.exe',
67114
}), [
68115
'cmd.exe',
69-
['/d', '/s', '/c', `script "quoted parameter"; second command`],
116+
['/d', '/s', '/c', /\.cmd$/],
70117
{
71118
env: {
72119
npm_package_json: /package\.json$/,
@@ -83,13 +130,18 @@ if (isWindows) {
83130
})
84131
} else {
85132
t.test('posix', t => {
133+
whichPaths.set('sh', '/bin/sh')
134+
t.teardown(() => {
135+
whichPaths.delete('sh')
136+
})
86137
t.match(makeSpawnArgs({
87138
event: 'event',
88139
path: 'path',
89-
cmd: 'script "quoted parameter"; second command',
140+
cmd: 'script',
141+
args: ['"quoted parameter";', 'second command'],
90142
}), [
91143
'sh',
92-
['-c', `script "quoted parameter"; second command`],
144+
['-c', /\.sh$/],
93145
{
94146
env: {
95147
npm_package_json: /package\.json$/,
@@ -111,7 +163,7 @@ if (isWindows) {
111163
scriptShell: 'cmd.exe',
112164
}), [
113165
'cmd.exe',
114-
['/d', '/s', '/c', `script "quoted parameter"; second command`],
166+
['/d', '/s', '/c', /\.cmd$/],
115167
{
116168
env: {
117169
npm_package_json: /package\.json$/,

0 commit comments

Comments
 (0)