-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement argument escaping when the
shell
option is set
BREAKING CHANGE: when the `shell` option is set provided arguments will automatically be escaped
- Loading branch information
Showing
6 changed files
with
515 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
'use strict' | ||
|
||
// eslint-disable-next-line max-len | ||
// this code adapted from: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ | ||
const cmd = (input, doubleEscape) => { | ||
if (!input.length) { | ||
return '""' | ||
} | ||
|
||
let result | ||
if (!/[ \t\n\v"]/.test(input)) { | ||
result = input | ||
} else { | ||
result = '"' | ||
for (let i = 0; i <= input.length; ++i) { | ||
let slashCount = 0 | ||
while (input[i] === '\\') { | ||
++i | ||
++slashCount | ||
} | ||
|
||
if (i === input.length) { | ||
result += '\\'.repeat(slashCount * 2) | ||
break | ||
} | ||
|
||
if (input[i] === '"') { | ||
result += '\\'.repeat(slashCount * 2 + 1) | ||
result += input[i] | ||
} else { | ||
result += '\\'.repeat(slashCount) | ||
result += input[i] | ||
} | ||
} | ||
result += '"' | ||
} | ||
|
||
// and finally, prefix shell meta chars with a ^ | ||
result = result.replace(/[ !%^&()<>|"]/g, '^$&') | ||
if (doubleEscape) { | ||
result = result.replace(/[ !%^&()<>|"]/g, '^$&') | ||
} | ||
|
||
return result | ||
} | ||
|
||
const sh = (input) => { | ||
if (!input.length) { | ||
return `''` | ||
} | ||
|
||
if (!/[\t\n\r "#$&'()*;<>?\\`|~]/.test(input)) { | ||
return input | ||
} | ||
|
||
// replace single quotes with '\'' and wrap the whole result in a fresh set of quotes | ||
const result = `'${input.replace(/'/g, `'\\''`)}'` | ||
// if the input string already had single quotes around it, clean those up | ||
.replace(/^(?:'')+(?!$)/, '') | ||
.replace(/\\'''/g, `\\'`) | ||
|
||
return result | ||
} | ||
|
||
module.exports = { | ||
cmd, | ||
sh, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
'use strict' | ||
|
||
const { writeFileSync: writeFile } = require('fs') | ||
const { join } = require('path') | ||
const t = require('tap') | ||
const promiseSpawn = require('../lib/index.js') | ||
|
||
const escape = require('../lib/escape.js') | ||
const isWindows = process.platform === 'win32' | ||
|
||
t.test('sh', (t) => { | ||
const expectations = [ | ||
['', `''`], | ||
['test', 'test'], | ||
['test words', `'test words'`], | ||
['$1', `'$1'`], | ||
['"$1"', `'"$1"'`], | ||
[`'$1'`, `\\''$1'\\'`], | ||
['\\$1', `'\\$1'`], | ||
['--arg="$1"', `'--arg="$1"'`], | ||
['--arg=npm exec -c "$1"', `'--arg=npm exec -c "$1"'`], | ||
[`--arg=npm exec -c '$1'`, `'--arg=npm exec -c '\\''$1'\\'`], | ||
[`'--arg=npm exec -c "$1"'`, `\\''--arg=npm exec -c "$1"'\\'`], | ||
] | ||
|
||
for (const [input, expectation] of expectations) { | ||
t.equal(escape.sh(input), expectation, | ||
`expected to escape \`${input}\` to \`${expectation}\``) | ||
} | ||
|
||
t.test('integration', { skip: isWindows && 'posix only' }, async (t) => { | ||
for (const [input] of expectations) { | ||
const p = await promiseSpawn('node', ['-p', 'process.argv[1]', '--', input], | ||
{ shell: true, stdioString: true }) | ||
const stdout = p.stdout.trim() | ||
t.equal(stdout, input, `expected \`${stdout}\` to equal \`${input}\``) | ||
} | ||
|
||
t.end() | ||
}) | ||
|
||
t.end() | ||
}) | ||
|
||
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^%\\^"'^"`], | ||
[`'--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'], | ||
['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^"'], | ||
['hello\\\\"world', '^^^"hello\\\\\\\\\\^^^"world^^^"', 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) { | ||
const msg = `expected to${double ? ' double' : ''} escape \`${input}\` to \`${expectation}\`` | ||
t.equal(escape.cmd(input, double), expectation, msg) | ||
} | ||
|
||
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) | ||
|
||
const spawnOpts = { shell: true, stdioString: true } | ||
for (const [input,, double] of expectations) { | ||
const p = double | ||
? await promiseSpawn(shimFile, [input], spawnOpts) | ||
: await promiseSpawn('node', ['-p', 'process.argv[1]', '--', input], spawnOpts) | ||
t.equal(p.stdout, input, `expected \`${p.stdout}\` to equal \`${input}\``) | ||
} | ||
|
||
t.end() | ||
}) | ||
|
||
t.end() | ||
}) |
Oops, something went wrong.