index.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. var assign = require('lodash.assign')
  2. var camelCase = require('camelcase')
  3. var path = require('path')
  4. var tokenizeArgString = require('./lib/tokenize-arg-string')
  5. var util = require('util')
  6. function parse (args, opts) {
  7. if (!opts) opts = {}
  8. // allow a string argument to be passed in rather
  9. // than an argv array.
  10. args = tokenizeArgString(args)
  11. // aliases might have transitive relationships, normalize this.
  12. var aliases = combineAliases(opts.alias || {})
  13. var configuration = assign({}, {
  14. 'short-option-groups': true,
  15. 'camel-case-expansion': true,
  16. 'dot-notation': true,
  17. 'parse-numbers': true,
  18. 'boolean-negation': true
  19. }, opts.configuration)
  20. var defaults = opts.default || {}
  21. var configObjects = opts.configObjects || []
  22. var envPrefix = opts.envPrefix
  23. var newAliases = {}
  24. // allow a i18n handler to be passed in, default to a fake one (util.format).
  25. var __ = opts.__ || function (str) {
  26. return util.format.apply(util, Array.prototype.slice.call(arguments))
  27. }
  28. var error = null
  29. var flags = {
  30. aliases: {},
  31. arrays: {},
  32. bools: {},
  33. strings: {},
  34. numbers: {},
  35. counts: {},
  36. normalize: {},
  37. configs: {},
  38. defaulted: {},
  39. nargs: {}
  40. }
  41. ;[].concat(opts.array).filter(Boolean).forEach(function (key) {
  42. flags.arrays[key] = true
  43. })
  44. ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
  45. flags.bools[key] = true
  46. })
  47. ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
  48. flags.strings[key] = true
  49. })
  50. ;[].concat(opts.number).filter(Boolean).forEach(function (key) {
  51. flags.numbers[key] = true
  52. })
  53. ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
  54. flags.counts[key] = true
  55. })
  56. ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
  57. flags.normalize[key] = true
  58. })
  59. Object.keys(opts.narg || {}).forEach(function (k) {
  60. flags.nargs[k] = opts.narg[k]
  61. })
  62. if (Array.isArray(opts.config) || typeof opts.config === 'string') {
  63. ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
  64. flags.configs[key] = true
  65. })
  66. } else {
  67. Object.keys(opts.config || {}).forEach(function (k) {
  68. flags.configs[k] = opts.config[k]
  69. })
  70. }
  71. // create a lookup table that takes into account all
  72. // combinations of aliases: {f: ['foo'], foo: ['f']}
  73. extendAliases(opts.key, aliases, opts.default, flags.arrays)
  74. // apply default values to all aliases.
  75. Object.keys(defaults).forEach(function (key) {
  76. (flags.aliases[key] || []).forEach(function (alias) {
  77. defaults[alias] = defaults[key]
  78. })
  79. })
  80. var argv = { _: [] }
  81. Object.keys(flags.bools).forEach(function (key) {
  82. setArg(key, !(key in defaults) ? false : defaults[key])
  83. setDefaulted(key)
  84. })
  85. var notFlags = []
  86. if (args.indexOf('--') !== -1) {
  87. notFlags = args.slice(args.indexOf('--') + 1)
  88. args = args.slice(0, args.indexOf('--'))
  89. }
  90. for (var i = 0; i < args.length; i++) {
  91. var arg = args[i]
  92. var broken
  93. var key
  94. var letters
  95. var m
  96. var next
  97. var value
  98. // -- seperated by =
  99. if (arg.match(/^--.+=/) || (
  100. !configuration['short-option-groups'] && arg.match(/^-.+=/)
  101. )) {
  102. // Using [\s\S] instead of . because js doesn't support the
  103. // 'dotall' regex modifier. See:
  104. // http://stackoverflow.com/a/1068308/13216
  105. m = arg.match(/^--?([^=]+)=([\s\S]*)$/)
  106. // nargs format = '--f=monkey washing cat'
  107. if (checkAllAliases(m[1], flags.nargs)) {
  108. args.splice(i + 1, 0, m[2])
  109. i = eatNargs(i, m[1], args)
  110. // arrays format = '--f=a b c'
  111. } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
  112. args.splice(i + 1, 0, m[2])
  113. i = eatArray(i, m[1], args)
  114. } else {
  115. setArg(m[1], m[2])
  116. }
  117. } else if (arg.match(/^--no-.+/) && configuration['boolean-negation']) {
  118. key = arg.match(/^--no-(.+)/)[1]
  119. setArg(key, false)
  120. // -- seperated by space.
  121. } else if (arg.match(/^--.+/) || (
  122. !configuration['short-option-groups'] && arg.match(/^-.+/)
  123. )) {
  124. key = arg.match(/^--?(.+)/)[1]
  125. // nargs format = '--foo a b c'
  126. if (checkAllAliases(key, flags.nargs)) {
  127. i = eatNargs(i, key, args)
  128. // array format = '--foo a b c'
  129. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  130. i = eatArray(i, key, args)
  131. } else {
  132. next = args[i + 1]
  133. if (next !== undefined && !next.match(/^-/) &&
  134. !checkAllAliases(key, flags.bools) &&
  135. !checkAllAliases(key, flags.counts)) {
  136. setArg(key, next)
  137. i++
  138. } else if (/^(true|false)$/.test(next)) {
  139. setArg(key, next)
  140. i++
  141. } else {
  142. setArg(key, defaultForType(guessType(key, flags)))
  143. }
  144. }
  145. // dot-notation flag seperated by '='.
  146. } else if (arg.match(/^-.\..+=/)) {
  147. m = arg.match(/^-([^=]+)=([\s\S]*)$/)
  148. setArg(m[1], m[2])
  149. // dot-notation flag seperated by space.
  150. } else if (arg.match(/^-.\..+/)) {
  151. next = args[i + 1]
  152. key = arg.match(/^-(.\..+)/)[1]
  153. if (next !== undefined && !next.match(/^-/) &&
  154. !checkAllAliases(key, flags.bools) &&
  155. !checkAllAliases(key, flags.counts)) {
  156. setArg(key, next)
  157. i++
  158. } else {
  159. setArg(key, defaultForType(guessType(key, flags)))
  160. }
  161. } else if (arg.match(/^-[^-]+/)) {
  162. letters = arg.slice(1, -1).split('')
  163. broken = false
  164. for (var j = 0; j < letters.length; j++) {
  165. next = arg.slice(j + 2)
  166. if (letters[j + 1] && letters[j + 1] === '=') {
  167. value = arg.slice(j + 3)
  168. key = letters[j]
  169. // nargs format = '-f=monkey washing cat'
  170. if (checkAllAliases(key, flags.nargs)) {
  171. args.splice(i + 1, 0, value)
  172. i = eatNargs(i, key, args)
  173. // array format = '-f=a b c'
  174. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  175. args.splice(i + 1, 0, value)
  176. i = eatArray(i, key, args)
  177. } else {
  178. setArg(key, value)
  179. }
  180. broken = true
  181. break
  182. }
  183. if (next === '-') {
  184. setArg(letters[j], next)
  185. continue
  186. }
  187. if (/[A-Za-z]/.test(letters[j]) &&
  188. /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
  189. setArg(letters[j], next)
  190. broken = true
  191. break
  192. }
  193. if (letters[j + 1] && letters[j + 1].match(/\W/)) {
  194. setArg(letters[j], next)
  195. broken = true
  196. break
  197. } else {
  198. setArg(letters[j], defaultForType(guessType(letters[j], flags)))
  199. }
  200. }
  201. key = arg.slice(-1)[0]
  202. if (!broken && key !== '-') {
  203. // nargs format = '-f a b c'
  204. if (checkAllAliases(key, flags.nargs)) {
  205. i = eatNargs(i, key, args)
  206. // array format = '-f a b c'
  207. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  208. i = eatArray(i, key, args)
  209. } else {
  210. next = args[i + 1]
  211. if (next !== undefined && !/^(-|--)[^-]/.test(next) &&
  212. !checkAllAliases(key, flags.bools) &&
  213. !checkAllAliases(key, flags.counts)) {
  214. setArg(key, next)
  215. i++
  216. } else if (/^(true|false)$/.test(next)) {
  217. setArg(key, next)
  218. i++
  219. } else {
  220. setArg(key, defaultForType(guessType(key, flags)))
  221. }
  222. }
  223. }
  224. } else {
  225. argv._.push(
  226. flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
  227. )
  228. }
  229. }
  230. // order of precedence:
  231. // 1. command line arg
  232. // 2. value from config file
  233. // 3. value from config objects
  234. // 4. value from env var
  235. // 5. configured default value
  236. applyEnvVars(argv, true) // special case: check env vars that point to config file
  237. setConfig(argv)
  238. setConfigObjects()
  239. applyEnvVars(argv, false)
  240. applyDefaultsAndAliases(argv, flags.aliases, defaults)
  241. // for any counts either not in args or without an explicit default, set to 0
  242. Object.keys(flags.counts).forEach(function (key) {
  243. if (!hasKey(argv, key.split('.'))) setArg(key, 0)
  244. })
  245. notFlags.forEach(function (key) {
  246. argv._.push(key)
  247. })
  248. // how many arguments should we consume, based
  249. // on the nargs option?
  250. function eatNargs (i, key, args) {
  251. var toEat = checkAllAliases(key, flags.nargs)
  252. if (args.length - (i + 1) < toEat) error = Error(__('Not enough arguments following: %s', key))
  253. for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
  254. setArg(key, args[ii])
  255. }
  256. return (i + toEat)
  257. }
  258. // if an option is an array, eat all non-hyphenated arguments
  259. // following it... YUM!
  260. // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
  261. function eatArray (i, key, args) {
  262. var start = i + 1
  263. for (var ii = i + 1; ii < args.length; ii++) {
  264. if (/^-/.test(args[ii])) {
  265. if (ii === start) {
  266. setArg(key, defaultForType('array'))
  267. }
  268. break
  269. }
  270. i = ii
  271. setArg(key, args[ii])
  272. }
  273. return i
  274. }
  275. function setArg (key, val) {
  276. unsetDefaulted(key)
  277. // handle parsing boolean arguments --foo=true --bar false.
  278. if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
  279. if (typeof val === 'string') val = val === 'true'
  280. }
  281. if (/-/.test(key) && !(flags.aliases[key] && flags.aliases[key].length) && configuration['camel-case-expansion']) {
  282. var c = camelCase(key)
  283. flags.aliases[key] = [c]
  284. newAliases[c] = true
  285. }
  286. var value = val
  287. if (!checkAllAliases(key, flags.strings)) {
  288. if (isNumber(val)) value = Number(val)
  289. if (!isUndefined(val) && !isNumber(val) && checkAllAliases(key, flags.numbers)) value = NaN
  290. }
  291. // increment a count given as arg (either no value or value parsed as boolean)
  292. if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
  293. value = increment
  294. }
  295. // Set normalized value when key is in 'normalize' and in 'arrays'
  296. if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
  297. value = path.normalize(val)
  298. }
  299. var splitKey = key.split('.')
  300. setKey(argv, splitKey, value)
  301. // handle populating aliases of the full key
  302. if (flags.aliases[key]) {
  303. flags.aliases[key].forEach(function (x) {
  304. x = x.split('.')
  305. setKey(argv, x, value)
  306. })
  307. }
  308. // handle populating aliases of the first element of the dot-notation key
  309. if (splitKey.length > 1 && configuration['dot-notation']) {
  310. ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
  311. x = x.split('.')
  312. // expand alias with nested objects in key
  313. var a = [].concat(splitKey)
  314. a.shift() // nuke the old key.
  315. x = x.concat(a)
  316. setKey(argv, x, value)
  317. })
  318. }
  319. // Set normalize getter and setter when key is in 'normalize' but isn't an array
  320. if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
  321. var keys = [key].concat(flags.aliases[key] || [])
  322. keys.forEach(function (key) {
  323. argv.__defineSetter__(key, function (v) {
  324. val = path.normalize(v)
  325. })
  326. argv.__defineGetter__(key, function () {
  327. return typeof val === 'string' ? path.normalize(val) : val
  328. })
  329. })
  330. }
  331. }
  332. // set args from config.json file, this should be
  333. // applied last so that defaults can be applied.
  334. function setConfig (argv) {
  335. var configLookup = {}
  336. // expand defaults/aliases, in-case any happen to reference
  337. // the config.json file.
  338. applyDefaultsAndAliases(configLookup, flags.aliases, defaults)
  339. Object.keys(flags.configs).forEach(function (configKey) {
  340. var configPath = argv[configKey] || configLookup[configKey]
  341. if (configPath) {
  342. try {
  343. var config = null
  344. var resolvedConfigPath = path.resolve(process.cwd(), configPath)
  345. if (typeof flags.configs[configKey] === 'function') {
  346. try {
  347. config = flags.configs[configKey](resolvedConfigPath)
  348. } catch (e) {
  349. config = e
  350. }
  351. if (config instanceof Error) {
  352. error = config
  353. return
  354. }
  355. } else {
  356. config = require(resolvedConfigPath)
  357. }
  358. setConfigObject(config)
  359. } catch (ex) {
  360. if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath))
  361. }
  362. }
  363. })
  364. }
  365. // set args from config object.
  366. // it recursively checks nested objects.
  367. function setConfigObject (config, prev) {
  368. Object.keys(config).forEach(function (key) {
  369. var value = config[key]
  370. var fullKey = prev ? prev + '.' + key : key
  371. if (Object.prototype.toString.call(value) === '[object Object]') {
  372. // if the value is an object but not an array, check nested object
  373. setConfigObject(value, fullKey)
  374. } else {
  375. // setting arguments via CLI takes precedence over
  376. // values within the config file.
  377. if (!hasKey(argv, fullKey.split('.')) || (flags.defaulted[fullKey])) {
  378. setArg(fullKey, value)
  379. }
  380. }
  381. })
  382. }
  383. // set all config objects passed in opts
  384. function setConfigObjects () {
  385. if (typeof configObjects === 'undefined') return
  386. configObjects.forEach(function (configObject) {
  387. setConfigObject(configObject)
  388. })
  389. }
  390. function applyEnvVars (argv, configOnly) {
  391. if (typeof envPrefix === 'undefined') return
  392. var prefix = typeof envPrefix === 'string' ? envPrefix : ''
  393. Object.keys(process.env).forEach(function (envVar) {
  394. if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
  395. // get array of nested keys and convert them to camel case
  396. var keys = envVar.split('__').map(function (key, i) {
  397. if (i === 0) {
  398. key = key.substring(prefix.length)
  399. }
  400. return camelCase(key)
  401. })
  402. if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
  403. setArg(keys.join('.'), process.env[envVar])
  404. }
  405. }
  406. })
  407. }
  408. function applyDefaultsAndAliases (obj, aliases, defaults) {
  409. Object.keys(defaults).forEach(function (key) {
  410. if (!hasKey(obj, key.split('.'))) {
  411. setKey(obj, key.split('.'), defaults[key])
  412. ;(aliases[key] || []).forEach(function (x) {
  413. if (hasKey(obj, x.split('.'))) return
  414. setKey(obj, x.split('.'), defaults[key])
  415. })
  416. }
  417. })
  418. }
  419. function hasKey (obj, keys) {
  420. var o = obj
  421. if (!configuration['dot-notation']) keys = [keys.join('.')]
  422. keys.slice(0, -1).forEach(function (key) {
  423. o = (o[key] || {})
  424. })
  425. var key = keys[keys.length - 1]
  426. if (typeof o !== 'object') return false
  427. else return key in o
  428. }
  429. function setKey (obj, keys, value) {
  430. var o = obj
  431. if (!configuration['dot-notation']) keys = [keys.join('.')]
  432. keys.slice(0, -1).forEach(function (key) {
  433. if (o[key] === undefined) o[key] = {}
  434. o = o[key]
  435. })
  436. var key = keys[keys.length - 1]
  437. if (value === increment) {
  438. o[key] = increment(o[key])
  439. } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
  440. o[key] = Array.isArray(value) ? value : [value]
  441. } else if (o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
  442. o[key] = value
  443. } else if (Array.isArray(o[key])) {
  444. o[key].push(value)
  445. } else {
  446. o[key] = [ o[key], value ]
  447. }
  448. }
  449. // extend the aliases list with inferred aliases.
  450. function extendAliases () {
  451. Array.prototype.slice.call(arguments).forEach(function (obj) {
  452. Object.keys(obj || {}).forEach(function (key) {
  453. // short-circuit if we've already added a key
  454. // to the aliases array, for example it might
  455. // exist in both 'opts.default' and 'opts.key'.
  456. if (flags.aliases[key]) return
  457. flags.aliases[key] = [].concat(aliases[key] || [])
  458. // For "--option-name", also set argv.optionName
  459. flags.aliases[key].concat(key).forEach(function (x) {
  460. if (/-/.test(x) && configuration['camel-case-expansion']) {
  461. var c = camelCase(x)
  462. flags.aliases[key].push(c)
  463. newAliases[c] = true
  464. }
  465. })
  466. flags.aliases[key].forEach(function (x) {
  467. flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) {
  468. return x !== y
  469. }))
  470. })
  471. })
  472. })
  473. }
  474. // check if a flag is set for any of a key's aliases.
  475. function checkAllAliases (key, flag) {
  476. var isSet = false
  477. var toCheck = [].concat(flags.aliases[key] || [], key)
  478. toCheck.forEach(function (key) {
  479. if (flag[key]) isSet = flag[key]
  480. })
  481. return isSet
  482. }
  483. function setDefaulted (key) {
  484. [].concat(flags.aliases[key] || [], key).forEach(function (k) {
  485. flags.defaulted[k] = true
  486. })
  487. }
  488. function unsetDefaulted (key) {
  489. [].concat(flags.aliases[key] || [], key).forEach(function (k) {
  490. delete flags.defaulted[k]
  491. })
  492. }
  493. // return a default value, given the type of a flag.,
  494. // e.g., key of type 'string' will default to '', rather than 'true'.
  495. function defaultForType (type) {
  496. var def = {
  497. boolean: true,
  498. string: '',
  499. number: undefined,
  500. array: []
  501. }
  502. return def[type]
  503. }
  504. // given a flag, enforce a default type.
  505. function guessType (key, flags) {
  506. var type = 'boolean'
  507. if (flags.strings && flags.strings[key]) type = 'string'
  508. else if (flags.numbers && flags.numbers[key]) type = 'number'
  509. else if (flags.arrays && flags.arrays[key]) type = 'array'
  510. return type
  511. }
  512. function isNumber (x) {
  513. if (!configuration['parse-numbers']) return false
  514. if (typeof x === 'number') return true
  515. if (/^0x[0-9a-f]+$/i.test(x)) return true
  516. return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
  517. }
  518. function isUndefined (num) {
  519. return num === undefined
  520. }
  521. return {
  522. argv: argv,
  523. error: error,
  524. aliases: flags.aliases,
  525. newAliases: newAliases,
  526. configuration: configuration
  527. }
  528. }
  529. // if any aliases reference each other, we should
  530. // merge them together.
  531. function combineAliases (aliases) {
  532. var aliasArrays = []
  533. var change = true
  534. var combined = {}
  535. // turn alias lookup hash {key: ['alias1', 'alias2']} into
  536. // a simple array ['key', 'alias1', 'alias2']
  537. Object.keys(aliases).forEach(function (key) {
  538. aliasArrays.push(
  539. [].concat(aliases[key], key)
  540. )
  541. })
  542. // combine arrays until zero changes are
  543. // made in an iteration.
  544. while (change) {
  545. change = false
  546. for (var i = 0; i < aliasArrays.length; i++) {
  547. for (var ii = i + 1; ii < aliasArrays.length; ii++) {
  548. var intersect = aliasArrays[i].filter(function (v) {
  549. return aliasArrays[ii].indexOf(v) !== -1
  550. })
  551. if (intersect.length) {
  552. aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii])
  553. aliasArrays.splice(ii, 1)
  554. change = true
  555. break
  556. }
  557. }
  558. }
  559. }
  560. // map arrays back to the hash-lookup (de-dupe while
  561. // we're at it).
  562. aliasArrays.forEach(function (aliasArray) {
  563. aliasArray = aliasArray.filter(function (v, i, self) {
  564. return self.indexOf(v) === i
  565. })
  566. combined[aliasArray.pop()] = aliasArray
  567. })
  568. return combined
  569. }
  570. // this function should only be called when a count is given as an arg
  571. // it is NOT called to set a default value
  572. // thus we can start the count at 1 instead of 0
  573. function increment (orig) {
  574. return orig !== undefined ? orig + 1 : 1
  575. }
  576. function Parser (args, opts) {
  577. var result = parse(args.slice(), opts)
  578. return result.argv
  579. }
  580. // parse arguments and return detailed
  581. // meta information, aliases, etc.
  582. Parser.detailed = function (args, opts) {
  583. return parse(args.slice(), opts)
  584. }
  585. module.exports = Parser