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')
7 function parse (args, opts) {
9 // allow a string argument to be passed in rather
10 // than an argv array.
11 args = tokenizeArgString(args)
12 // aliases might have transitive relationships, normalize this.
13 var aliases = combineAliases(opts.alias || {})
14 var configuration = assign({}, {
15 'short-option-groups': true,
16 'camel-case-expansion': true,
18 'parse-numbers': true,
19 'boolean-negation': true
20 }, opts.configuration)
21 var defaults = opts.default || {}
22 var configObjects = opts.configObjects || []
23 var envPrefix = opts.envPrefix
25 // allow a i18n handler to be passed in, default to a fake one (util.format).
26 var __ = opts.__ || function (str) {
27 return util.format.apply(util, Array.prototype.slice.call(arguments))
43 ;[].concat(opts.array).filter(Boolean).forEach(function (key) {
44 flags.arrays[key] = true
47 ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
48 flags.bools[key] = true
51 ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
52 flags.strings[key] = true
55 ;[].concat(opts.number).filter(Boolean).forEach(function (key) {
56 flags.numbers[key] = true
59 ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
60 flags.counts[key] = true
63 ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
64 flags.normalize[key] = true
67 Object.keys(opts.narg || {}).forEach(function (k) {
68 flags.nargs[k] = opts.narg[k]
71 if (Array.isArray(opts.config) || typeof opts.config === 'string') {
72 ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
73 flags.configs[key] = true
76 Object.keys(opts.config || {}).forEach(function (k) {
77 flags.configs[k] = opts.config[k]
81 // create a lookup table that takes into account all
82 // combinations of aliases: {f: ['foo'], foo: ['f']}
83 extendAliases(opts.key, aliases, opts.default, flags.arrays)
85 // apply default values to all aliases.
86 Object.keys(defaults).forEach(function (key) {
87 (flags.aliases[key] || []).forEach(function (alias) {
88 defaults[alias] = defaults[key]
94 Object.keys(flags.bools).forEach(function (key) {
95 setArg(key, !(key in defaults) ? false : defaults[key])
100 if (args.indexOf('--') !== -1) {
101 notFlags = args.slice(args.indexOf('--') + 1)
102 args = args.slice(0, args.indexOf('--'))
105 for (var i = 0; i < args.length; i++) {
115 if (arg.match(/^--.+=/) || (
116 !configuration['short-option-groups'] && arg.match(/^-.+=/)
118 // Using [\s\S] instead of . because js doesn't support the
119 // 'dotall' regex modifier. See:
120 // http://stackoverflow.com/a/1068308/13216
121 m = arg.match(/^--?([^=]+)=([\s\S]*)$/)
123 // nargs format = '--f=monkey washing cat'
124 if (checkAllAliases(m[1], flags.nargs)) {
125 args.splice(i + 1, 0, m[2])
126 i = eatNargs(i, m[1], args)
127 // arrays format = '--f=a b c'
128 } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
129 args.splice(i + 1, 0, m[2])
130 i = eatArray(i, m[1], args)
134 } else if (arg.match(/^--no-.+/) && configuration['boolean-negation']) {
135 key = arg.match(/^--no-(.+)/)[1]
138 // -- seperated by space.
139 } else if (arg.match(/^--.+/) || (
140 !configuration['short-option-groups'] && arg.match(/^-.+/)
142 key = arg.match(/^--?(.+)/)[1]
144 // nargs format = '--foo a b c'
145 if (checkAllAliases(key, flags.nargs)) {
146 i = eatNargs(i, key, args)
147 // array format = '--foo a b c'
148 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
149 i = eatArray(i, key, args)
153 if (next !== undefined && !next.match(/^-/) &&
154 !checkAllAliases(key, flags.bools) &&
155 !checkAllAliases(key, flags.counts)) {
158 } else if (/^(true|false)$/.test(next)) {
162 setArg(key, defaultForType(guessType(key, flags)))
166 // dot-notation flag seperated by '='.
167 } else if (arg.match(/^-.\..+=/)) {
168 m = arg.match(/^-([^=]+)=([\s\S]*)$/)
171 // dot-notation flag seperated by space.
172 } else if (arg.match(/^-.\..+/)) {
174 key = arg.match(/^-(.\..+)/)[1]
176 if (next !== undefined && !next.match(/^-/) &&
177 !checkAllAliases(key, flags.bools) &&
178 !checkAllAliases(key, flags.counts)) {
182 setArg(key, defaultForType(guessType(key, flags)))
184 } else if (arg.match(/^-[^-]+/)) {
185 letters = arg.slice(1, -1).split('')
188 for (var j = 0; j < letters.length; j++) {
189 next = arg.slice(j + 2)
191 if (letters[j + 1] && letters[j + 1] === '=') {
192 value = arg.slice(j + 3)
195 // nargs format = '-f=monkey washing cat'
196 if (checkAllAliases(key, flags.nargs)) {
197 args.splice(i + 1, 0, value)
198 i = eatNargs(i, key, args)
199 // array format = '-f=a b c'
200 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
201 args.splice(i + 1, 0, value)
202 i = eatArray(i, key, args)
212 setArg(letters[j], next)
216 if (/[A-Za-z]/.test(letters[j]) &&
217 /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
218 setArg(letters[j], next)
223 if (letters[j + 1] && letters[j + 1].match(/\W/)) {
224 setArg(letters[j], next)
228 setArg(letters[j], defaultForType(guessType(letters[j], flags)))
232 key = arg.slice(-1)[0]
234 if (!broken && key !== '-') {
235 // nargs format = '-f a b c'
236 if (checkAllAliases(key, flags.nargs)) {
237 i = eatNargs(i, key, args)
238 // array format = '-f a b c'
239 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
240 i = eatArray(i, key, args)
244 if (next !== undefined && !/^(-|--)[^-]/.test(next) &&
245 !checkAllAliases(key, flags.bools) &&
246 !checkAllAliases(key, flags.counts)) {
249 } else if (/^(true|false)$/.test(next)) {
253 setArg(key, defaultForType(guessType(key, flags)))
259 flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
264 // order of precedence:
265 // 1. command line arg
266 // 2. value from config file
267 // 3. value from config objects
268 // 4. value from env var
269 // 5. configured default value
270 applyEnvVars(argv, true) // special case: check env vars that point to config file
273 applyEnvVars(argv, false)
274 applyDefaultsAndAliases(argv, flags.aliases, defaults)
276 // for any counts either not in args or without an explicit default, set to 0
277 Object.keys(flags.counts).forEach(function (key) {
278 if (!hasKey(argv, key.split('.'))) setArg(key, 0)
281 notFlags.forEach(function (key) {
285 // how many arguments should we consume, based
286 // on the nargs option?
287 function eatNargs (i, key, args) {
288 var toEat = checkAllAliases(key, flags.nargs)
290 if (args.length - (i + 1) < toEat) error = Error(__('Not enough arguments following: %s', key))
292 for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
293 setArg(key, args[ii])
299 // if an option is an array, eat all non-hyphenated arguments
300 // following it... YUM!
301 // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
302 function eatArray (i, key, args) {
304 for (var ii = i + 1; ii < args.length; ii++) {
305 if (/^-/.test(args[ii])) {
307 setArg(key, defaultForType('array'))
312 setArg(key, args[ii])
318 function setArg (key, val) {
321 // handle parsing boolean arguments --foo=true --bar false.
322 if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
323 if (typeof val === 'string') val = val === 'true'
326 if (/-/.test(key) && !(flags.aliases[key] && flags.aliases[key].length) && configuration['camel-case-expansion']) {
327 var c = camelCase(key)
328 flags.aliases[key] = [c]
333 if (!checkAllAliases(key, flags.strings)) {
334 if (isNumber(val)) value = Number(val)
335 if (!isUndefined(val) && !isNumber(val) && checkAllAliases(key, flags.numbers)) value = NaN
338 // increment a count given as arg (either no value or value parsed as boolean)
339 if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
343 // Set normalized value when key is in 'normalize' and in 'arrays'
344 if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
345 value = path.normalize(val)
348 var splitKey = key.split('.')
349 setKey(argv, splitKey, value)
351 // handle populating aliases of the full key
352 if (flags.aliases[key]) {
353 flags.aliases[key].forEach(function (x) {
355 setKey(argv, x, value)
359 // handle populating aliases of the first element of the dot-notation key
360 if (splitKey.length > 1 && configuration['dot-notation']) {
361 ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
364 // expand alias with nested objects in key
365 var a = [].concat(splitKey)
366 a.shift() // nuke the old key.
369 setKey(argv, x, value)
373 // Set normalize getter and setter when key is in 'normalize' but isn't an array
374 if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
375 var keys = [key].concat(flags.aliases[key] || [])
376 keys.forEach(function (key) {
377 argv.__defineSetter__(key, function (v) {
378 val = path.normalize(v)
381 argv.__defineGetter__(key, function () {
382 return typeof val === 'string' ? path.normalize(val) : val
388 // set args from config.json file, this should be
389 // applied last so that defaults can be applied.
390 function setConfig (argv) {
391 var configLookup = {}
393 // expand defaults/aliases, in-case any happen to reference
394 // the config.json file.
395 applyDefaultsAndAliases(configLookup, flags.aliases, defaults)
397 Object.keys(flags.configs).forEach(function (configKey) {
398 var configPath = argv[configKey] || configLookup[configKey]
402 var resolvedConfigPath = path.resolve(process.cwd(), configPath)
404 if (typeof flags.configs[configKey] === 'function') {
406 config = flags.configs[configKey](resolvedConfigPath)
410 if (config instanceof Error) {
415 config = require(resolvedConfigPath)
418 setConfigObject(config)
420 if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath))
426 // set args from config object.
427 // it recursively checks nested objects.
428 function setConfigObject (config, prev) {
429 Object.keys(config).forEach(function (key) {
430 var value = config[key]
431 var fullKey = prev ? prev + '.' + key : key
433 if (Object.prototype.toString.call(value) === '[object Object]') {
434 // if the value is an object but not an array, check nested object
435 setConfigObject(value, fullKey)
437 // setting arguments via CLI takes precedence over
438 // values within the config file.
439 if (!hasKey(argv, fullKey.split('.')) || (flags.defaulted[fullKey])) {
440 setArg(fullKey, value)
446 // set all config objects passed in opts
447 function setConfigObjects () {
448 if (typeof configObjects === 'undefined') return
449 configObjects.forEach(function (configObject) {
450 setConfigObject(configObject)
454 function applyEnvVars (argv, configOnly) {
455 if (typeof envPrefix === 'undefined') return
457 var prefix = typeof envPrefix === 'string' ? envPrefix : ''
458 Object.keys(process.env).forEach(function (envVar) {
459 if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
460 // get array of nested keys and convert them to camel case
461 var keys = envVar.split('__').map(function (key, i) {
463 key = key.substring(prefix.length)
465 return camelCase(key)
468 if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
469 setArg(keys.join('.'), process.env[envVar])
475 function applyDefaultsAndAliases (obj, aliases, defaults) {
476 Object.keys(defaults).forEach(function (key) {
477 if (!hasKey(obj, key.split('.'))) {
478 setKey(obj, key.split('.'), defaults[key])
480 ;(aliases[key] || []).forEach(function (x) {
481 if (hasKey(obj, x.split('.'))) return
482 setKey(obj, x.split('.'), defaults[key])
488 function hasKey (obj, keys) {
491 if (!configuration['dot-notation']) keys = [keys.join('.')]
493 keys.slice(0, -1).forEach(function (key) {
497 var key = keys[keys.length - 1]
499 if (typeof o !== 'object') return false
503 function setKey (obj, keys, value) {
506 if (!configuration['dot-notation']) keys = [keys.join('.')]
508 keys.slice(0, -1).forEach(function (key) {
509 if (o[key] === undefined) o[key] = {}
513 var key = keys[keys.length - 1]
515 if (value === increment) {
516 o[key] = increment(o[key])
517 } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
518 o[key] = Array.isArray(value) ? value : [value]
519 } else if (o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
521 } else if (Array.isArray(o[key])) {
524 o[key] = [ o[key], value ]
528 // extend the aliases list with inferred aliases.
529 function extendAliases () {
530 Array.prototype.slice.call(arguments).forEach(function (obj) {
531 Object.keys(obj || {}).forEach(function (key) {
532 // short-circuit if we've already added a key
533 // to the aliases array, for example it might
534 // exist in both 'opts.default' and 'opts.key'.
535 if (flags.aliases[key]) return
537 flags.aliases[key] = [].concat(aliases[key] || [])
538 // For "--option-name", also set argv.optionName
539 flags.aliases[key].concat(key).forEach(function (x) {
540 if (/-/.test(x) && configuration['camel-case-expansion']) {
542 flags.aliases[key].push(c)
546 flags.aliases[key].forEach(function (x) {
547 flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) {
555 // check if a flag is set for any of a key's aliases.
556 function checkAllAliases (key, flag) {
558 var toCheck = [].concat(flags.aliases[key] || [], key)
560 toCheck.forEach(function (key) {
561 if (flag[key]) isSet = flag[key]
567 function setDefaulted (key) {
568 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
569 flags.defaulted[k] = true
573 function unsetDefaulted (key) {
574 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
575 delete flags.defaulted[k]
579 // return a default value, given the type of a flag.,
580 // e.g., key of type 'string' will default to '', rather than 'true'.
581 function defaultForType (type) {
592 // given a flag, enforce a default type.
593 function guessType (key, flags) {
596 if (flags.strings && flags.strings[key]) type = 'string'
597 else if (flags.numbers && flags.numbers[key]) type = 'number'
598 else if (flags.arrays && flags.arrays[key]) type = 'array'
603 function isNumber (x) {
604 if (!configuration['parse-numbers']) return false
605 if (typeof x === 'number') return true
606 if (/^0x[0-9a-f]+$/i.test(x)) return true
607 return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
610 function isUndefined (num) {
611 return num === undefined
617 aliases: flags.aliases,
618 newAliases: newAliases,
619 configuration: configuration
623 // if any aliases reference each other, we should
624 // merge them together.
625 function combineAliases (aliases) {
630 // turn alias lookup hash {key: ['alias1', 'alias2']} into
631 // a simple array ['key', 'alias1', 'alias2']
632 Object.keys(aliases).forEach(function (key) {
634 [].concat(aliases[key], key)
638 // combine arrays until zero changes are
639 // made in an iteration.
642 for (var i = 0; i < aliasArrays.length; i++) {
643 for (var ii = i + 1; ii < aliasArrays.length; ii++) {
644 var intersect = aliasArrays[i].filter(function (v) {
645 return aliasArrays[ii].indexOf(v) !== -1
648 if (intersect.length) {
649 aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii])
650 aliasArrays.splice(ii, 1)
658 // map arrays back to the hash-lookup (de-dupe while
660 aliasArrays.forEach(function (aliasArray) {
661 aliasArray = aliasArray.filter(function (v, i, self) {
662 return self.indexOf(v) === i
664 combined[aliasArray.pop()] = aliasArray
670 // this function should only be called when a count is given as an arg
671 // it is NOT called to set a default value
672 // thus we can start the count at 1 instead of 0
673 function increment (orig) {
674 return orig !== undefined ? orig + 1 : 1
677 function Parser (args, opts) {
678 var result = parse(args.slice(), opts)
683 // parse arguments and return detailed
684 // meta information, aliases, etc.
685 Parser.detailed = function (args, opts) {
686 return parse(args.slice(), opts)
689 module.exports = Parser