@npmcli/promise-spawn
Advanced tools
Comparing version 5.0.0 to 6.0.0
155
lib/index.js
@@ -0,15 +1,23 @@ | ||
'use strict' | ||
const { spawn } = require('child_process') | ||
const os = require('os') | ||
const which = require('which') | ||
const isPipe = (stdio = 'pipe', fd) => | ||
stdio === 'pipe' || stdio === null ? true | ||
: Array.isArray(stdio) ? isPipe(stdio[fd], fd) | ||
: false | ||
const escape = require('./escape.js') | ||
// 'extra' object is for decorating the error a bit more | ||
const promiseSpawn = (cmd, args, opts = {}, extra = {}) => { | ||
if (opts.shell) { | ||
return spawnWithShell(cmd, args, opts, extra) | ||
} | ||
let proc | ||
const p = new Promise((res, rej) => { | ||
proc = spawn(cmd, args, opts) | ||
const stdout = [] | ||
const stderr = [] | ||
const reject = er => rej(Object.assign(er, { | ||
@@ -21,3 +29,5 @@ cmd, | ||
})) | ||
proc.on('error', reject) | ||
if (proc.stdout) { | ||
@@ -27,2 +37,3 @@ proc.stdout.on('data', c => stdout.push(c)).on('error', reject) | ||
} | ||
if (proc.stderr) { | ||
@@ -32,2 +43,3 @@ proc.stderr.on('data', c => stderr.push(c)).on('error', reject) | ||
} | ||
proc.on('close', (code, signal) => { | ||
@@ -42,2 +54,3 @@ const result = { | ||
} | ||
if (code || signal) { | ||
@@ -56,12 +69,132 @@ rej(Object.assign(new Error('command failed'), result)) | ||
const stdioResult = (stdout, stderr, { stdioString, stdio }) => | ||
stdioString ? { | ||
stdout: isPipe(stdio, 1) ? Buffer.concat(stdout).toString().trim() : null, | ||
stderr: isPipe(stdio, 2) ? Buffer.concat(stderr).toString().trim() : null, | ||
const spawnWithShell = (cmd, args, opts, extra) => { | ||
let command = opts.shell | ||
// if shell is set to true, we use a platform default. we can't let the core | ||
// spawn method decide this for us because we need to know what shell is in use | ||
// ahead of time so that we can escape arguments properly. we don't need coverage here. | ||
if (command === true) { | ||
// istanbul ignore next | ||
command = process.platform === 'win32' ? process.env.ComSpec : 'sh' | ||
} | ||
: { | ||
stdout: isPipe(stdio, 1) ? Buffer.concat(stdout) : null, | ||
stderr: isPipe(stdio, 2) ? Buffer.concat(stderr) : null, | ||
const options = { ...opts, shell: false } | ||
const realArgs = [] | ||
let script = cmd | ||
// first, determine if we're in windows because if we are we need to know if we're | ||
// running an .exe or a .cmd/.bat since the latter requires extra escaping | ||
const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(command) | ||
if (isCmd) { | ||
let doubleEscape = false | ||
// find the actual command we're running | ||
let initialCmd = '' | ||
let insideQuotes = false | ||
for (let i = 0; i < cmd.length; ++i) { | ||
const char = cmd.charAt(i) | ||
if (char === ' ' && !insideQuotes) { | ||
break | ||
} | ||
initialCmd += char | ||
if (char === '"' || char === "'") { | ||
insideQuotes = !insideQuotes | ||
} | ||
} | ||
let pathToInitial | ||
try { | ||
pathToInitial = which.sync(initialCmd, { | ||
path: (options.env && options.env.PATH) || process.env.PATH, | ||
pathext: (options.env && options.env.PATHEXT) || process.env.PATHEXT, | ||
}).toLowerCase() | ||
} catch (err) { | ||
pathToInitial = initialCmd.toLowerCase() | ||
} | ||
doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat') | ||
for (const arg of args) { | ||
script += ` ${escape.cmd(arg, doubleEscape)}` | ||
} | ||
realArgs.push('/d', '/s', '/c', script) | ||
options.windowsVerbatimArguments = true | ||
} else { | ||
for (const arg of args) { | ||
script += ` ${escape.sh(arg)}` | ||
} | ||
realArgs.push('-c', script) | ||
} | ||
return promiseSpawn(command, realArgs, options, extra) | ||
} | ||
// open a file with the default application as defined by the user's OS | ||
const open = (_args, opts = {}, extra = {}) => { | ||
const options = { ...opts, shell: true } | ||
const args = [].concat(_args) | ||
let platform = process.platform | ||
// process.platform === 'linux' may actually indicate WSL, if that's the case | ||
// we want to treat things as win32 anyway so the host can open the argument | ||
if (platform === 'linux' && os.release().includes('Microsoft')) { | ||
platform = 'win32' | ||
} | ||
let command = options.command | ||
if (!command) { | ||
if (platform === 'win32') { | ||
// spawnWithShell does not do the additional os.release() check, so we | ||
// have to force the shell here to make sure we treat WSL as windows. | ||
options.shell = process.env.ComSpec | ||
// also, the start command accepts a title so to make sure that we don't | ||
// accidentally interpret the first arg as the title, we stick an empty | ||
// string immediately after the start command | ||
command = 'start ""' | ||
} else if (platform === 'darwin') { | ||
command = 'open' | ||
} else { | ||
command = 'xdg-open' | ||
} | ||
} | ||
return spawnWithShell(command, args, options, extra) | ||
} | ||
promiseSpawn.open = open | ||
const isPipe = (stdio = 'pipe', fd) => { | ||
if (stdio === 'pipe' || stdio === null) { | ||
return true | ||
} | ||
if (Array.isArray(stdio)) { | ||
return isPipe(stdio[fd], fd) | ||
} | ||
return false | ||
} | ||
const stdioResult = (stdout, stderr, { stdioString = true, stdio }) => { | ||
const result = { | ||
stdout: null, | ||
stderr: null, | ||
} | ||
// stdio is [stdin, stdout, stderr] | ||
if (isPipe(stdio, 1)) { | ||
result.stdout = Buffer.concat(stdout) | ||
if (stdioString) { | ||
result.stdout = result.stdout.toString().trim() | ||
} | ||
} | ||
if (isPipe(stdio, 2)) { | ||
result.stderr = Buffer.concat(stderr) | ||
if (stdioString) { | ||
result.stderr = result.stderr.toString().trim() | ||
} | ||
} | ||
return result | ||
} | ||
module.exports = promiseSpawn |
{ | ||
"name": "@npmcli/promise-spawn", | ||
"version": "5.0.0", | ||
"version": "6.0.0", | ||
"files": [ | ||
@@ -35,4 +35,5 @@ "bin/", | ||
"@npmcli/eslint-config": "^4.0.0", | ||
"@npmcli/template-oss": "4.7.1", | ||
"@npmcli/template-oss": "4.8.0", | ||
"minipass": "^3.1.1", | ||
"spawk": "^1.7.1", | ||
"tap": "^16.0.1" | ||
@@ -45,4 +46,7 @@ }, | ||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", | ||
"version": "4.7.1" | ||
"version": "4.8.0" | ||
}, | ||
"dependencies": { | ||
"which": "^2.0.2" | ||
} | ||
} |
@@ -14,3 +14,3 @@ # @npmcli/promise-spawn | ||
cwd: '/tmp/some/path', // defaults to process.cwd() | ||
stdioString: false, // stdout/stderr as strings rather than buffers | ||
stdioString: true, // stdout/stderr as strings rather than buffers | ||
stdio: 'pipe', // any node spawn stdio arg is valid here | ||
@@ -53,3 +53,3 @@ // any other arguments to node child_process.spawn can go here as well, | ||
- `stdioString` Boolean, default `false`. Return stdout/stderr output as | ||
- `stdioString` Boolean, default `true`. Return stdout/stderr output as | ||
strings rather than buffers. | ||
@@ -59,2 +59,24 @@ - `cwd` String, default `process.cwd()`. Current working directory for | ||
effective uid/gid when run as root on Unix systems. | ||
- `shell` Boolean or String. If false, no shell is used during spawn. If true, | ||
the system default shell is used. If a String, that specific shell is used. | ||
When a shell is used, the given command runs from within that shell by | ||
concatenating the command and its escaped arguments and running the result. | ||
This option is _not_ passed through to `child_process.spawn`. | ||
- Any other options for `child_process.spawn` can be passed as well. | ||
### `promiseSpawn.open(arg, opts, extra)` -> `Promise` | ||
Use the operating system to open `arg` with a default program. This is useful | ||
for things like opening the user's default browser to a specific URL. | ||
Depending on the platform in use this will use `start` (win32), `open` (darwin) | ||
or `xdg-open` (everything else). In the case of Windows Subsystem for Linux we | ||
use the default win32 behavior as it is much more predictable to open the arg | ||
using the host operating system. | ||
#### Options | ||
Options are identical to `promiseSpawn` except for the following: | ||
- `command` String, the command to use to open the file in question. Default is | ||
one of `start`, `open` or `xdg-open` depending on platform in use. |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
11744
5
219
80
1
5
4
+ Addedwhich@^2.0.2
+ Addedisexe@2.0.0(transitive)
+ Addedwhich@2.0.2(transitive)