1'use strict' 2const argsert = require('./argsert') 3const objFilter = require('./obj-filter') 4const specialKeys = ['$0', '--', '_'] 5 6// validation-type-stuff, missing params, 7// bad implications, custom checks. 8module.exports = function validation (yargs, usage, y18n) { 9 const __ = y18n.__ 10 const __n = y18n.__n 11 const self = {} 12 13 // validate appropriate # of non-option 14 // arguments were provided, i.e., '_'. 15 self.nonOptionCount = function nonOptionCount (argv) { 16 const demandedCommands = yargs.getDemandedCommands() 17 // don't count currently executing commands 18 const _s = argv._.length - yargs.getContext().commands.length 19 20 if (demandedCommands._ && (_s < demandedCommands._.min || _s > demandedCommands._.max)) { 21 if (_s < demandedCommands._.min) { 22 if (demandedCommands._.minMsg !== undefined) { 23 usage.fail( 24 // replace $0 with observed, $1 with expected. 25 demandedCommands._.minMsg ? demandedCommands._.minMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.min) : null 26 ) 27 } else { 28 usage.fail( 29 __('Not enough non-option arguments: got %s, need at least %s', _s, demandedCommands._.min) 30 ) 31 } 32 } else if (_s > demandedCommands._.max) { 33 if (demandedCommands._.maxMsg !== undefined) { 34 usage.fail( 35 // replace $0 with observed, $1 with expected. 36 demandedCommands._.maxMsg ? demandedCommands._.maxMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.max) : null 37 ) 38 } else { 39 usage.fail( 40 __('Too many non-option arguments: got %s, maximum of %s', _s, demandedCommands._.max) 41 ) 42 } 43 } 44 } 45 } 46 47 // validate the appropriate # of <required> 48 // positional arguments were provided: 49 self.positionalCount = function positionalCount (required, observed) { 50 if (observed < required) { 51 usage.fail( 52 __('Not enough non-option arguments: got %s, need at least %s', observed, required) 53 ) 54 } 55 } 56 57 // make sure all the required arguments are present. 58 self.requiredArguments = function requiredArguments (argv) { 59 const demandedOptions = yargs.getDemandedOptions() 60 let missing = null 61 62 Object.keys(demandedOptions).forEach((key) => { 63 if (!argv.hasOwnProperty(key) || typeof argv[key] === 'undefined') { 64 missing = missing || {} 65 missing[key] = demandedOptions[key] 66 } 67 }) 68 69 if (missing) { 70 const customMsgs = [] 71 Object.keys(missing).forEach((key) => { 72 const msg = missing[key] 73 if (msg && customMsgs.indexOf(msg) < 0) { 74 customMsgs.push(msg) 75 } 76 }) 77 78 const customMsg = customMsgs.length ? `\n${customMsgs.join('\n')}` : '' 79 80 usage.fail(__n( 81 'Missing required argument: %s', 82 'Missing required arguments: %s', 83 Object.keys(missing).length, 84 Object.keys(missing).join(', ') + customMsg 85 )) 86 } 87 } 88 89 // check for unknown arguments (strict-mode). 90 self.unknownArguments = function unknownArguments (argv, aliases, positionalMap) { 91 const commandKeys = yargs.getCommandInstance().getCommands() 92 const unknown = [] 93 const currentContext = yargs.getContext() 94 95 Object.keys(argv).forEach((key) => { 96 if (specialKeys.indexOf(key) === -1 && 97 !positionalMap.hasOwnProperty(key) && 98 !yargs._getParseContext().hasOwnProperty(key) && 99 !self.isValidAndSomeAliasIsNotNew(key, aliases) 100 ) { 101 unknown.push(key) 102 } 103 }) 104 105 if ((currentContext.commands.length > 0) || (commandKeys.length > 0)) { 106 argv._.slice(currentContext.commands.length).forEach((key) => { 107 if (commandKeys.indexOf(key) === -1) { 108 unknown.push(key) 109 } 110 }) 111 } 112 113 if (unknown.length > 0) { 114 usage.fail(__n( 115 'Unknown argument: %s', 116 'Unknown arguments: %s', 117 unknown.length, 118 unknown.join(', ') 119 )) 120 } 121 } 122 123 // check for a key that is not an alias, or for which every alias is new, 124 // implying that it was invented by the parser, e.g., during camelization 125 self.isValidAndSomeAliasIsNotNew = function isValidAndSomeAliasIsNotNew (key, aliases) { 126 if (!aliases.hasOwnProperty(key)) { 127 return false 128 } 129 const newAliases = yargs.parsed.newAliases 130 for (let a of [key, ...aliases[key]]) { 131 if (!newAliases.hasOwnProperty(a) || !newAliases[key]) { 132 return true 133 } 134 } 135 return false 136 } 137 138 // validate arguments limited to enumerated choices 139 self.limitedChoices = function limitedChoices (argv) { 140 const options = yargs.getOptions() 141 const invalid = {} 142 143 if (!Object.keys(options.choices).length) return 144 145 Object.keys(argv).forEach((key) => { 146 if (specialKeys.indexOf(key) === -1 && 147 options.choices.hasOwnProperty(key)) { 148 [].concat(argv[key]).forEach((value) => { 149 // TODO case-insensitive configurability 150 if (options.choices[key].indexOf(value) === -1 && 151 value !== undefined) { 152 invalid[key] = (invalid[key] || []).concat(value) 153 } 154 }) 155 } 156 }) 157 158 const invalidKeys = Object.keys(invalid) 159 160 if (!invalidKeys.length) return 161 162 let msg = __('Invalid values:') 163 invalidKeys.forEach((key) => { 164 msg += `\n ${__( 165 'Argument: %s, Given: %s, Choices: %s', 166 key, 167 usage.stringifiedValues(invalid[key]), 168 usage.stringifiedValues(options.choices[key]) 169 )}` 170 }) 171 usage.fail(msg) 172 } 173 174 // custom checks, added using the `check` option on yargs. 175 let checks = [] 176 self.check = function check (f, global) { 177 checks.push({ 178 func: f, 179 global 180 }) 181 } 182 183 self.customChecks = function customChecks (argv, aliases) { 184 for (let i = 0, f; (f = checks[i]) !== undefined; i++) { 185 const func = f.func 186 let result = null 187 try { 188 result = func(argv, aliases) 189 } catch (err) { 190 usage.fail(err.message ? err.message : err, err) 191 continue 192 } 193 194 if (!result) { 195 usage.fail(__('Argument check failed: %s', func.toString())) 196 } else if (typeof result === 'string' || result instanceof Error) { 197 usage.fail(result.toString(), result) 198 } 199 } 200 } 201 202 // check implications, argument foo implies => argument bar. 203 let implied = {} 204 self.implies = function implies (key, value) { 205 argsert('<string|object> [array|number|string]', [key, value], arguments.length) 206 207 if (typeof key === 'object') { 208 Object.keys(key).forEach((k) => { 209 self.implies(k, key[k]) 210 }) 211 } else { 212 yargs.global(key) 213 if (!implied[key]) { 214 implied[key] = [] 215 } 216 if (Array.isArray(value)) { 217 value.forEach((i) => self.implies(key, i)) 218 } else { 219 implied[key].push(value) 220 } 221 } 222 } 223 self.getImplied = function getImplied () { 224 return implied 225 } 226 227 function keyExists (argv, val) { 228 // convert string '1' to number 1 229 let num = Number(val) 230 val = isNaN(num) ? val : num 231 232 if (typeof val === 'number') { 233 // check length of argv._ 234 val = argv._.length >= val 235 } else if (val.match(/^--no-.+/)) { 236 // check if key/value doesn't exist 237 val = val.match(/^--no-(.+)/)[1] 238 val = !argv[val] 239 } else { 240 // check if key/value exists 241 val = argv[val] 242 } 243 return val 244 } 245 246 self.implications = function implications (argv) { 247 const implyFail = [] 248 249 Object.keys(implied).forEach((key) => { 250 const origKey = key 251 ;(implied[key] || []).forEach((value) => { 252 let key = origKey 253 const origValue = value 254 key = keyExists(argv, key) 255 value = keyExists(argv, value) 256 257 if (key && !value) { 258 implyFail.push(` ${origKey} -> ${origValue}`) 259 } 260 }) 261 }) 262 263 if (implyFail.length) { 264 let msg = `${__('Implications failed:')}\n` 265 266 implyFail.forEach((value) => { 267 msg += (value) 268 }) 269 270 usage.fail(msg) 271 } 272 } 273 274 let conflicting = {} 275 self.conflicts = function conflicts (key, value) { 276 argsert('<string|object> [array|string]', [key, value], arguments.length) 277 278 if (typeof key === 'object') { 279 Object.keys(key).forEach((k) => { 280 self.conflicts(k, key[k]) 281 }) 282 } else { 283 yargs.global(key) 284 if (!conflicting[key]) { 285 conflicting[key] = [] 286 } 287 if (Array.isArray(value)) { 288 value.forEach((i) => self.conflicts(key, i)) 289 } else { 290 conflicting[key].push(value) 291 } 292 } 293 } 294 self.getConflicting = () => conflicting 295 296 self.conflicting = function conflictingFn (argv) { 297 Object.keys(argv).forEach((key) => { 298 if (conflicting[key]) { 299 conflicting[key].forEach((value) => { 300 // we default keys to 'undefined' that have been configured, we should not 301 // apply conflicting check unless they are a value other than 'undefined'. 302 if (value && argv[key] !== undefined && argv[value] !== undefined) { 303 usage.fail(__('Arguments %s and %s are mutually exclusive', key, value)) 304 } 305 }) 306 } 307 }) 308 } 309 310 self.recommendCommands = function recommendCommands (cmd, potentialCommands) { 311 const distance = require('./levenshtein') 312 const threshold = 3 // if it takes more than three edits, let's move on. 313 potentialCommands = potentialCommands.sort((a, b) => b.length - a.length) 314 315 let recommended = null 316 let bestDistance = Infinity 317 for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) { 318 const d = distance(cmd, candidate) 319 if (d <= threshold && d < bestDistance) { 320 bestDistance = d 321 recommended = candidate 322 } 323 } 324 if (recommended) usage.fail(__('Did you mean %s?', recommended)) 325 } 326 327 self.reset = function reset (localLookup) { 328 implied = objFilter(implied, (k, v) => !localLookup[k]) 329 conflicting = objFilter(conflicting, (k, v) => !localLookup[k]) 330 checks = checks.filter(c => c.global) 331 return self 332 } 333 334 let frozens = [] 335 self.freeze = function freeze () { 336 let frozen = {} 337 frozens.push(frozen) 338 frozen.implied = implied 339 frozen.checks = checks 340 frozen.conflicting = conflicting 341 } 342 self.unfreeze = function unfreeze () { 343 let frozen = frozens.pop() 344 implied = frozen.implied 345 checks = frozen.checks 346 conflicting = frozen.conflicting 347 } 348 349 return self 350} 351