UNPKG

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