| '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 | |
| } |