UNPKG

11.2 kBJavaScriptView Raw
1var abbrev = require('abbrev')
2const debug = require('./debug')
3const defaultTypeDefs = require('./type-defs')
4
5function nopt (args, { types, shorthands, typeDefs, invalidHandler }) {
6 debug(types, shorthands, args, typeDefs)
7
8 var data = {}
9 var argv = {
10 remain: [],
11 cooked: args,
12 original: args.slice(0),
13 }
14
15 parse(args, data, argv.remain, { typeDefs, types, shorthands })
16
17 // now data is full
18 clean(data, { types, typeDefs, invalidHandler })
19 data.argv = argv
20
21 Object.defineProperty(data.argv, 'toString', {
22 value: function () {
23 return this.original.map(JSON.stringify).join(' ')
24 },
25 enumerable: false,
26 })
27
28 return data
29}
30
31function clean (data, { types, typeDefs, invalidHandler }) {
32 const StringType = typeDefs.String.type
33 const NumberType = typeDefs.Number.type
34 const ArrayType = typeDefs.Array.type
35 const BooleanType = typeDefs.Boolean.type
36 const DateType = typeDefs.Date.type
37
38 var remove = {}
39 var typeDefault = [false, true, null, StringType, ArrayType]
40
41 Object.keys(data).forEach(function (k) {
42 if (k === 'argv') {
43 return
44 }
45 var val = data[k]
46 var isArray = Array.isArray(val)
47 var type = types[k]
48 if (!isArray) {
49 val = [val]
50 }
51 if (!type) {
52 type = typeDefault
53 }
54 if (type === ArrayType) {
55 type = typeDefault.concat(ArrayType)
56 }
57 if (!Array.isArray(type)) {
58 type = [type]
59 }
60
61 debug('val=%j', val)
62 debug('types=', type)
63 val = val.map(function (v) {
64 // if it's an unknown value, then parse false/true/null/numbers/dates
65 if (typeof v === 'string') {
66 debug('string %j', v)
67 v = v.trim()
68 if ((v === 'null' && ~type.indexOf(null))
69 || (v === 'true' &&
70 (~type.indexOf(true) || ~type.indexOf(BooleanType)))
71 || (v === 'false' &&
72 (~type.indexOf(false) || ~type.indexOf(BooleanType)))) {
73 v = JSON.parse(v)
74 debug('jsonable %j', v)
75 } else if (~type.indexOf(NumberType) && !isNaN(v)) {
76 debug('convert to number', v)
77 v = +v
78 } else if (~type.indexOf(DateType) && !isNaN(Date.parse(v))) {
79 debug('convert to date', v)
80 v = new Date(v)
81 }
82 }
83
84 if (!Object.prototype.hasOwnProperty.call(types, k)) {
85 return v
86 }
87
88 // allow `--no-blah` to set 'blah' to null if null is allowed
89 if (v === false && ~type.indexOf(null) &&
90 !(~type.indexOf(false) || ~type.indexOf(BooleanType))) {
91 v = null
92 }
93
94 var d = {}
95 d[k] = v
96 debug('prevalidated val', d, v, types[k])
97 if (!validate(d, k, v, types[k], { typeDefs })) {
98 if (invalidHandler) {
99 invalidHandler(k, v, types[k], data)
100 } else if (invalidHandler !== false) {
101 debug('invalid: ' + k + '=' + v, types[k])
102 }
103 return remove
104 }
105 debug('validated v', d, v, types[k])
106 return d[k]
107 }).filter(function (v) {
108 return v !== remove
109 })
110
111 // if we allow Array specifically, then an empty array is how we
112 // express 'no value here', not null. Allow it.
113 if (!val.length && type.indexOf(ArrayType) === -1) {
114 debug('VAL HAS NO LENGTH, DELETE IT', val, k, type.indexOf(ArrayType))
115 delete data[k]
116 } else if (isArray) {
117 debug(isArray, data[k], val)
118 data[k] = val
119 } else {
120 data[k] = val[0]
121 }
122
123 debug('k=%s val=%j', k, val, data[k])
124 })
125}
126
127function validate (data, k, val, type, { typeDefs }) {
128 const ArrayType = typeDefs.Array.type
129 // arrays are lists of types.
130 if (Array.isArray(type)) {
131 for (let i = 0, l = type.length; i < l; i++) {
132 if (type[i] === ArrayType) {
133 continue
134 }
135 if (validate(data, k, val, type[i], { typeDefs })) {
136 return true
137 }
138 }
139 delete data[k]
140 return false
141 }
142
143 // an array of anything?
144 if (type === ArrayType) {
145 return true
146 }
147
148 // Original comment:
149 // NaN is poisonous. Means that something is not allowed.
150 // New comment: Changing this to an isNaN check breaks a lot of tests.
151 // Something is being assumed here that is not actually what happens in
152 // practice. Fixing it is outside the scope of getting linting to pass in
153 // this repo. Leaving as-is for now.
154 /* eslint-disable-next-line no-self-compare */
155 if (type !== type) {
156 debug('Poison NaN', k, val, type)
157 delete data[k]
158 return false
159 }
160
161 // explicit list of values
162 if (val === type) {
163 debug('Explicitly allowed %j', val)
164 data[k] = val
165 return true
166 }
167
168 // now go through the list of typeDefs, validate against each one.
169 var ok = false
170 var types = Object.keys(typeDefs)
171 for (let i = 0, l = types.length; i < l; i++) {
172 debug('test type %j %j %j', k, val, types[i])
173 var t = typeDefs[types[i]]
174 if (t && (
175 (type && type.name && t.type && t.type.name) ?
176 (type.name === t.type.name) :
177 (type === t.type)
178 )) {
179 var d = {}
180 ok = t.validate(d, k, val) !== false
181 val = d[k]
182 if (ok) {
183 data[k] = val
184 break
185 }
186 }
187 }
188 debug('OK? %j (%j %j %j)', ok, k, val, types[types.length - 1])
189
190 if (!ok) {
191 delete data[k]
192 }
193 return ok
194}
195
196function parse (args, data, remain, { typeDefs, types, shorthands }) {
197 const StringType = typeDefs.String.type
198 const NumberType = typeDefs.String.type
199 const ArrayType = typeDefs.Array.type
200 const BooleanType = typeDefs.Boolean.type
201
202 debug('parse', args, data, remain)
203
204 var abbrevs = abbrev(Object.keys(types))
205 var shortAbbr = abbrev(Object.keys(shorthands))
206
207 for (var i = 0; i < args.length; i++) {
208 var arg = args[i]
209 debug('arg', arg)
210
211 if (arg.match(/^-{2,}$/)) {
212 // done with keys.
213 // the rest are args.
214 remain.push.apply(remain, args.slice(i + 1))
215 args[i] = '--'
216 break
217 }
218 var hadEq = false
219 if (arg.charAt(0) === '-' && arg.length > 1) {
220 var at = arg.indexOf('=')
221 if (at > -1) {
222 hadEq = true
223 var v = arg.slice(at + 1)
224 arg = arg.slice(0, at)
225 args.splice(i, 1, arg, v)
226 }
227
228 // see if it's a shorthand
229 // if so, splice and back up to re-parse it.
230 var shRes = resolveShort(arg, shortAbbr, abbrevs, { shorthands })
231 debug('arg=%j shRes=%j', arg, shRes)
232 if (shRes) {
233 debug(arg, shRes)
234 args.splice.apply(args, [i, 1].concat(shRes))
235 if (arg !== shRes[0]) {
236 i--
237 continue
238 }
239 }
240 arg = arg.replace(/^-+/, '')
241 var no = null
242 while (arg.toLowerCase().indexOf('no-') === 0) {
243 no = !no
244 arg = arg.slice(3)
245 }
246
247 if (abbrevs[arg]) {
248 arg = abbrevs[arg]
249 }
250
251 var argType = types[arg]
252 var isTypeArray = Array.isArray(argType)
253 if (isTypeArray && argType.length === 1) {
254 isTypeArray = false
255 argType = argType[0]
256 }
257
258 var isArray = argType === ArrayType ||
259 isTypeArray && argType.indexOf(ArrayType) !== -1
260
261 // allow unknown things to be arrays if specified multiple times.
262 if (
263 !Object.prototype.hasOwnProperty.call(types, arg) &&
264 Object.prototype.hasOwnProperty.call(data, arg)
265 ) {
266 if (!Array.isArray(data[arg])) {
267 data[arg] = [data[arg]]
268 }
269 isArray = true
270 }
271
272 var val
273 var la = args[i + 1]
274
275 var isBool = typeof no === 'boolean' ||
276 argType === BooleanType ||
277 isTypeArray && argType.indexOf(BooleanType) !== -1 ||
278 (typeof argType === 'undefined' && !hadEq) ||
279 (la === 'false' &&
280 (argType === null ||
281 isTypeArray && ~argType.indexOf(null)))
282
283 if (isBool) {
284 // just set and move along
285 val = !no
286 // however, also support --bool true or --bool false
287 if (la === 'true' || la === 'false') {
288 val = JSON.parse(la)
289 la = null
290 if (no) {
291 val = !val
292 }
293 i++
294 }
295
296 // also support "foo":[Boolean, "bar"] and "--foo bar"
297 if (isTypeArray && la) {
298 if (~argType.indexOf(la)) {
299 // an explicit type
300 val = la
301 i++
302 } else if (la === 'null' && ~argType.indexOf(null)) {
303 // null allowed
304 val = null
305 i++
306 } else if (!la.match(/^-{2,}[^-]/) &&
307 !isNaN(la) &&
308 ~argType.indexOf(NumberType)) {
309 // number
310 val = +la
311 i++
312 } else if (!la.match(/^-[^-]/) && ~argType.indexOf(StringType)) {
313 // string
314 val = la
315 i++
316 }
317 }
318
319 if (isArray) {
320 (data[arg] = data[arg] || []).push(val)
321 } else {
322 data[arg] = val
323 }
324
325 continue
326 }
327
328 if (argType === StringType) {
329 if (la === undefined) {
330 la = ''
331 } else if (la.match(/^-{1,2}[^-]+/)) {
332 la = ''
333 i--
334 }
335 }
336
337 if (la && la.match(/^-{2,}$/)) {
338 la = undefined
339 i--
340 }
341
342 val = la === undefined ? true : la
343 if (isArray) {
344 (data[arg] = data[arg] || []).push(val)
345 } else {
346 data[arg] = val
347 }
348
349 i++
350 continue
351 }
352 remain.push(arg)
353 }
354}
355
356function resolveShort (arg, shortAbbr, abbrevs, { shorthands }) {
357 // handle single-char shorthands glommed together, like
358 // npm ls -glp, but only if there is one dash, and only if
359 // all of the chars are single-char shorthands, and it's
360 // not a match to some other abbrev.
361 arg = arg.replace(/^-+/, '')
362
363 // if it's an exact known option, then don't go any further
364 if (abbrevs[arg] === arg) {
365 return null
366 }
367
368 // if it's an exact known shortopt, same deal
369 if (shorthands[arg]) {
370 // make it an array, if it's a list of words
371 if (shorthands[arg] && !Array.isArray(shorthands[arg])) {
372 shorthands[arg] = shorthands[arg].split(/\s+/)
373 }
374
375 return shorthands[arg]
376 }
377
378 // first check to see if this arg is a set of single-char shorthands
379 var singles = shorthands.___singles
380 if (!singles) {
381 singles = Object.keys(shorthands).filter(function (s) {
382 return s.length === 1
383 }).reduce(function (l, r) {
384 l[r] = true
385 return l
386 }, {})
387 shorthands.___singles = singles
388 debug('shorthand singles', singles)
389 }
390
391 var chrs = arg.split('').filter(function (c) {
392 return singles[c]
393 })
394
395 if (chrs.join('') === arg) {
396 return chrs.map(function (c) {
397 return shorthands[c]
398 }).reduce(function (l, r) {
399 return l.concat(r)
400 }, [])
401 }
402
403 // if it's an arg abbrev, and not a literal shorthand, then prefer the arg
404 if (abbrevs[arg] && !shorthands[arg]) {
405 return null
406 }
407
408 // if it's an abbr for a shorthand, then use that
409 if (shortAbbr[arg]) {
410 arg = shortAbbr[arg]
411 }
412
413 // make it an array, if it's a list of words
414 if (shorthands[arg] && !Array.isArray(shorthands[arg])) {
415 shorthands[arg] = shorthands[arg].split(/\s+/)
416 }
417
418 return shorthands[arg]
419}
420
421module.exports = {
422 nopt,
423 clean,
424 parse,
425 validate,
426 resolveShort,
427 typeDefs: defaultTypeDefs,
428}