const _ = require('lodash'); module.exports = { cast: { /** * Helper to coerce number like strings into integers. * Perform safety checks, and return the result. * * @param {String} arg - The stringified number argument. * @returns {Number} - The supplied argument, casted to an integer. */ integer: (arg) => { const num = Number(arg); if (!_.isSafeInteger(num) || num <= 0) { throw new Error('The value must be a positive integer.'); } return num.valueOf(); }, /** * Helper for collecting argument passed multiple times. * * --folder f1 --folder f2 * * @param {String} val - The argument value. * @param {String[]} memo - The array that is populated by argument values. * @returns {String[]} - The array of argument values collected. */ memoize: (val, memo) => { memo.push(val); return memo; }, /** * Helper for collecting argument passed multiple times as key=value. * * --global-var "foo=bar" --global-var "alpha=beta" * * @param {String} val - The argument value, passed as key=value. * @param {Array} memo - The array that is populated by key value pairs. * @returns {Array} - [{key, value}] - The object representation of the current CLI variable. */ memoizeKeyVal: (val, memo) => { let arg, eqIndex = val.indexOf('='); // This is done instead of splitting by `=` to avoid chopping off `=` that could be present in the value arg = eqIndex !== -1 ? { key: val.slice(0, eqIndex), value: val.slice(eqIndex + 1) } : { key: val, value: undefined }; memo.push(arg); return memo; }, /** * Helper to coerce comma separated string to an array. * * eg. item1,item2 * * @param {String} list - The comma separated string. * @returns {String[]} - [item1, item2] - The array representation of the passed string. */ csvParse: (list) => { return _.split(list, ','); }, colorOptions: (value) => { if (!(/^(auto|on|off)$/).test(value)) { throw new Error(`invalid value \`${value}\` for --color. Expected: auto|on|off`); } return value; } }, /** * Extract selected options in the provided command. * Omits commander private variables and other objects. * * @param {Object} command - Commander.Command Instance * @returns {Object} - Extracted options from command */ commanderToObject: (command) => { return _.reduce(command, (result, value, key) => { // Exclude command's private `_` variables and other objects const validProp = !_.startsWith(key, '_') && !_.includes(['commands', 'options', 'parent'], key); validProp && (result[key] = value); return result; }, {}); }, /** * Remove nested options having the provided prefix from `process.argv`. * * @param {String[]} argv - Argument vector. * @param {String} optionPrefix - Argument prefix to search for. * @returns {String[]} - All arguments without prefixed options and their values */ omitNestedOptions: (argv, optionPrefix) => { let args = [], len, i; for (i = 0, len = argv.length; i < len; i++) { if (!_.startsWith(argv[i], optionPrefix)) { args.push(argv[i]); continue; } // For prefixed args also omit its value, --prefix-arg value if (argv[i + 1] && !_.startsWith(argv[i + 1], '-')) { ++i; } } return args; }, /** * Parse nested options having the provided prefix from `process.argv`. * * @param {String[]} argv - Argument vector. * @param {String} optionPrefix - Argument prefix to search for. * @param {String[]} options - Selected options. * @returns {Object} Parsed object with nested options. * * @example * let argv = ['--reporters=json,html', '--reporter-html-template=template.hb', '--reporter-export=path'], * options = ['json', 'html']; * parseNestedOptions(argv, '--reporter-', options); * //returns * { * _generic: { export: path }, * html: { template: template.hb }, * json: {} * } * */ parseNestedOptions: (argv, optionPrefix, options) => { let args = [], parsed = { _generic: {} }, name, path, len, eqIndex, i; // Extract prefixed arguments from argv for (i = 0, len = argv.length; i < len; i++) { const arg = argv[i]; if (!_.startsWith(arg, optionPrefix)) { continue; } // skip non-prefixed args eqIndex = arg.indexOf('='); if (eqIndex !== -1) { // Split the attribute if its like key=value args.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1)); } else if (argv[i + 1] && !_.startsWith(argv[i + 1], '-')) { // Also push the next parameter if it's not an option. args.push(arg, argv[++i]); } else { args.push(arg); } } // ensure that whatever option is provided a blank options object is forwarded _.forEach(options, (option) => { parsed[option] = {}; }); // Parse nested options for (i = 0, len = args.length; i < len; i++) { const arg = args[i].replace(optionPrefix, ''); name = _.split(arg, '-', 1)[0]; // eg. `cli` in --reporter-cli-silent // if we have a valid option, the path should be the .camelCaseOfTheRest // otherwise, we add it to the generic options. path = _.includes(options, name) ? [name, _.camelCase(arg.replace(name + '-', ''))].join('.') : ['_generic', _.camelCase(arg)].join('.'); // If the next arg is an option, set the current arg to true, // otherwise set it to the next arg. _.set(parsed, path, (!args[i + 1] || _.startsWith(args[i + 1], '-')) ? true : args[++i]); } return parsed; } };