1 // fancy-pants parsing of argv, originally forked
2 // from minimist: https://www.npmjs.com/package/minimist
3 var camelCase = require('camelcase'),
6 function increment (orig) {
7 return orig !== undefined ? orig + 1 : 0
10 module.exports = function (args, opts) {
12 var flags = { arrays: {}, bools: {}, strings: {}, counts: {}, normalize: {}, configs: {} }
14 ;[].concat(opts['array']).filter(Boolean).forEach(function (key) {
15 flags.arrays[key] = true
18 ;[].concat(opts['boolean']).filter(Boolean).forEach(function (key) {
19 flags.bools[key] = true
22 ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
23 flags.strings[key] = true
26 ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
27 flags.counts[key] = true
30 ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
31 flags.normalize[key] = true
34 ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
35 flags.configs[key] = true
41 extendAliases(opts.key)
42 extendAliases(opts.alias)
44 var defaults = opts['default'] || {}
45 Object.keys(defaults).forEach(function (key) {
46 if (/-/.test(key) && !opts.alias[key]) {
47 aliases[key] = aliases[key] || []
49 (aliases[key] || []).forEach(function (alias) {
50 defaults[alias] = defaults[key]
56 Object.keys(flags.bools).forEach(function (key) {
57 setArg(key, !(key in defaults) ? false : defaults[key])
61 if (args.indexOf('--') !== -1) {
62 notFlags = args.slice(args.indexOf('--') + 1)
63 args = args.slice(0, args.indexOf('--'))
66 for (var i = 0; i < args.length; i++) {
76 if (arg.match(/^--.+=/)) {
77 // Using [\s\S] instead of . because js doesn't support the
78 // 'dotall' regex modifier. See:
79 // http://stackoverflow.com/a/1068308/13216
80 m = arg.match(/^--([^=]+)=([\s\S]*)$/)
82 // nargs format = '--f=monkey washing cat'
83 if (checkAllAliases(m[1], opts.narg)) {
84 args.splice(i + 1, m[1], m[2])
85 i = eatNargs(i, m[1], args)
86 // arrays format = '--f=a b c'
87 } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
88 args.splice(i + 1, m[1], m[2])
89 i = eatArray(i, m[1], args)
93 } else if (arg.match(/^--no-.+/)) {
94 key = arg.match(/^--no-(.+)/)[1]
97 // -- seperated by space.
98 } else if (arg.match(/^--.+/)) {
99 key = arg.match(/^--(.+)/)[1]
101 // nargs format = '--foo a b c'
102 if (checkAllAliases(key, opts.narg)) {
103 i = eatNargs(i, key, args)
104 // array format = '--foo a b c'
105 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
106 i = eatArray(i, key, args)
110 if (next !== undefined && !next.match(/^-/)
111 && !checkAllAliases(key, flags.bools)
112 && !checkAllAliases(key, flags.counts)) {
115 } else if (/^(true|false)$/.test(next)) {
119 setArg(key, defaultForType(guessType(key, flags)))
123 // dot-notation flag seperated by '='.
124 } else if (arg.match(/^-.\..+=/)) {
125 m = arg.match(/^-([^=]+)=([\s\S]*)$/)
128 // dot-notation flag seperated by space.
129 } else if (arg.match(/^-.\..+/)) {
131 key = arg.match(/^-(.\..+)/)[1]
133 if (next !== undefined && !next.match(/^-/)
134 && !checkAllAliases(key, flags.bools)
135 && !checkAllAliases(key, flags.counts)) {
139 setArg(key, defaultForType(guessType(key, flags)))
141 } else if (arg.match(/^-[^-]+/)) {
142 letters = arg.slice(1, -1).split('')
145 for (var j = 0; j < letters.length; j++) {
146 next = arg.slice(j + 2)
148 if (letters[j + 1] && letters[j + 1] === '=') {
149 value = arg.slice(j + 3)
152 // nargs format = '-f=monkey washing cat'
153 if (checkAllAliases(letters[j], opts.narg)) {
154 args.splice(i + 1, 0, value)
155 i = eatNargs(i, key, args)
156 // array format = '-f=a b c'
157 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
158 args.splice(i + 1, 0, value)
159 i = eatArray(i, key, args)
169 setArg(letters[j], next)
173 if (/[A-Za-z]/.test(letters[j])
174 && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
175 setArg(letters[j], next)
180 if (letters[j + 1] && letters[j + 1].match(/\W/)) {
181 setArg(letters[j], arg.slice(j + 2))
185 setArg(letters[j], defaultForType(guessType(letters[j], flags)))
189 key = arg.slice(-1)[0]
191 if (!broken && key !== '-') {
192 // nargs format = '-f a b c'
193 if (checkAllAliases(key, opts.narg)) {
194 i = eatNargs(i, key, args)
195 // array format = '-f a b c'
196 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
197 i = eatArray(i, key, args)
199 if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1])
200 && !checkAllAliases(key, flags.bools)
201 && !checkAllAliases(key, flags.counts)) {
202 setArg(key, args[i + 1])
204 } else if (args[i + 1] && /true|false/.test(args[i + 1])) {
205 setArg(key, args[i + 1])
208 setArg(key, defaultForType(guessType(key, flags)))
214 flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
220 applyDefaultsAndAliases(argv, aliases, defaults)
222 Object.keys(flags.counts).forEach(function (key) {
223 setArg(key, defaults[key])
226 notFlags.forEach(function (key) {
230 // how many arguments should we consume, based
231 // on the nargs option?
232 function eatNargs (i, key, args) {
233 var toEat = checkAllAliases(key, opts.narg)
235 if (args.length - (i + 1) < toEat) throw Error('not enough arguments following: ' + key)
237 for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
238 setArg(key, args[ii])
244 // if an option is an array, eat all non-hyphenated arguments
245 // following it... YUM!
246 // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
247 function eatArray (i, key, args) {
248 for (var ii = i + 1; ii < args.length; ii++) {
249 if (/^-/.test(args[ii])) break
251 setArg(key, args[ii])
257 function setArg (key, val) {
258 // handle parsing boolean arguments --foo=true --bar false.
259 if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
260 if (typeof val === 'string') val = val === 'true'
263 if (/-/.test(key) && !(aliases[key] && aliases[key].length)) {
264 var c = camelCase(key)
269 var value = !checkAllAliases(key, flags.strings) && isNumber(val) ? Number(val) : val
271 if (checkAllAliases(key, flags.counts)) {
275 var splitKey = key.split('.')
276 setKey(argv, splitKey, value)
278 ;(aliases[splitKey[0]] || []).forEach(function (x) {
281 // handle populating dot notation for both
282 // the key and its aliases.
283 if (splitKey.length > 1) {
284 var a = [].concat(splitKey)
285 a.shift() // nuke the old key.
289 setKey(argv, x, value)
292 var keys = [key].concat(aliases[key] || [])
293 for (var i = 0, l = keys.length; i < l; i++) {
294 if (flags.normalize[keys[i]]) {
295 keys.forEach(function (key) {
296 argv.__defineSetter__(key, function (v) {
297 val = path.normalize(v)
300 argv.__defineGetter__(key, function () {
301 return typeof val === 'string' ?
302 path.normalize(val) : val
310 // set args from config.json file, this should be
311 // applied last so that defaults can be applied.
312 function setConfig (argv) {
313 var configLookup = {}
315 // expand defaults/aliases, in-case any happen to reference
316 // the config.json file.
317 applyDefaultsAndAliases(configLookup, aliases, defaults)
319 Object.keys(flags.configs).forEach(function (configKey) {
320 var configPath = argv[configKey] || configLookup[configKey]
323 var config = require(path.resolve(process.cwd(), configPath))
325 Object.keys(config).forEach(function (key) {
326 // setting arguments via CLI takes precedence over
327 // values within the config file.
328 if (argv[key] === undefined) {
330 setArg(key, config[key])
334 throw Error('invalid json config file: ' + configPath)
340 function applyDefaultsAndAliases (obj, aliases, defaults) {
341 Object.keys(defaults).forEach(function (key) {
342 if (!hasKey(obj, key.split('.'))) {
343 setKey(obj, key.split('.'), defaults[key])
345 ;(aliases[key] || []).forEach(function (x) {
346 setKey(obj, x.split('.'), defaults[key])
352 function hasKey (obj, keys) {
354 keys.slice(0, -1).forEach(function (key) {
358 var key = keys[keys.length - 1]
362 function setKey (obj, keys, value) {
364 keys.slice(0, -1).forEach(function (key) {
365 if (o[key] === undefined) o[key] = {}
369 var key = keys[keys.length - 1]
370 if (value === increment) {
371 o[key] = increment(o[key])
372 } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
373 o[key] = Array.isArray(value) ? value : [value]
374 } else if (o[key] === undefined || typeof o[key] === 'boolean') {
376 } else if (Array.isArray(o[key])) {
379 o[key] = [ o[key], value ]
383 // extend the aliases list with inferred aliases.
384 function extendAliases (obj) {
385 Object.keys(obj || {}).forEach(function (key) {
386 aliases[key] = [].concat(opts.alias[key] || [])
387 // For "--option-name", also set argv.optionName
388 aliases[key].concat(key).forEach(function (x) {
395 aliases[key].forEach(function (x) {
396 aliases[x] = [key].concat(aliases[key].filter(function (y) {
403 // check if a flag is set for any of a key's aliases.
404 function checkAllAliases (key, flag) {
406 toCheck = [].concat(aliases[key] || [], key)
408 toCheck.forEach(function (key) {
409 if (flag[key]) isSet = flag[key]
415 // return a default value, given the type of a flag.,
416 // e.g., key of type 'string' will default to '', rather than 'true'.
417 function defaultForType (type) {
427 // given a flag, enforce a default type.
428 function guessType (key, flags) {
431 if (flags.strings && flags.strings[key]) type = 'string'
432 else if (flags.arrays && flags.arrays[key]) type = 'array'
437 function isNumber (x) {
438 if (typeof x === 'number') return true
439 if (/^0x[0-9a-f]+$/i.test(x)) return true
440 return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
446 newAliases: newAliases