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.
160 lines
6.3 KiB
160 lines
6.3 KiB
const commander = require('commander');
|
|
const logger = require('./logger');
|
|
const { commands } = require('./cli-flags');
|
|
const runHelp = require('../groups/runHelp');
|
|
const { defaultCommands } = require('./commands');
|
|
|
|
/**
|
|
* Creates Argument parser corresponding to the supplied options
|
|
* parse the args and return the result
|
|
*
|
|
* @param {object[]} options Array of objects with details about flags
|
|
* @param {string[]} args process.argv or it's subset
|
|
* @param {boolean} argsOnly false if all of process.argv has been provided, true if
|
|
* args is only a subset of process.argv that removes the first couple elements
|
|
*/
|
|
const argParser = (options, args, argsOnly = false, name = '') => {
|
|
const parser = new commander.Command();
|
|
// Set parser name
|
|
parser.name(name);
|
|
parser.storeOptionsAsProperties(false);
|
|
|
|
commands.reduce((parserInstance, cmd) => {
|
|
parser
|
|
.command(cmd.name)
|
|
.alias(cmd.alias)
|
|
.description(cmd.description)
|
|
.usage(cmd.usage)
|
|
.allowUnknownOption(true)
|
|
.action(async () => {
|
|
const cliArgs = args.slice(args.indexOf(cmd.name) + 1 || args.indexOf(cmd.alias) + 1);
|
|
return await require('../commands/resolveCommand')(defaultCommands[cmd.name], ...cliArgs);
|
|
});
|
|
return parser;
|
|
}, parser);
|
|
|
|
// Prevent default behavior
|
|
parser.on('command:*', () => {});
|
|
|
|
// Use customized help output if available
|
|
parser.on('option:help', () => {
|
|
runHelp(args);
|
|
process.exit(0);
|
|
});
|
|
|
|
// Allow execution if unknown arguments are present
|
|
parser.allowUnknownOption(true);
|
|
|
|
// Register options on the parser
|
|
options.reduce((parserInstance, option) => {
|
|
let optionType = option.type;
|
|
let isStringOrBool = false;
|
|
if (Array.isArray(optionType)) {
|
|
// filter out duplicate types
|
|
optionType = optionType.filter((type, index) => {
|
|
return optionType.indexOf(type) === index;
|
|
});
|
|
|
|
// the only multi type currently supported is String and Boolean,
|
|
// if there is a case where a different multi type is needed it
|
|
// must be added here
|
|
if (optionType.length === 0) {
|
|
// if no type is provided in the array fall back to Boolean
|
|
optionType = Boolean;
|
|
} else if (optionType.length === 1 || optionType.length > 2) {
|
|
// treat arrays with 1 or > 2 args as a single type
|
|
optionType = optionType[0];
|
|
} else {
|
|
// only String and Boolean multi type is supported
|
|
if (optionType.includes(Boolean) && optionType.includes(String)) {
|
|
isStringOrBool = true;
|
|
} else {
|
|
optionType = optionType[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
const flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`;
|
|
let flagsWithType = flags;
|
|
if (isStringOrBool) {
|
|
// commander recognizes [value] as an optional placeholder,
|
|
// making this flag work either as a string or a boolean
|
|
flagsWithType = `${flags} [value]`;
|
|
} else if (optionType !== Boolean) {
|
|
// <value> is a required placeholder for any non-Boolean types
|
|
flagsWithType = `${flags} <value>`;
|
|
}
|
|
|
|
if (isStringOrBool || optionType === Boolean || optionType === String) {
|
|
if (option.multiple) {
|
|
// a multiple argument parsing function
|
|
const multiArg = (value, previous = []) => previous.concat([value]);
|
|
parserInstance.option(flagsWithType, option.description, multiArg, option.defaultValue).action(() => {});
|
|
} else {
|
|
// Prevent default behavior for standalone options
|
|
parserInstance.option(flagsWithType, option.description, option.defaultValue).action(() => {});
|
|
}
|
|
} else if (optionType === Number) {
|
|
// this will parse the flag as a number
|
|
parserInstance.option(flagsWithType, option.description, Number, option.defaultValue);
|
|
} else {
|
|
// in this case the type is a parsing function
|
|
parserInstance.option(flagsWithType, option.description, optionType, option.defaultValue).action(() => {});
|
|
}
|
|
|
|
if (option.negative) {
|
|
// commander requires explicitly adding the negated version of boolean flags
|
|
const negatedFlag = `--no-${option.name}`;
|
|
parserInstance.option(negatedFlag, `negates ${option.name}`).action(() => {});
|
|
}
|
|
|
|
return parserInstance;
|
|
}, parser);
|
|
|
|
// if we are parsing a subset of process.argv that includes
|
|
// only the arguments themselves (e.g. ['--option', 'value'])
|
|
// then we need from: 'user' passed into commander parse
|
|
// otherwise we are parsing a full process.argv
|
|
// (e.g. ['node', '/path/to/...', '--option', 'value'])
|
|
const parseOptions = argsOnly ? { from: 'user' } : {};
|
|
|
|
const result = parser.parse(args, parseOptions);
|
|
const opts = result.opts();
|
|
|
|
const unknownArgs = result.args;
|
|
args.forEach((arg) => {
|
|
const flagName = arg.slice(5);
|
|
const option = options.find((opt) => opt.name === flagName);
|
|
const flag = `--${flagName}`;
|
|
const flagUsed = args.includes(flag) && !unknownArgs.includes(flag);
|
|
let alias = '';
|
|
let aliasUsed = false;
|
|
if (option && option.alias) {
|
|
alias = `-${option.alias}`;
|
|
aliasUsed = args.includes(alias) && !unknownArgs.includes(alias);
|
|
}
|
|
|
|
// this is a negated flag that is not an unknown flag, but the flag
|
|
// it is negating was also provided
|
|
if (arg.startsWith('--no-') && (flagUsed || aliasUsed) && !unknownArgs.includes(arg)) {
|
|
logger.warn(
|
|
`You provided both ${
|
|
flagUsed ? flag : alias
|
|
} and ${arg}. We will use only the last of these flags that you provided in your CLI arguments`,
|
|
);
|
|
}
|
|
});
|
|
|
|
Object.keys(opts).forEach((key) => {
|
|
if (opts[key] === undefined) {
|
|
delete opts[key];
|
|
}
|
|
});
|
|
|
|
return {
|
|
unknownArgs,
|
|
opts,
|
|
};
|
|
};
|
|
|
|
module.exports = argParser;
|
|
|