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