@jsdevtools/ez-spawn
Version:
Simple, consistent sync or async process spawning
217 lines (185 loc) • 5.82 kB
JavaScript
;
const { parseArgsStringToArgv } = require("string-argv"); // possible alternative: parse-spawn-args
const detectType = require("type-detect");
module.exports = normalizeArgs;
/**
* This function normalizes the arguments of the {@link sync} and {@link async}
* so they can be passed to Node's {@link child_process.spawn} or
* {@link child_process.spawn} functions.
*
* @param {string|string[]} command
* The command to run (e.g. "git"), or the command and its arguments as a string
* (e.g. "git commit -a -m fixed_stuff"), or the command and its arguments as an
* array (e.g. ["git", "commit", "-a", "-m", "fixed stuff"]).
*
* @param {string|string[]} [args]
* The command arguments as a string (e.g. "git commit -a -m fixed_stuff") or as an array
* (e.g. ["git", "commit", "-a", "-m", "fixed stuff"]).
*
* @param {object} [options]
* The same options as {@link child_process.spawn} or {@link child_process.spawnSync}.
*
* @param {function} [callback]
* The callback that will receive the results, if applicable.
*
* @returns {object}
*/
function normalizeArgs (params) {
let command, args, options, callback, error;
try {
// Shift the arguments, if necessary
({ command, args, options, callback } = shiftArgs(params));
let commandArgs = [];
if (typeof command === "string" && args === undefined) {
// The command parameter is actually the command AND arguments,
// so split the string into an array
command = splitArgString(command);
}
if (Array.isArray(command)) {
// Split the command from the arguments
commandArgs = command.slice(1);
command = command[0];
}
if (typeof args === "string") {
// Convert the `args` argument from a string an array
args = splitArgString(args);
}
if (Array.isArray(args)) {
// Add these arguments to any arguments from above
args = commandArgs.concat(args);
}
if (args === undefined || args === null) {
args = commandArgs;
}
if (options === undefined || options === null) {
options = {};
}
// Set default options
options.encoding = options.encoding || "utf8";
// Validate all arguments
validateArgs(command, args, options, callback);
}
catch (err) {
error = err;
// Sanitize args that are used as output
command = String(command || "");
args = (Array.isArray(args) ? args : []).map((arg) => String(arg || ""));
}
return { command, args, options, callback, error };
}
/**
* Detects whether any optional arguments have been omitted,
* and shifts the other arguments as needed.
*
* @param {string|string[]} command
* @param {string|string[]} [args]
* @param {object} [options]
* @param {function} [callback]
* @returns {object}
*/
function shiftArgs (params) {
params = Array.prototype.slice.call(params);
let command, args, options, callback;
// Check for a callback as the final parameter
let lastParam = params[params.length - 1];
if (typeof lastParam === "function") {
callback = lastParam;
params.pop();
}
// Check for an options object as the second-to-last parameter
lastParam = params[params.length - 1];
if (lastParam === null || lastParam === undefined ||
(typeof lastParam === "object" && !Array.isArray(lastParam))) {
options = lastParam;
params.pop();
}
// The first parameter is the command
command = params.shift();
// All remaining parameters are the args
if (params.length === 0) {
args = undefined;
}
else if (params.length === 1 && Array.isArray(params[0])) {
args = params[0];
}
else if (params.length === 1 && params[0] === "") {
args = [];
}
else {
args = params;
}
return { command, args, options, callback };
}
/**
* Validates all arguments, and throws an error if any are invalid.
*
* @param {string} command
* @param {string[]} args
* @param {object} options
* @param {function} [callback]
*/
function validateArgs (command, args, options, callback) {
if (command === undefined || command === null) {
throw new Error("The command to execute is missing.");
}
if (typeof command !== "string") {
throw new Error("The command to execute should be a string, not " + friendlyType(command));
}
if (!Array.isArray(args)) {
throw new Error(
"The command arguments should be a string or an array, not " +
friendlyType(args)
);
}
for (let i = 0; i < args.length; i++) {
let arg = args[i];
if (typeof arg !== "string") {
throw new Error(
`The command arguments should be strings, but argument #${i + 1} is ` +
friendlyType(arg)
);
}
}
if (typeof options !== "object") {
throw new Error(
"The options should be an object, not " +
friendlyType(options)
);
}
if (callback !== undefined && callback !== null) {
if (typeof callback !== "function") {
throw new Error("The callback should be a function, not " + friendlyType(callback));
}
}
}
/**
* Splits an argument string (e.g. git commit -a -m "fixed stuff")
* into an array (e.g. ["git", "commit", "-a", "-m", "fixed stuff"]).
*
* @param {string} argString
* @returns {string[]}
*/
function splitArgString (argString) {
try {
return parseArgsStringToArgv(argString);
}
catch (error) {
throw new Error(`Could not parse the string: ${argString}\n${error.message}`);
}
}
/**
* Returns the friendly type name of the given value, for use in error messages.
*
* @param {*} val
* @returns {string}
*/
function friendlyType (val) {
let type = detectType(val);
let firstChar = String(type)[0].toLowerCase();
if (["a", "e", "i", "o", "u"].indexOf(firstChar) === -1) {
return `a ${type}.`;
}
else {
return `an ${type}.`;
}
}