You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
9.7 KiB
387 lines
9.7 KiB
'use strict';
|
|
|
|
const spawn = require('child_process').spawn;
|
|
const path = require('path');
|
|
const camelcase = require('camelcase');
|
|
const stringSimilarity = require('string-similarity');
|
|
|
|
module.exports = {
|
|
handleType(value) {
|
|
let type = value;
|
|
if (typeof value !== 'function') {
|
|
type = value.constructor;
|
|
}
|
|
|
|
// Depending on the type of the default value,
|
|
// select a default initializer function
|
|
switch (type) {
|
|
case String:
|
|
return ['[value]'];
|
|
case Array:
|
|
return ['<list>'];
|
|
case Number:
|
|
case parseInt:
|
|
return ['<n>', parseInt];
|
|
default:
|
|
return [''];
|
|
}
|
|
},
|
|
|
|
readOption(option) {
|
|
let value = option.defaultValue;
|
|
const contents = {};
|
|
|
|
// If option has been used, get its value
|
|
for (const name of option.usage) {
|
|
const fromArgs = this.raw[name];
|
|
if (typeof fromArgs !== 'undefined') {
|
|
value = fromArgs;
|
|
}
|
|
}
|
|
|
|
// Process the option's value
|
|
for (let name of option.usage) {
|
|
let propVal = value;
|
|
|
|
// Convert the value to an array when the option is called just once
|
|
if (
|
|
Array.isArray(option.defaultValue) &&
|
|
typeof propVal !== typeof option.defaultValue
|
|
) {
|
|
propVal = [propVal];
|
|
}
|
|
|
|
if (
|
|
typeof option.defaultValue !== 'undefined' &&
|
|
typeof propVal !== typeof option.defaultValue
|
|
) {
|
|
propVal = option.defaultValue;
|
|
}
|
|
|
|
let condition = true;
|
|
|
|
if (option.init) {
|
|
// Only use the toString initializer if value is a number
|
|
if (option.init === toString) {
|
|
condition = propVal.constructor === Number;
|
|
}
|
|
|
|
if (condition) {
|
|
// Pass it through the initializer
|
|
propVal = option.init(propVal);
|
|
}
|
|
}
|
|
|
|
// Camelcase option name (skip short flag)
|
|
if (name.length > 1) {
|
|
name = camelcase(name);
|
|
}
|
|
|
|
// Add option to list
|
|
contents[name] = propVal;
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
|
|
getOptions(definedSubcommand) {
|
|
const options = {};
|
|
const args = {};
|
|
|
|
// Copy over the arguments
|
|
Object.assign(args, this.raw);
|
|
delete args._;
|
|
|
|
// Set option defaults
|
|
for (const option of this.details.options) {
|
|
if (typeof option.defaultValue === 'undefined') {
|
|
continue;
|
|
}
|
|
|
|
Object.assign(options, this.readOption(option));
|
|
}
|
|
|
|
// Override defaults if used in command line
|
|
for (const option in args) {
|
|
if (!{}.hasOwnProperty.call(args, option)) {
|
|
continue;
|
|
}
|
|
|
|
const related = this.isDefined(option, 'options');
|
|
|
|
if (related) {
|
|
const details = this.readOption(related);
|
|
Object.assign(options, details);
|
|
}
|
|
|
|
if (!related && !definedSubcommand) {
|
|
// Unknown Option
|
|
const availableOptions = [];
|
|
this.details.options.forEach(opt => {
|
|
availableOptions.push.apply(availableOptions, opt.usage);
|
|
});
|
|
|
|
const suggestOption = stringSimilarity.findBestMatch(
|
|
option,
|
|
availableOptions
|
|
);
|
|
|
|
process.stdout.write(`The option "${option}" is unknown.`);
|
|
|
|
if (suggestOption.bestMatch.rating >= 0.5) {
|
|
process.stdout.write(' Did you mean the following one?\n');
|
|
|
|
const suggestion = this.details.options.filter(item => {
|
|
for (const flag of item.usage) {
|
|
if (flag === suggestOption.bestMatch.target) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
process.stdout.write(
|
|
this.generateDetails(suggestion)[0].trim() + '\n'
|
|
);
|
|
|
|
// eslint-disable-next-line unicorn/no-process-exit
|
|
process.exit();
|
|
} else {
|
|
process.stdout.write(` Here's a list of all available options: \n`);
|
|
this.showHelp();
|
|
}
|
|
}
|
|
}
|
|
|
|
return options;
|
|
},
|
|
|
|
generateExamples() {
|
|
const examples = this.details.examples;
|
|
const parts = [];
|
|
|
|
for (const item in examples) {
|
|
if (!{}.hasOwnProperty.call(examples, item)) {
|
|
continue;
|
|
}
|
|
const usage = this.printSubColor('$ ' + examples[item].usage);
|
|
const description = this.printMainColor(
|
|
'- ' + examples[item].description
|
|
);
|
|
parts.push(` ${description}\n\n ${usage}\n\n`);
|
|
}
|
|
|
|
return parts;
|
|
},
|
|
|
|
generateDetails(kind) {
|
|
// Get all properties of kind from global scope
|
|
const items = typeof kind === 'string' ? this.details[kind] : kind;
|
|
const parts = [];
|
|
const isCmd = kind === 'commands';
|
|
|
|
// Sort items alphabetically
|
|
items.sort((a, b) => {
|
|
const first = isCmd ? a.usage : a.usage[1];
|
|
const second = isCmd ? b.usage : b.usage[1];
|
|
|
|
switch (true) {
|
|
case first < second:
|
|
return -1;
|
|
case first > second:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
for (const item in items) {
|
|
if (!{}.hasOwnProperty.call(items, item)) {
|
|
continue;
|
|
}
|
|
|
|
let usage = items[item].usage;
|
|
let initial = items[item].defaultValue;
|
|
|
|
// If usage is an array, show its contents
|
|
if (usage.constructor === Array) {
|
|
if (isCmd) {
|
|
usage = usage.join(', ');
|
|
} else {
|
|
const isVersion = usage.indexOf('v');
|
|
usage = `-${usage[0]}, --${usage[1]}`;
|
|
|
|
if (!initial) {
|
|
initial = items[item].init;
|
|
}
|
|
|
|
usage +=
|
|
initial && isVersion === -1
|
|
? ' ' + this.handleType(initial)[0]
|
|
: '';
|
|
}
|
|
}
|
|
|
|
// Overwrite usage with readable syntax
|
|
items[item].usage = usage;
|
|
}
|
|
|
|
// Find length of longest option or command
|
|
// Before doing that, make a copy of the original array
|
|
const longest = items.slice().sort((a, b) => {
|
|
return b.usage.length - a.usage.length;
|
|
})[0].usage.length;
|
|
|
|
for (const item of items) {
|
|
let usage = item.usage;
|
|
let description = item.description;
|
|
const defaultValue = item.defaultValue;
|
|
const difference = longest - usage.length;
|
|
|
|
// Compensate the difference to longest property with spaces
|
|
usage += ' '.repeat(difference);
|
|
|
|
// Add some space around it as well
|
|
if (typeof defaultValue !== 'undefined') {
|
|
if (typeof defaultValue === 'boolean') {
|
|
description += ` (${defaultValue
|
|
? 'enabled'
|
|
: 'disabled'} by default)`;
|
|
} else {
|
|
description += ` (defaults to ${JSON.stringify(defaultValue)})`;
|
|
}
|
|
}
|
|
parts.push(
|
|
' ' +
|
|
this.printMainColor(usage) +
|
|
' ' +
|
|
this.printSubColor(description)
|
|
);
|
|
}
|
|
|
|
return parts;
|
|
},
|
|
|
|
runCommand(details, options) {
|
|
// If help is disabled, remove initializer
|
|
if (details.usage === 'help' && !this.config.help) {
|
|
details.init = false;
|
|
}
|
|
|
|
// If command has initializer, call it
|
|
if (details.init) {
|
|
const sub = [].concat(this.sub);
|
|
sub.shift();
|
|
|
|
return details.init.bind(this)(details.usage, sub, options);
|
|
}
|
|
|
|
// Generate full name of binary
|
|
const subCommand = Array.isArray(details.usage)
|
|
? details.usage[0]
|
|
: details.usage;
|
|
let full = this.binary + '-' + subCommand;
|
|
|
|
const args = process.argv;
|
|
let i = 0;
|
|
|
|
while (i < 3) {
|
|
args.shift();
|
|
i++;
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
const binaryExt = path.extname(this.binary);
|
|
const mainModule = process.env.APPVEYOR ? '_fixture' : process.mainModule.filename;
|
|
|
|
full = `${mainModule}-${subCommand}`;
|
|
|
|
if (path.extname(this.binary)) {
|
|
full = `${mainModule.replace(binaryExt, '')}-${subCommand}${binaryExt}`;
|
|
}
|
|
// Run binary of sub command on windows
|
|
args.unshift(full);
|
|
this.child = spawn(process.execPath, args, {
|
|
stdio: 'inherit'
|
|
});
|
|
} else {
|
|
// Run binary of sub command
|
|
this.child = spawn(full, args, {
|
|
stdio: 'inherit'
|
|
});
|
|
}
|
|
|
|
// Throw an error if something fails within that binary
|
|
this.child.on('error', err => {
|
|
throw err;
|
|
});
|
|
|
|
this.child.on('exit', (code, signal) => {
|
|
process.on('exit', () => {
|
|
this.child = null;
|
|
if (signal) {
|
|
process.kill(process.pid, signal);
|
|
} else {
|
|
process.exit(code);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Proxy SIGINT to child process
|
|
process.on('SIGINT', () => {
|
|
if (this.child) {
|
|
this.child.kill('SIGINT');
|
|
this.child.kill('SIGTERM'); // If that didn't work, we're probably in an infinite loop, so make it die
|
|
}
|
|
});
|
|
},
|
|
|
|
checkVersion(parent) {
|
|
// Load parent module
|
|
try {
|
|
const pkginfo = require('pkginfo');
|
|
pkginfo(parent);
|
|
} catch (err) {
|
|
// Do nothing, but version could not be aquired
|
|
}
|
|
|
|
// And get its version property
|
|
const version = parent.exports.version || '-/-';
|
|
|
|
if (version) {
|
|
// If it exists, register it as a default option
|
|
this.option('version', 'Output the version number');
|
|
|
|
// And immediately output it if used in command line
|
|
if (this.raw.v || this.raw.version) {
|
|
console.log(version);
|
|
|
|
// eslint-disable-next-line unicorn/no-process-exit
|
|
process.exit();
|
|
}
|
|
}
|
|
},
|
|
|
|
isDefined(name, list) {
|
|
// Get all items of kind
|
|
const children = this.details[list];
|
|
|
|
// Check if a child matches the requested name
|
|
for (const child of children) {
|
|
const usage = child.usage;
|
|
const type = usage.constructor;
|
|
|
|
if (type === Array && usage.indexOf(name) > -1) {
|
|
return child;
|
|
}
|
|
|
|
if (type === String && usage === name) {
|
|
return child;
|
|
}
|
|
}
|
|
|
|
// If nothing matches, item is not defined
|
|
return false;
|
|
}
|
|
};
|
|
|