Files

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;