mirror of
https://github.com/Rushilwiz/spaceout.git
synced 2025-04-22 04:19:49 -04:00
205 lines
7.8 KiB
JavaScript
205 lines
7.8 KiB
JavaScript
const commander = require('commander');
|
|
const logger = require('./logger');
|
|
const { commands } = require('./cli-flags');
|
|
const runHelp = require('../groups/runHelp');
|
|
const runVersion = require('../groups/runVersion');
|
|
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('./resolve-command')(defaultCommands[cmd.name], ...cliArgs);
|
|
});
|
|
|
|
return parser;
|
|
}, parser);
|
|
|
|
// Prevent default behavior
|
|
parser.on('command:*', () => {});
|
|
|
|
// Use customized help output
|
|
if (args.includes('--help') || args.includes('help')) {
|
|
runHelp(args);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Use Customized version
|
|
if (args.includes('--version') || args.includes('version') || args.includes('-v')) {
|
|
runVersion(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 if (option.multipleType) {
|
|
// for options which accept multiple types like env
|
|
// so you can do `--env platform=staging --env production`
|
|
// { platform: "staging", production: true }
|
|
const multiArg = (value, previous = {}) => {
|
|
// this ensures we're only splitting by the first `=`
|
|
const [allKeys, val] = value.split(/=(.+)/, 2);
|
|
const splitKeys = allKeys.split(/\.(?!$)/);
|
|
|
|
let prevRef = previous;
|
|
|
|
splitKeys.forEach((someKey, index) => {
|
|
if (!prevRef[someKey]) {
|
|
prevRef[someKey] = {};
|
|
}
|
|
|
|
if ('string' === typeof prevRef[someKey]) {
|
|
prevRef[someKey] = {};
|
|
}
|
|
|
|
if (index === splitKeys.length - 1) {
|
|
prevRef[someKey] = val || true;
|
|
}
|
|
|
|
prevRef = prevRef[someKey];
|
|
});
|
|
|
|
return previous;
|
|
};
|
|
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;
|