| 'use strict' |
| const argsert = require('./argsert') |
| const objFilter = require('./obj-filter') |
| const specialKeys = ['$0', '--', '_'] |
| |
| // validation-type-stuff, missing params, |
| // bad implications, custom checks. |
| module.exports = function validation (yargs, usage, y18n) { |
| const __ = y18n.__ |
| const __n = y18n.__n |
| const self = {} |
| |
| // validate appropriate # of non-option |
| // arguments were provided, i.e., '_'. |
| self.nonOptionCount = function nonOptionCount (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 positionalCount (required, observed) { |
| if (observed < required) { |
| usage.fail( |
| __('Not enough non-option arguments: got %s, need at least %s', observed, required) |
| ) |
| } |
| } |
| |
| // make sure all the required arguments are present. |
| self.requiredArguments = function requiredArguments (argv) { |
| const demandedOptions = yargs.getDemandedOptions() |
| let missing = null |
| |
| Object.keys(demandedOptions).forEach((key) => { |
| if (!argv.hasOwnProperty(key) || typeof argv[key] === 'undefined') { |
| missing = missing || {} |
| missing[key] = demandedOptions[key] |
| } |
| }) |
| |
| if (missing) { |
| const customMsgs = [] |
| Object.keys(missing).forEach((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 unknownArguments (argv, aliases, positionalMap) { |
| const commandKeys = yargs.getCommandInstance().getCommands() |
| const unknown = [] |
| const currentContext = yargs.getContext() |
| |
| Object.keys(argv).forEach((key) => { |
| if (specialKeys.indexOf(key) === -1 && |
| !positionalMap.hasOwnProperty(key) && |
| !yargs._getParseContext().hasOwnProperty(key) && |
| !self.isValidAndSomeAliasIsNotNew(key, aliases) |
| ) { |
| unknown.push(key) |
| } |
| }) |
| |
| if ((currentContext.commands.length > 0) || (commandKeys.length > 0)) { |
| argv._.slice(currentContext.commands.length).forEach((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(', ') |
| )) |
| } |
| } |
| |
| // check for a key that is not an alias, or for which every alias is new, |
| // implying that it was invented by the parser, e.g., during camelization |
| self.isValidAndSomeAliasIsNotNew = function isValidAndSomeAliasIsNotNew (key, aliases) { |
| if (!aliases.hasOwnProperty(key)) { |
| return false |
| } |
| const newAliases = yargs.parsed.newAliases |
| for (let a of [key, ...aliases[key]]) { |
| if (!newAliases.hasOwnProperty(a) || !newAliases[key]) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // validate arguments limited to enumerated choices |
| self.limitedChoices = function limitedChoices (argv) { |
| const options = yargs.getOptions() |
| const invalid = {} |
| |
| if (!Object.keys(options.choices).length) return |
| |
| Object.keys(argv).forEach((key) => { |
| if (specialKeys.indexOf(key) === -1 && |
| options.choices.hasOwnProperty(key)) { |
| [].concat(argv[key]).forEach((value) => { |
| // TODO case-insensitive configurability |
| if (options.choices[key].indexOf(value) === -1 && |
| value !== undefined) { |
| invalid[key] = (invalid[key] || []).concat(value) |
| } |
| }) |
| } |
| }) |
| |
| const invalidKeys = Object.keys(invalid) |
| |
| if (!invalidKeys.length) return |
| |
| let msg = __('Invalid values:') |
| invalidKeys.forEach((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. |
| let checks = [] |
| self.check = function check (f, global) { |
| checks.push({ |
| func: f, |
| global |
| }) |
| } |
| |
| self.customChecks = function customChecks (argv, aliases) { |
| for (let i = 0, f; (f = checks[i]) !== undefined; i++) { |
| const func = f.func |
| let 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. |
| let implied = {} |
| self.implies = function implies (key, value) { |
| argsert('<string|object> [array|number|string]', [key, value], arguments.length) |
| |
| if (typeof key === 'object') { |
| Object.keys(key).forEach((k) => { |
| self.implies(k, key[k]) |
| }) |
| } else { |
| yargs.global(key) |
| if (!implied[key]) { |
| implied[key] = [] |
| } |
| if (Array.isArray(value)) { |
| value.forEach((i) => self.implies(key, i)) |
| } else { |
| implied[key].push(value) |
| } |
| } |
| } |
| self.getImplied = function getImplied () { |
| return implied |
| } |
| |
| function keyExists (argv, val) { |
| // convert string '1' to number 1 |
| let num = Number(val) |
| val = isNaN(num) ? val : num |
| |
| if (typeof val === 'number') { |
| // check length of argv._ |
| val = argv._.length >= val |
| } else if (val.match(/^--no-.+/)) { |
| // check if key/value doesn't exist |
| val = val.match(/^--no-(.+)/)[1] |
| val = !argv[val] |
| } else { |
| // check if key/value exists |
| val = argv[val] |
| } |
| return val |
| } |
| |
| self.implications = function implications (argv) { |
| const implyFail = [] |
| |
| Object.keys(implied).forEach((key) => { |
| const origKey = key |
| ;(implied[key] || []).forEach((value) => { |
| let key = origKey |
| const origValue = value |
| key = keyExists(argv, key) |
| value = keyExists(argv, value) |
| |
| if (key && !value) { |
| implyFail.push(` ${origKey} -> ${origValue}`) |
| } |
| }) |
| }) |
| |
| if (implyFail.length) { |
| let msg = `${__('Implications failed:')}\n` |
| |
| implyFail.forEach((value) => { |
| msg += (value) |
| }) |
| |
| usage.fail(msg) |
| } |
| } |
| |
| let conflicting = {} |
| self.conflicts = function conflicts (key, value) { |
| argsert('<string|object> [array|string]', [key, value], arguments.length) |
| |
| if (typeof key === 'object') { |
| Object.keys(key).forEach((k) => { |
| self.conflicts(k, key[k]) |
| }) |
| } else { |
| yargs.global(key) |
| if (!conflicting[key]) { |
| conflicting[key] = [] |
| } |
| if (Array.isArray(value)) { |
| value.forEach((i) => self.conflicts(key, i)) |
| } else { |
| conflicting[key].push(value) |
| } |
| } |
| } |
| self.getConflicting = () => conflicting |
| |
| self.conflicting = function conflictingFn (argv) { |
| Object.keys(argv).forEach((key) => { |
| if (conflicting[key]) { |
| conflicting[key].forEach((value) => { |
| // we default keys to 'undefined' that have been configured, we should not |
| // apply conflicting check unless they are a value other than 'undefined'. |
| if (value && argv[key] !== undefined && argv[value] !== undefined) { |
| usage.fail(__('Arguments %s and %s are mutually exclusive', key, value)) |
| } |
| }) |
| } |
| }) |
| } |
| |
| self.recommendCommands = function recommendCommands (cmd, potentialCommands) { |
| const distance = require('./levenshtein') |
| const threshold = 3 // if it takes more than three edits, let's move on. |
| potentialCommands = potentialCommands.sort((a, b) => b.length - a.length) |
| |
| let recommended = null |
| let bestDistance = Infinity |
| for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) { |
| const 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 reset (localLookup) { |
| implied = objFilter(implied, (k, v) => !localLookup[k]) |
| conflicting = objFilter(conflicting, (k, v) => !localLookup[k]) |
| checks = checks.filter(c => c.global) |
| return self |
| } |
| |
| let frozens = [] |
| self.freeze = function freeze () { |
| let frozen = {} |
| frozens.push(frozen) |
| frozen.implied = implied |
| frozen.checks = checks |
| frozen.conflicting = conflicting |
| } |
| self.unfreeze = function unfreeze () { |
| let frozen = frozens.pop() |
| implied = frozen.implied |
| checks = frozen.checks |
| conflicting = frozen.conflicting |
| } |
| |
| return self |
| } |