nopt-lib.js 13 KB

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