parser.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. // fancy-pants parsing of argv, originally forked
  2. // from minimist: https://www.npmjs.com/package/minimist
  3. var camelCase = require('camelcase'),
  4. path = require('path')
  5. function increment (orig) {
  6. return orig !== undefined ? orig + 1 : 0
  7. }
  8. module.exports = function (args, opts) {
  9. if (!opts) opts = {}
  10. var flags = { arrays: {}, bools: {}, strings: {}, counts: {}, normalize: {}, configs: {} }
  11. ;[].concat(opts['array']).filter(Boolean).forEach(function (key) {
  12. flags.arrays[key] = true
  13. })
  14. ;[].concat(opts['boolean']).filter(Boolean).forEach(function (key) {
  15. flags.bools[key] = true
  16. })
  17. ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
  18. flags.strings[key] = true
  19. })
  20. ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
  21. flags.counts[key] = true
  22. })
  23. ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
  24. flags.normalize[key] = true
  25. })
  26. ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
  27. flags.configs[key] = true
  28. })
  29. var aliases = {},
  30. newAliases = {}
  31. extendAliases(opts.key)
  32. extendAliases(opts.alias)
  33. var defaults = opts['default'] || {}
  34. Object.keys(defaults).forEach(function (key) {
  35. if (/-/.test(key) && !opts.alias[key]) {
  36. aliases[key] = aliases[key] || []
  37. }
  38. (aliases[key] || []).forEach(function (alias) {
  39. defaults[alias] = defaults[key]
  40. })
  41. })
  42. var argv = { _: [] }
  43. Object.keys(flags.bools).forEach(function (key) {
  44. setArg(key, !(key in defaults) ? false : defaults[key])
  45. })
  46. var notFlags = []
  47. if (args.indexOf('--') !== -1) {
  48. notFlags = args.slice(args.indexOf('--') + 1)
  49. args = args.slice(0, args.indexOf('--'))
  50. }
  51. for (var i = 0; i < args.length; i++) {
  52. var arg = args[i],
  53. broken,
  54. key,
  55. letters,
  56. m,
  57. next,
  58. value
  59. // -- seperated by =
  60. if (arg.match(/^--.+=/)) {
  61. // Using [\s\S] instead of . because js doesn't support the
  62. // 'dotall' regex modifier. See:
  63. // http://stackoverflow.com/a/1068308/13216
  64. m = arg.match(/^--([^=]+)=([\s\S]*)$/)
  65. // nargs format = '--f=monkey washing cat'
  66. if (checkAllAliases(m[1], opts.narg)) {
  67. args.splice(i + 1, m[1], m[2])
  68. i = eatNargs(i, m[1], args)
  69. // arrays format = '--f=a b c'
  70. } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
  71. args.splice(i + 1, m[1], m[2])
  72. i = eatArray(i, m[1], args)
  73. } else {
  74. setArg(m[1], m[2])
  75. }
  76. } else if (arg.match(/^--no-.+/)) {
  77. key = arg.match(/^--no-(.+)/)[1]
  78. setArg(key, false)
  79. // -- seperated by space.
  80. } else if (arg.match(/^--.+/)) {
  81. key = arg.match(/^--(.+)/)[1]
  82. // nargs format = '--foo a b c'
  83. if (checkAllAliases(key, opts.narg)) {
  84. i = eatNargs(i, key, args)
  85. // array format = '--foo a b c'
  86. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  87. i = eatArray(i, key, args)
  88. } else {
  89. next = args[i + 1]
  90. if (next !== undefined && !next.match(/^-/)
  91. && !checkAllAliases(key, flags.bools)
  92. && !checkAllAliases(key, flags.counts)) {
  93. setArg(key, next)
  94. i++
  95. } else if (/^(true|false)$/.test(next)) {
  96. setArg(key, next)
  97. i++
  98. } else {
  99. setArg(key, defaultForType(guessType(key, flags)))
  100. }
  101. }
  102. // dot-notation flag seperated by '='.
  103. } else if (arg.match(/^-.\..+=/)) {
  104. m = arg.match(/^-([^=]+)=([\s\S]*)$/)
  105. setArg(m[1], m[2])
  106. // dot-notation flag seperated by space.
  107. } else if (arg.match(/^-.\..+/)) {
  108. next = args[i + 1]
  109. key = arg.match(/^-(.\..+)/)[1]
  110. if (next !== undefined && !next.match(/^-/)
  111. && !checkAllAliases(key, flags.bools)
  112. && !checkAllAliases(key, flags.counts)) {
  113. setArg(key, next)
  114. i++
  115. } else {
  116. setArg(key, defaultForType(guessType(key, flags)))
  117. }
  118. } else if (arg.match(/^-[^-]+/)) {
  119. letters = arg.slice(1, -1).split('')
  120. broken = false
  121. for (var j = 0; j < letters.length; j++) {
  122. next = arg.slice(j + 2)
  123. if (letters[j + 1] && letters[j + 1] === '=') {
  124. value = arg.slice(j + 3)
  125. key = letters[j]
  126. // nargs format = '-f=monkey washing cat'
  127. if (checkAllAliases(letters[j], opts.narg)) {
  128. args.splice(i + 1, 0, value)
  129. i = eatNargs(i, key, args)
  130. // array format = '-f=a b c'
  131. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  132. args.splice(i + 1, 0, value)
  133. i = eatArray(i, key, args)
  134. } else {
  135. setArg(key, value)
  136. }
  137. broken = true
  138. break
  139. }
  140. if (next === '-') {
  141. setArg(letters[j], next)
  142. continue
  143. }
  144. if (/[A-Za-z]/.test(letters[j])
  145. && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
  146. setArg(letters[j], next)
  147. broken = true
  148. break
  149. }
  150. if (letters[j + 1] && letters[j + 1].match(/\W/)) {
  151. setArg(letters[j], arg.slice(j + 2))
  152. broken = true
  153. break
  154. } else {
  155. setArg(letters[j], defaultForType(guessType(letters[j], flags)))
  156. }
  157. }
  158. key = arg.slice(-1)[0]
  159. if (!broken && key !== '-') {
  160. // nargs format = '-f a b c'
  161. if (checkAllAliases(key, opts.narg)) {
  162. i = eatNargs(i, key, args)
  163. // array format = '-f a b c'
  164. } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
  165. i = eatArray(i, key, args)
  166. } else {
  167. if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1])
  168. && !checkAllAliases(key, flags.bools)
  169. && !checkAllAliases(key, flags.counts)) {
  170. setArg(key, args[i + 1])
  171. i++
  172. } else if (args[i + 1] && /true|false/.test(args[i + 1])) {
  173. setArg(key, args[i + 1])
  174. i++
  175. } else {
  176. setArg(key, defaultForType(guessType(key, flags)))
  177. }
  178. }
  179. }
  180. } else {
  181. argv._.push(
  182. flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
  183. )
  184. }
  185. }
  186. setConfig(argv)
  187. applyDefaultsAndAliases(argv, aliases, defaults)
  188. Object.keys(flags.counts).forEach(function (key) {
  189. setArg(key, defaults[key])
  190. })
  191. notFlags.forEach(function (key) {
  192. argv._.push(key)
  193. })
  194. // how many arguments should we consume, based
  195. // on the nargs option?
  196. function eatNargs (i, key, args) {
  197. var toEat = checkAllAliases(key, opts.narg)
  198. if (args.length - (i + 1) < toEat) throw Error('not enough arguments following: ' + key)
  199. for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
  200. setArg(key, args[ii])
  201. }
  202. return (i + toEat)
  203. }
  204. // if an option is an array, eat all non-hyphenated arguments
  205. // following it... YUM!
  206. // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
  207. function eatArray (i, key, args) {
  208. for (var ii = i + 1; ii < args.length; ii++) {
  209. if (/^-/.test(args[ii])) break
  210. i = ii
  211. setArg(key, args[ii])
  212. }
  213. return i
  214. }
  215. function setArg (key, val) {
  216. // handle parsing boolean arguments --foo=true --bar false.
  217. if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
  218. if (typeof val === 'string') val = val === 'true'
  219. }
  220. if (/-/.test(key) && !(aliases[key] && aliases[key].length)) {
  221. var c = camelCase(key)
  222. aliases[key] = [c]
  223. newAliases[c] = true
  224. }
  225. var value = !checkAllAliases(key, flags.strings) && isNumber(val) ? Number(val) : val
  226. if (checkAllAliases(key, flags.counts)) {
  227. value = increment
  228. }
  229. var splitKey = key.split('.')
  230. setKey(argv, splitKey, value)
  231. ;(aliases[splitKey[0]] || []).forEach(function (x) {
  232. x = x.split('.')
  233. // handle populating dot notation for both
  234. // the key and its aliases.
  235. if (splitKey.length > 1) {
  236. var a = [].concat(splitKey)
  237. a.shift() // nuke the old key.
  238. x = x.concat(a)
  239. }
  240. setKey(argv, x, value)
  241. })
  242. var keys = [key].concat(aliases[key] || [])
  243. for (var i = 0, l = keys.length; i < l; i++) {
  244. if (flags.normalize[keys[i]]) {
  245. keys.forEach(function (key) {
  246. argv.__defineSetter__(key, function (v) {
  247. val = path.normalize(v)
  248. })
  249. argv.__defineGetter__(key, function () {
  250. return typeof val === 'string' ?
  251. path.normalize(val) : val
  252. })
  253. })
  254. break
  255. }
  256. }
  257. }
  258. // set args from config.json file, this should be
  259. // applied last so that defaults can be applied.
  260. function setConfig (argv) {
  261. var configLookup = {}
  262. // expand defaults/aliases, in-case any happen to reference
  263. // the config.json file.
  264. applyDefaultsAndAliases(configLookup, aliases, defaults)
  265. Object.keys(flags.configs).forEach(function (configKey) {
  266. var configPath = argv[configKey] || configLookup[configKey]
  267. if (configPath) {
  268. try {
  269. var config = require(path.resolve(process.cwd(), configPath))
  270. Object.keys(config).forEach(function (key) {
  271. // setting arguments via CLI takes precedence over
  272. // values within the config file.
  273. if (argv[key] === undefined) {
  274. delete argv[key]
  275. setArg(key, config[key])
  276. }
  277. })
  278. } catch (ex) {
  279. throw Error('invalid json config file: ' + configPath)
  280. }
  281. }
  282. })
  283. }
  284. function applyDefaultsAndAliases (obj, aliases, defaults) {
  285. Object.keys(defaults).forEach(function (key) {
  286. if (!hasKey(obj, key.split('.'))) {
  287. setKey(obj, key.split('.'), defaults[key])
  288. ;(aliases[key] || []).forEach(function (x) {
  289. setKey(obj, x.split('.'), defaults[key])
  290. })
  291. }
  292. })
  293. }
  294. function hasKey (obj, keys) {
  295. var o = obj
  296. keys.slice(0, -1).forEach(function (key) {
  297. o = (o[key] || {})
  298. })
  299. var key = keys[keys.length - 1]
  300. return key in o
  301. }
  302. function setKey (obj, keys, value) {
  303. var o = obj
  304. keys.slice(0, -1).forEach(function (key) {
  305. if (o[key] === undefined) o[key] = {}
  306. o = o[key]
  307. })
  308. var key = keys[keys.length - 1]
  309. if (value === increment) {
  310. o[key] = increment(o[key])
  311. } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
  312. o[key] = Array.isArray(value) ? value : [value]
  313. } else if (o[key] === undefined || typeof o[key] === 'boolean') {
  314. o[key] = value
  315. } else if (Array.isArray(o[key])) {
  316. o[key].push(value)
  317. } else {
  318. o[key] = [ o[key], value ]
  319. }
  320. }
  321. // extend the aliases list with inferred aliases.
  322. function extendAliases (obj) {
  323. Object.keys(obj || {}).forEach(function (key) {
  324. aliases[key] = [].concat(opts.alias[key] || [])
  325. // For "--option-name", also set argv.optionName
  326. aliases[key].concat(key).forEach(function (x) {
  327. if (/-/.test(x)) {
  328. var c = camelCase(x)
  329. aliases[key].push(c)
  330. newAliases[c] = true
  331. }
  332. })
  333. aliases[key].forEach(function (x) {
  334. aliases[x] = [key].concat(aliases[key].filter(function (y) {
  335. return x !== y
  336. }))
  337. })
  338. })
  339. }
  340. // check if a flag is set for any of a key's aliases.
  341. function checkAllAliases (key, flag) {
  342. var isSet = false,
  343. toCheck = [].concat(aliases[key] || [], key)
  344. toCheck.forEach(function (key) {
  345. if (flag[key]) isSet = flag[key]
  346. })
  347. return isSet
  348. }
  349. // return a default value, given the type of a flag.,
  350. // e.g., key of type 'string' will default to '', rather than 'true'.
  351. function defaultForType (type) {
  352. var def = {
  353. boolean: true,
  354. string: '',
  355. array: []
  356. }
  357. return def[type]
  358. }
  359. // given a flag, enforce a default type.
  360. function guessType (key, flags) {
  361. var type = 'boolean'
  362. if (flags.strings && flags.strings[key]) type = 'string'
  363. else if (flags.arrays && flags.arrays[key]) type = 'array'
  364. return type
  365. }
  366. function isNumber (x) {
  367. if (typeof x === 'number') return true
  368. if (/^0x[0-9a-f]+$/i.test(x)) return true
  369. return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
  370. }
  371. return {
  372. argv: argv,
  373. aliases: aliases,
  374. newAliases: newAliases
  375. }
  376. }