Team Ai
Datasetpublic

codekingpro/portable-devtools

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