1
|
'use strict'
|
2
|
const argsert = require('./argsert')
|
3
|
const objFilter = require('./obj-filter')
|
4
|
const specialKeys = ['$0', '--', '_']
|
5
|
|
6
|
// validation-type-stuff, missing params,
|
7
|
// bad implications, custom checks.
|
8
|
module.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
|
!aliases.hasOwnProperty(key)
|
100
|
) {
|
101
|
unknown.push(key)
|
102
|
}
|
103
|
})
|
104
|
|
105
|
if (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
|
// validate arguments limited to enumerated choices
|
124
|
self.limitedChoices = function limitedChoices (argv) {
|
125
|
const options = yargs.getOptions()
|
126
|
const invalid = {}
|
127
|
|
128
|
if (!Object.keys(options.choices).length) return
|
129
|
|
130
|
Object.keys(argv).forEach((key) => {
|
131
|
if (specialKeys.indexOf(key) === -1 &&
|
132
|
options.choices.hasOwnProperty(key)) {
|
133
|
[].concat(argv[key]).forEach((value) => {
|
134
|
// TODO case-insensitive configurability
|
135
|
if (options.choices[key].indexOf(value) === -1 &&
|
136
|
value !== undefined) {
|
137
|
invalid[key] = (invalid[key] || []).concat(value)
|
138
|
}
|
139
|
})
|
140
|
}
|
141
|
})
|
142
|
|
143
|
const invalidKeys = Object.keys(invalid)
|
144
|
|
145
|
if (!invalidKeys.length) return
|
146
|
|
147
|
let msg = __('Invalid values:')
|
148
|
invalidKeys.forEach((key) => {
|
149
|
msg += `\n ${__(
|
150
|
'Argument: %s, Given: %s, Choices: %s',
|
151
|
key,
|
152
|
usage.stringifiedValues(invalid[key]),
|
153
|
usage.stringifiedValues(options.choices[key])
|
154
|
)}`
|
155
|
})
|
156
|
usage.fail(msg)
|
157
|
}
|
158
|
|
159
|
// custom checks, added using the `check` option on yargs.
|
160
|
let checks = []
|
161
|
self.check = function check (f, global) {
|
162
|
checks.push({
|
163
|
func: f,
|
164
|
global
|
165
|
})
|
166
|
}
|
167
|
|
168
|
self.customChecks = function customChecks (argv, aliases) {
|
169
|
for (let i = 0, f; (f = checks[i]) !== undefined; i++) {
|
170
|
const func = f.func
|
171
|
let result = null
|
172
|
try {
|
173
|
result = func(argv, aliases)
|
174
|
} catch (err) {
|
175
|
usage.fail(err.message ? err.message : err, err)
|
176
|
continue
|
177
|
}
|
178
|
|
179
|
if (!result) {
|
180
|
usage.fail(__('Argument check failed: %s', func.toString()))
|
181
|
} else if (typeof result === 'string' || result instanceof Error) {
|
182
|
usage.fail(result.toString(), result)
|
183
|
}
|
184
|
}
|
185
|
}
|
186
|
|
187
|
// check implications, argument foo implies => argument bar.
|
188
|
let implied = {}
|
189
|
self.implies = function implies (key, value) {
|
190
|
argsert('<string|object> [array|number|string]', [key, value], arguments.length)
|
191
|
|
192
|
if (typeof key === 'object') {
|
193
|
Object.keys(key).forEach((k) => {
|
194
|
self.implies(k, key[k])
|
195
|
})
|
196
|
} else {
|
197
|
yargs.global(key)
|
198
|
if (!implied[key]) {
|
199
|
implied[key] = []
|
200
|
}
|
201
|
if (Array.isArray(value)) {
|
202
|
value.forEach((i) => self.implies(key, i))
|
203
|
} else {
|
204
|
implied[key].push(value)
|
205
|
}
|
206
|
}
|
207
|
}
|
208
|
self.getImplied = function getImplied () {
|
209
|
return implied
|
210
|
}
|
211
|
|
212
|
self.implications = function implications (argv) {
|
213
|
const implyFail = []
|
214
|
|
215
|
Object.keys(implied).forEach((key) => {
|
216
|
const origKey = key
|
217
|
;(implied[key] || []).forEach((value) => {
|
218
|
let num
|
219
|
let key = origKey
|
220
|
const origValue = value
|
221
|
|
222
|
// convert string '1' to number 1
|
223
|
num = Number(key)
|
224
|
key = isNaN(num) ? key : num
|
225
|
|
226
|
if (typeof key === 'number') {
|
227
|
// check length of argv._
|
228
|
key = argv._.length >= key
|
229
|
} else if (key.match(/^--no-.+/)) {
|
230
|
// check if key doesn't exist
|
231
|
key = key.match(/^--no-(.+)/)[1]
|
232
|
key = !argv[key]
|
233
|
} else {
|
234
|
// check if key exists
|
235
|
key = argv[key]
|
236
|
}
|
237
|
|
238
|
num = Number(value)
|
239
|
value = isNaN(num) ? value : num
|
240
|
|
241
|
if (typeof value === 'number') {
|
242
|
value = argv._.length >= value
|
243
|
} else if (value.match(/^--no-.+/)) {
|
244
|
value = value.match(/^--no-(.+)/)[1]
|
245
|
value = !argv[value]
|
246
|
} else {
|
247
|
value = argv[value]
|
248
|
}
|
249
|
if (key && !value) {
|
250
|
implyFail.push(` ${origKey} -> ${origValue}`)
|
251
|
}
|
252
|
})
|
253
|
})
|
254
|
|
255
|
if (implyFail.length) {
|
256
|
let msg = `${__('Implications failed:')}\n`
|
257
|
|
258
|
implyFail.forEach((value) => {
|
259
|
msg += (value)
|
260
|
})
|
261
|
|
262
|
usage.fail(msg)
|
263
|
}
|
264
|
}
|
265
|
|
266
|
let conflicting = {}
|
267
|
self.conflicts = function conflicts (key, value) {
|
268
|
argsert('<string|object> [array|string]', [key, value], arguments.length)
|
269
|
|
270
|
if (typeof key === 'object') {
|
271
|
Object.keys(key).forEach((k) => {
|
272
|
self.conflicts(k, key[k])
|
273
|
})
|
274
|
} else {
|
275
|
yargs.global(key)
|
276
|
if (!conflicting[key]) {
|
277
|
conflicting[key] = []
|
278
|
}
|
279
|
if (Array.isArray(value)) {
|
280
|
value.forEach((i) => self.conflicts(key, i))
|
281
|
} else {
|
282
|
conflicting[key].push(value)
|
283
|
}
|
284
|
}
|
285
|
}
|
286
|
self.getConflicting = () => conflicting
|
287
|
|
288
|
self.conflicting = function conflictingFn (argv) {
|
289
|
Object.keys(argv).forEach((key) => {
|
290
|
if (conflicting[key]) {
|
291
|
conflicting[key].forEach((value) => {
|
292
|
// we default keys to 'undefined' that have been configured, we should not
|
293
|
// apply conflicting check unless they are a value other than 'undefined'.
|
294
|
if (value && argv[key] !== undefined && argv[value] !== undefined) {
|
295
|
usage.fail(__('Arguments %s and %s are mutually exclusive', key, value))
|
296
|
}
|
297
|
})
|
298
|
}
|
299
|
})
|
300
|
}
|
301
|
|
302
|
self.recommendCommands = function recommendCommands (cmd, potentialCommands) {
|
303
|
const distance = require('./levenshtein')
|
304
|
const threshold = 3 // if it takes more than three edits, let's move on.
|
305
|
potentialCommands = potentialCommands.sort((a, b) => b.length - a.length)
|
306
|
|
307
|
let recommended = null
|
308
|
let bestDistance = Infinity
|
309
|
for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
|
310
|
const d = distance(cmd, candidate)
|
311
|
if (d <= threshold && d < bestDistance) {
|
312
|
bestDistance = d
|
313
|
recommended = candidate
|
314
|
}
|
315
|
}
|
316
|
if (recommended) usage.fail(__('Did you mean %s?', recommended))
|
317
|
}
|
318
|
|
319
|
self.reset = function reset (localLookup) {
|
320
|
implied = objFilter(implied, (k, v) => !localLookup[k])
|
321
|
conflicting = objFilter(conflicting, (k, v) => !localLookup[k])
|
322
|
checks = checks.filter(c => c.global)
|
323
|
return self
|
324
|
}
|
325
|
|
326
|
let frozen
|
327
|
self.freeze = function freeze () {
|
328
|
frozen = {}
|
329
|
frozen.implied = implied
|
330
|
frozen.checks = checks
|
331
|
frozen.conflicting = conflicting
|
332
|
}
|
333
|
self.unfreeze = function unfreeze () {
|
334
|
implied = frozen.implied
|
335
|
checks = frozen.checks
|
336
|
conflicting = frozen.conflicting
|
337
|
frozen = undefined
|
338
|
}
|
339
|
|
340
|
return self
|
341
|
}
|