| const objFilter = require('./obj-filter') |
| |
| // validation-type-stuff, missing params, |
| // bad implications, custom checks. |
| module.exports = function (yargs, usage, y18n) { |
| const __ = y18n.__ |
| const __n = y18n.__n |
| const self = {} |
| |
| // validate appropriate # of non-option |
| // arguments were provided, i.e., '_'. |
| self.nonOptionCount = function (argv) { |
| const demandedCommands = yargs.getDemandedCommands() |
| // don't count currently executing commands |
| const _s = argv._.length - yargs.getContext().commands.length |
| |
| if (demandedCommands._ && (_s < demandedCommands._.min || _s > demandedCommands._.max)) { |
| if (_s < demandedCommands._.min) { |
| if (demandedCommands._.minMsg !== undefined) { |
| usage.fail( |
| // replace $0 with observed, $1 with expected. |
| demandedCommands._.minMsg ? demandedCommands._.minMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.min) : null |
| ) |
| } else { |
| usage.fail( |
| __('Not enough non-option arguments: got %s, need at least %s', _s, demandedCommands._.min) |
| ) |
| } |
| } else if (_s > demandedCommands._.max) { |
| if (demandedCommands._.maxMsg !== undefined) { |
| usage.fail( |
| // replace $0 with observed, $1 with expected. |
| demandedCommands._.maxMsg ? demandedCommands._.maxMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.max) : null |
| ) |
| } else { |
| usage.fail( |
| __('Too many non-option arguments: got %s, maximum of %s', _s, demandedCommands._.max) |
| ) |
| } |
| } |
| } |
| } |
| |
| // validate the appropriate # of <required> |
| // positional arguments were provided: |
| self.positionalCount = function (required, observed) { |
| if (observed < required) { |
| usage.fail( |
| __('Not enough non-option arguments: got %s, need at least %s', observed, required) |
| ) |
| } |
| } |
| |
| // make sure that any args that require an |
| // value (--foo=bar), have a value. |
| self.missingArgumentValue = function (argv) { |
| const defaultValues = [true, false, ''] |
| const options = yargs.getOptions() |
| |
| if (options.requiresArg.length > 0) { |
| const missingRequiredArgs = [] |
| |
| options.requiresArg.forEach(function (key) { |
| const value = argv[key] |
| |
| // if a value is explicitly requested, |
| // flag argument as missing if it does not |
| // look like foo=bar was entered. |
| if (~defaultValues.indexOf(value) || |
| (Array.isArray(value) && !value.length)) { |
| missingRequiredArgs.push(key) |
| } |
| }) |
| |
| if (missingRequiredArgs.length > 0) { |
| usage.fail(__n( |
| 'Missing argument value: %s', |
| 'Missing argument values: %s', |
| missingRequiredArgs.length, |
| missingRequiredArgs.join(', ') |
| )) |
| } |
| } |
| } |
| |
| // make sure all the required arguments are present. |
| self.requiredArguments = function (argv) { |
| const demandedOptions = yargs.getDemandedOptions() |
| var missing = null |
| |
| Object.keys(demandedOptions).forEach(function (key) { |
| if (!argv.hasOwnProperty(key) || typeof argv[key] === 'undefined') { |
| missing = missing || {} |
| missing[key] = demandedOptions[key] |
| } |
| }) |
| |
| if (missing) { |
| const customMsgs = [] |
| Object.keys(missing).forEach(function (key) { |
| const msg = missing[key] |
| if (msg && customMsgs.indexOf(msg) < 0) { |
| customMsgs.push(msg) |
| } |
| }) |
| |
| const customMsg = customMsgs.length ? '\n' + customMsgs.join('\n') : '' |
| |
| usage.fail(__n( |
| 'Missing required argument: %s', |
| 'Missing required arguments: %s', |
| Object.keys(missing).length, |
| Object.keys(missing).join(', ') + customMsg |
| )) |
| } |
| } |
| |
| // check for unknown arguments (strict-mode). |
| self.unknownArguments = function (argv, aliases, positionalMap) { |
| const aliasLookup = {} |
| const descriptions = usage.getDescriptions() |
| const demandedOptions = yargs.getDemandedOptions() |
| const commandKeys = yargs.getCommandInstance().getCommands() |
| const unknown = [] |
| const currentContext = yargs.getContext() |
| |
| Object.keys(aliases).forEach(function (key) { |
| aliases[key].forEach(function (alias) { |
| aliasLookup[alias] = key |
| }) |
| }) |
| |
| Object.keys(argv).forEach(function (key) { |
| if (key !== '$0' && key !== '_' && |
| !descriptions.hasOwnProperty(key) && |
| !demandedOptions.hasOwnProperty(key) && |
| !positionalMap.hasOwnProperty(key) && |
| !yargs._getParseContext().hasOwnProperty(key) && |
| !aliasLookup.hasOwnProperty(key)) { |
| unknown.push(key) |
| } |
| }) |
| |
| if (commandKeys.length > 0) { |
| argv._.slice(currentContext.commands.length).forEach(function (key) { |
| if (commandKeys.indexOf(key) === -1) { |
| unknown.push(key) |
| } |
| }) |
| } |
| |
| if (unknown.length > 0) { |
| usage.fail(__n( |
| 'Unknown argument: %s', |
| 'Unknown arguments: %s', |
| unknown.length, |
| unknown.join(', ') |
| )) |
| } |
| } |
| |
| // validate arguments limited to enumerated choices |
| self.limitedChoices = function (argv) { |
| const options = yargs.getOptions() |
| const invalid = {} |
| |
| if (!Object.keys(options.choices).length) return |
| |
| Object.keys(argv).forEach(function (key) { |
| if (key !== '$0' && key !== '_' && |
| options.choices.hasOwnProperty(key)) { |
| [].concat(argv[key]).forEach(function (value) { |
| // TODO case-insensitive configurability |
| if (options.choices[key].indexOf(value) === -1) { |
| invalid[key] = (invalid[key] || []).concat(value) |
| } |
| }) |
| } |
| }) |
| |
| const invalidKeys = Object.keys(invalid) |
| |
| if (!invalidKeys.length) return |
| |
| var msg = __('Invalid values:') |
| invalidKeys.forEach(function (key) { |
| msg += '\n ' + __( |
| 'Argument: %s, Given: %s, Choices: %s', |
| key, |
| usage.stringifiedValues(invalid[key]), |
| usage.stringifiedValues(options.choices[key]) |
| ) |
| }) |
| usage.fail(msg) |
| } |
| |
| // custom checks, added using the `check` option on yargs. |
| var checks = [] |
| self.check = function (f, global) { |
| checks.push({ |
| func: f, |
| global: global |
| }) |
| } |
| |
| self.customChecks = function (argv, aliases) { |
| for (var i = 0, f; (f = checks[i]) !== undefined; i++) { |
| var func = f.func |
| var result = null |
| try { |
| result = func(argv, aliases) |
| } catch (err) { |
| usage.fail(err.message ? err.message : err, err) |
| continue |
| } |
| |
| if (!result) { |
| usage.fail(__('Argument check failed: %s', func.toString())) |
| } else if (typeof result === 'string' || result instanceof Error) { |
| usage.fail(result.toString(), result) |
| } |
| } |
| } |
| |
| // check implications, argument foo implies => argument bar. |
| var implied = {} |
| self.implies = function (key, value) { |
| if (typeof key === 'object') { |
| Object.keys(key).forEach(function (k) { |
| self.implies(k, key[k]) |
| }) |
| } else { |
| yargs.global(key) |
| implied[key] = value |
| } |
| } |
| self.getImplied = function () { |
| return implied |
| } |
| |
| self.implications = function (argv) { |
| const implyFail = [] |
| |
| Object.keys(implied).forEach(function (key) { |
| var num |
| const origKey = key |
| var value = implied[key] |
| |
| // convert string '1' to number 1 |
| num = Number(key) |
| key = isNaN(num) ? key : num |
| |
| if (typeof key === 'number') { |
| // check length of argv._ |
| key = argv._.length >= key |
| } else if (key.match(/^--no-.+/)) { |
| // check if key doesn't exist |
| key = key.match(/^--no-(.+)/)[1] |
| key = !argv[key] |
| } else { |
| // check if key exists |
| key = argv[key] |
| } |
| |
| num = Number(value) |
| value = isNaN(num) ? value : num |
| |
| if (typeof value === 'number') { |
| value = argv._.length >= value |
| } else if (value.match(/^--no-.+/)) { |
| value = value.match(/^--no-(.+)/)[1] |
| value = !argv[value] |
| } else { |
| value = argv[value] |
| } |
| |
| if (key && !value) { |
| implyFail.push(origKey) |
| } |
| }) |
| |
| if (implyFail.length) { |
| var msg = __('Implications failed:') + '\n' |
| |
| implyFail.forEach(function (key) { |
| msg += (' ' + key + ' -> ' + implied[key]) |
| }) |
| |
| usage.fail(msg) |
| } |
| } |
| |
| var conflicting = {} |
| self.conflicts = function (key, value) { |
| if (typeof key === 'object') { |
| Object.keys(key).forEach(function (k) { |
| self.conflicts(k, key[k]) |
| }) |
| } else { |
| yargs.global(key) |
| conflicting[key] = value |
| } |
| } |
| self.getConflicting = function () { |
| return conflicting |
| } |
| |
| self.conflicting = function (argv) { |
| var args = Object.getOwnPropertyNames(argv) |
| |
| args.forEach(function (arg) { |
| if (conflicting[arg] && args.indexOf(conflicting[arg]) !== -1) { |
| usage.fail(__('Arguments %s and %s are mutually exclusive', arg, conflicting[arg])) |
| } |
| }) |
| } |
| |
| self.recommendCommands = function (cmd, potentialCommands) { |
| const distance = require('./levenshtein') |
| const threshold = 3 // if it takes more than three edits, let's move on. |
| potentialCommands = potentialCommands.sort(function (a, b) { return b.length - a.length }) |
| |
| var recommended = null |
| var bestDistance = Infinity |
| for (var i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) { |
| var d = distance(cmd, candidate) |
| if (d <= threshold && d < bestDistance) { |
| bestDistance = d |
| recommended = candidate |
| } |
| } |
| if (recommended) usage.fail(__('Did you mean %s?', recommended)) |
| } |
| |
| self.reset = function (localLookup) { |
| implied = objFilter(implied, function (k, v) { |
| return !localLookup[k] |
| }) |
| conflicting = objFilter(conflicting, function (k, v) { |
| return !localLookup[k] |
| }) |
| checks = checks.filter(function (c) { |
| return c.global |
| }) |
| return self |
| } |
| |
| var frozen |
| self.freeze = function () { |
| frozen = {} |
| frozen.implied = implied |
| frozen.checks = checks |
| frozen.conflicting = conflicting |
| } |
| self.unfreeze = function () { |
| implied = frozen.implied |
| checks = frozen.checks |
| conflicting = frozen.conflicting |
| frozen = undefined |
| } |
| |
| return self |
| } |