249 lines
7.1 KiB
JavaScript
249 lines
7.1 KiB
JavaScript
'use strict';
|
|
|
|
const os = require('os');
|
|
const yargsParser = require('yargs-parser');
|
|
const { prepareArgs, prepareArgsAndCall } = require('object-to-arguments');
|
|
const { inspectParameters, getParametersNames } = require('inspect-parameters-declaration');
|
|
const pipeFn = require('pipe-functions');
|
|
const getStdin = require('get-stdin');
|
|
const getHelp = require('./cli-help');
|
|
const deepMerge = require('deepmerge')
|
|
|
|
const defaultOptions = {
|
|
command: {
|
|
subcommandsDelimiter: undefined
|
|
},
|
|
options: {
|
|
validateRequiredParameters: false
|
|
},
|
|
version: {
|
|
option: 'version'
|
|
},
|
|
help: {
|
|
option: 'help',
|
|
stripAnsi: false
|
|
}
|
|
};
|
|
|
|
function callCommand(command, parsedArgs, optionsPipe) {
|
|
if (command && (command.value || command.action)) {
|
|
if (command.value) {
|
|
return execCommandPipeline(() => command.value, parsedArgs, command.pipe, optionsPipe);
|
|
}
|
|
|
|
return execCommandPipeline(command.action, parsedArgs, command.pipe, optionsPipe);
|
|
}
|
|
}
|
|
|
|
function removeEOL(str) {
|
|
return str.replace(RegExp(`${os.EOL}$`), '');
|
|
}
|
|
|
|
function checkStdin(stdinFn, stdinValue, parsedArgs, positionalArgs, argsAfterEndOfOptions) {
|
|
if (stdinValue === '') {
|
|
return parsedArgs;
|
|
}
|
|
|
|
stdinValue = removeEOL(stdinValue);
|
|
return stdinFn(stdinValue, parsedArgs, positionalArgs, argsAfterEndOfOptions);
|
|
}
|
|
|
|
function execCommandPipeline(command, parsedArgs, commandPipe = {}, optionsPipe = {}) {
|
|
const positionalArgs = parsedArgs._;
|
|
const argsAfterEndOfOptions = parsedArgs['--'];
|
|
delete parsedArgs._;
|
|
delete parsedArgs['--'];
|
|
|
|
let pipelineArray = [];
|
|
|
|
// Wait for stdin or start passing in parsedArgs
|
|
let pipeStdin = commandPipe.stdin || optionsPipe.stdin;
|
|
if (pipeStdin) {
|
|
pipelineArray.push(getStdin);
|
|
pipelineArray.push(stdinValue => checkStdin(pipeStdin, stdinValue, parsedArgs, positionalArgs, argsAfterEndOfOptions));
|
|
} else {
|
|
pipelineArray.push(parsedArgs);
|
|
}
|
|
|
|
// Options Before
|
|
if (optionsPipe.before) {
|
|
pipelineArray.push(args => optionsPipe.before(args, positionalArgs, argsAfterEndOfOptions));
|
|
}
|
|
|
|
// Command Before
|
|
if (commandPipe.before) {
|
|
pipelineArray.push(args => commandPipe.before(args, positionalArgs, argsAfterEndOfOptions));
|
|
}
|
|
|
|
// Main Command
|
|
pipelineArray.push(args => prepareArgsAndCall(command, args, ...positionalArgs));
|
|
|
|
// Command After
|
|
if (commandPipe.after) {
|
|
pipelineArray.push(result => commandPipe.after(result, parsedArgs, positionalArgs, argsAfterEndOfOptions));
|
|
}
|
|
|
|
// Options After
|
|
if (optionsPipe.after) {
|
|
pipelineArray.push(result => optionsPipe.after(result, parsedArgs, positionalArgs, argsAfterEndOfOptions));
|
|
}
|
|
|
|
// Exec pipeline
|
|
const pipeResult = pipeFn(...pipelineArray);
|
|
|
|
// Output Promises
|
|
if (pipeResult && pipeResult.then) {
|
|
return pipeResult
|
|
.then(result => result !== undefined && console.log(result))
|
|
.catch(console.error);
|
|
}
|
|
|
|
// console.log('pipeResult', command\);
|
|
|
|
// Output
|
|
if (pipeResult !== undefined) {
|
|
console.log(pipeResult);
|
|
}
|
|
}
|
|
|
|
function matchCommandFromArgs(args, command, commandPath = []) {
|
|
commandPath.push(command.name);
|
|
|
|
if (command.commands) {
|
|
for (let subcommand of command.commands) {
|
|
if (subcommand.name === args[0]) {
|
|
return matchCommandFromArgs(args.slice(1), subcommand, commandPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
command.commandPath = commandPath;
|
|
return { command, args };
|
|
}
|
|
|
|
function matchDelimitedSubcommand(subcommandArg = [], command, subcommandDelimiter, commandPath = []) {
|
|
subcommandArg = subcommandArg.constructor === Array ? subcommandArg : subcommandArg.split(subcommandDelimiter);
|
|
|
|
for (let subcommand of command.commands) {
|
|
if (subcommand.name === subcommandArg[0]) {
|
|
const nextSubcommand = subcommandArg.slice(1);
|
|
if (nextSubcommand.length) {
|
|
return matchDelimitedSubcommand(nextSubcommand, subcommand, subcommandDelimiter, commandPath);
|
|
} else {
|
|
return subcommand;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function matchCommand(args, command, subcommandsDelimiter) {
|
|
|
|
if (subcommandsDelimiter && command.commands) {
|
|
const matchedSubcommand = matchDelimitedSubcommand(args[0], command, subcommandsDelimiter);
|
|
if (matchedSubcommand) {
|
|
const commandPath = [command.name].concat(args[0].split(subcommandsDelimiter));
|
|
command = matchedSubcommand;
|
|
args = args.slice(1);
|
|
|
|
command.commandPath = commandPath;
|
|
return { command, args };
|
|
}
|
|
}
|
|
|
|
return matchCommandFromArgs(args.slice(0), command);
|
|
}
|
|
|
|
function buildOptionsFromParametersAndSpec(action, optionsSpec) {
|
|
// Build options from functions parameters and merge with options spec
|
|
return getParametersNames(action).map(parameter => {
|
|
const option = { name: parameter };
|
|
|
|
const [, variadic] = parameter.match(/^\.{3}(.*)/) || [];
|
|
if (variadic) {
|
|
option.name = variadic;
|
|
option.variadic = true;
|
|
}
|
|
|
|
const optionSpec = (optionsSpec && optionsSpec.find(optionSpec => optionSpec.name === option.name)) || {};
|
|
return Object.assign(option, optionSpec);
|
|
});
|
|
}
|
|
|
|
function buildYargsParserOptions(options) {
|
|
return options.reduce((result, option) => {
|
|
// yargsParser option type
|
|
if (option.type) {
|
|
const optionType = option.type.toString().toLowerCase();
|
|
result[optionType] = result[optionType] || [];
|
|
result[optionType].push(option.name);
|
|
}
|
|
|
|
// yargsParser option alias
|
|
if (option.alias) {
|
|
result.alias = result.alias || {};
|
|
result.alias[option.name] = option.alias
|
|
}
|
|
|
|
return result;
|
|
}, {});
|
|
}
|
|
|
|
function parseArgs(args, parserOptions) {
|
|
return yargsParser(args, Object.assign({
|
|
configuration: {
|
|
'boolean-negation': false,
|
|
'populate--': true
|
|
}
|
|
}, parserOptions));
|
|
}
|
|
|
|
function cliss(cliSpec, clissOptions = {}, argv) {
|
|
if (cliSpec.constructor === Function) {
|
|
cliSpec = {
|
|
action: cliSpec
|
|
};
|
|
}
|
|
|
|
clissOptions = deepMerge(defaultOptions, clissOptions);
|
|
|
|
const argsArray = argv || process.argv.slice(2);
|
|
|
|
const { command, args } = matchCommand(argsArray.slice(0), cliSpec, clissOptions.command.subcommandsDelimiter);
|
|
|
|
command.options = buildOptionsFromParametersAndSpec(command.action, command.options);
|
|
|
|
const yargsParserOptions = buildYargsParserOptions(command.options);
|
|
const parsedArgs = parseArgs(args, yargsParserOptions);
|
|
|
|
// Show version if --version is passed in (disabled if { versionOption: falsey })
|
|
if ((clissOptions.version.option && parsedArgs[clissOptions.version.option]) && (command.version || cliSpec.version)) {
|
|
return console.log(command.version || cliSpec.version);
|
|
}
|
|
|
|
const helpOptions = Object.assign({}, clissOptions.help, { subcommandsDelimiter: clissOptions.command.subcommandsDelimiter });
|
|
|
|
// Show help if the command doesn't have an action or value
|
|
if (!command.action && !command.value) {
|
|
return console.log(getHelp(command, helpOptions));
|
|
}
|
|
|
|
// Show help if the option says to validateRequiredParameters and some of then are not being passed in
|
|
if (clissOptions.options.validateRequiredParameters) {
|
|
const everyRequiredOptionsAreSet = command.options.filter(option => option.required).every(requiredOption => {
|
|
return parsedArgs[requiredOption.name] !== undefined;
|
|
});
|
|
|
|
if (!everyRequiredOptionsAreSet) {
|
|
return console.log(getHelp(command, helpOptions));
|
|
}
|
|
}
|
|
|
|
// Show help if --help is passed in
|
|
if (clissOptions.help.option && parsedArgs[clissOptions.help.option]) {
|
|
return console.log(getHelp(command, helpOptions));
|
|
}
|
|
|
|
callCommand(command, parsedArgs, clissOptions.pipe);
|
|
}
|
|
|
|
module.exports = cliss; |