1 var camelCase = require('camelcase')
2 var path = require('path')
3 var tokenizeArgString = require('./lib/tokenize-arg-string')
4 var util = require('util')
6 function parse (args, opts) {
8 // allow a string argument to be passed in rather
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,
17 'parse-numbers': true,
18 'boolean-negation': true,
19 'negation-prefix': 'no-',
20 'duplicate-arguments-array': true,
21 'flatten-duplicate-arrays': true,
23 'combine-arrays': false
24 }, opts.configuration)
25 var defaults = opts.default || {}
26 var configObjects = opts.configObjects || []
27 var envPrefix = opts.envPrefix
28 var notFlagsOption = configuration['populate--']
29 var notFlagsArgv = notFlagsOption ? '--' : '_'
31 // allow a i18n handler to be passed in, default to a fake one (util.format).
32 var __ = opts.__ || function (str) {
33 return util.format.apply(util, Array.prototype.slice.call(arguments))
49 var negative = /^-[0-9]+(\.[0-9]+)?/
50 var negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)')
52 ;[].concat(opts.array).filter(Boolean).forEach(function (key) {
53 flags.arrays[key] = true
56 ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
57 flags.bools[key] = true
60 ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
61 flags.strings[key] = true
64 ;[].concat(opts.number).filter(Boolean).forEach(function (key) {
65 flags.numbers[key] = true
68 ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
69 flags.counts[key] = true
72 ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
73 flags.normalize[key] = true
76 Object.keys(opts.narg || {}).forEach(function (k) {
77 flags.nargs[k] = opts.narg[k]
80 Object.keys(opts.coerce || {}).forEach(function (k) {
81 flags.coercions[k] = opts.coerce[k]
84 if (Array.isArray(opts.config) || typeof opts.config === 'string') {
85 ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
86 flags.configs[key] = true
89 Object.keys(opts.config || {}).forEach(function (k) {
90 flags.configs[k] = opts.config[k]
94 // create a lookup table that takes into account all
95 // combinations of aliases: {f: ['foo'], foo: ['f']}
96 extendAliases(opts.key, aliases, opts.default, flags.arrays)
98 // apply default values to all aliases.
99 Object.keys(defaults).forEach(function (key) {
100 (flags.aliases[key] || []).forEach(function (alias) {
101 defaults[alias] = defaults[key]
107 Object.keys(flags.bools).forEach(function (key) {
108 setArg(key, !(key in defaults) ? false : defaults[key])
113 if (args.indexOf('--') !== -1) {
114 notFlags = args.slice(args.indexOf('--') + 1)
115 args = args.slice(0, args.indexOf('--'))
118 for (var i = 0; i < args.length; i++) {
128 if (arg.match(/^--.+=/) || (
129 !configuration['short-option-groups'] && arg.match(/^-.+=/)
131 // Using [\s\S] instead of . because js doesn't support the
132 // 'dotall' regex modifier. See:
133 // http://stackoverflow.com/a/1068308/13216
134 m = arg.match(/^--?([^=]+)=([\s\S]*)$/)
136 // nargs format = '--f=monkey washing cat'
137 if (checkAllAliases(m[1], flags.nargs)) {
138 args.splice(i + 1, 0, m[2])
139 i = eatNargs(i, m[1], args)
140 // arrays format = '--f=a b c'
141 } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
142 args.splice(i + 1, 0, m[2])
143 i = eatArray(i, m[1], args)
147 } else if (arg.match(negatedBoolean) && configuration['boolean-negation']) {
148 key = arg.match(negatedBoolean)[1]
151 // -- seperated by space.
152 } else if (arg.match(/^--.+/) || (
153 !configuration['short-option-groups'] && arg.match(/^-.+/)
155 key = arg.match(/^--?(.+)/)[1]
157 // nargs format = '--foo a b c'
158 if (checkAllAliases(key, flags.nargs)) {
159 i = eatNargs(i, key, args)
160 // array format = '--foo a b c'
161 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
162 i = eatArray(i, key, args)
166 if (next !== undefined && (!next.match(/^-/) ||
167 next.match(negative)) &&
168 !checkAllAliases(key, flags.bools) &&
169 !checkAllAliases(key, flags.counts)) {
172 } else if (/^(true|false)$/.test(next)) {
176 setArg(key, defaultForType(guessType(key, flags)))
180 // dot-notation flag seperated by '='.
181 } else if (arg.match(/^-.\..+=/)) {
182 m = arg.match(/^-([^=]+)=([\s\S]*)$/)
185 // dot-notation flag seperated by space.
186 } else if (arg.match(/^-.\..+/)) {
188 key = arg.match(/^-(.\..+)/)[1]
190 if (next !== undefined && !next.match(/^-/) &&
191 !checkAllAliases(key, flags.bools) &&
192 !checkAllAliases(key, flags.counts)) {
196 setArg(key, defaultForType(guessType(key, flags)))
198 } else if (arg.match(/^-[^-]+/) && !arg.match(negative)) {
199 letters = arg.slice(1, -1).split('')
202 for (var j = 0; j < letters.length; j++) {
203 next = arg.slice(j + 2)
205 if (letters[j + 1] && letters[j + 1] === '=') {
206 value = arg.slice(j + 3)
209 // nargs format = '-f=monkey washing cat'
210 if (checkAllAliases(key, flags.nargs)) {
211 args.splice(i + 1, 0, value)
212 i = eatNargs(i, key, args)
213 // array format = '-f=a b c'
214 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
215 args.splice(i + 1, 0, value)
216 i = eatArray(i, key, args)
226 setArg(letters[j], next)
230 // current letter is an alphabetic character and next value is a number
231 if (/[A-Za-z]/.test(letters[j]) &&
232 /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
233 setArg(letters[j], next)
238 if (letters[j + 1] && letters[j + 1].match(/\W/)) {
239 setArg(letters[j], next)
243 setArg(letters[j], defaultForType(guessType(letters[j], flags)))
247 key = arg.slice(-1)[0]
249 if (!broken && key !== '-') {
250 // nargs format = '-f a b c'
251 if (checkAllAliases(key, flags.nargs)) {
252 i = eatNargs(i, key, args)
253 // array format = '-f a b c'
254 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
255 i = eatArray(i, key, args)
259 if (next !== undefined && (!/^(-|--)[^-]/.test(next) ||
260 next.match(negative)) &&
261 !checkAllAliases(key, flags.bools) &&
262 !checkAllAliases(key, flags.counts)) {
265 } else if (/^(true|false)$/.test(next)) {
269 setArg(key, defaultForType(guessType(key, flags)))
274 argv._.push(maybeCoerceNumber('_', arg))
278 // order of precedence:
279 // 1. command line arg
280 // 2. value from env var
281 // 3. value from config file
282 // 4. value from config objects
283 // 5. configured default value
284 applyEnvVars(argv, true) // special case: check env vars that point to config file
285 applyEnvVars(argv, false)
288 applyDefaultsAndAliases(argv, flags.aliases, defaults)
291 // for any counts either not in args or without an explicit default, set to 0
292 Object.keys(flags.counts).forEach(function (key) {
293 if (!hasKey(argv, key.split('.'))) setArg(key, 0)
296 // '--' defaults to undefined.
297 if (notFlagsOption && notFlags.length) argv[notFlagsArgv] = []
298 notFlags.forEach(function (key) {
299 argv[notFlagsArgv].push(key)
302 // how many arguments should we consume, based
303 // on the nargs option?
304 function eatNargs (i, key, args) {
306 const toEat = checkAllAliases(key, flags.nargs)
308 // nargs will not consume flag arguments, e.g., -abc, --foo,
309 // and terminates when one is observed.
311 for (ii = i + 1; ii < args.length; ii++) {
312 if (!args[ii].match(/^-[^0-9]/)) available++
316 if (available < toEat) error = Error(__('Not enough arguments following: %s', key))
318 const consumed = Math.min(available, toEat)
319 for (ii = i + 1; ii < (consumed + i + 1); ii++) {
320 setArg(key, args[ii])
323 return (i + consumed)
326 // if an option is an array, eat all non-hyphenated arguments
327 // following it... YUM!
328 // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
329 function eatArray (i, key, args) {
332 var multipleArrayFlag = i > 0
333 for (var ii = i + 1; ii < args.length; ii++) {
334 if (/^-/.test(args[ii]) && !negative.test(args[ii])) {
336 setArg(key, defaultForType('array'))
338 multipleArrayFlag = true
342 argsToSet.push(args[ii])
344 if (multipleArrayFlag) {
345 setArg(key, argsToSet.map(function (arg) {
346 return processValue(key, arg)
349 argsToSet.forEach(function (arg) {
357 function setArg (key, val) {
360 if (/-/.test(key) && configuration['camel-case-expansion']) {
361 addNewAlias(key, camelCase(key))
364 var value = processValue(key, val)
366 var splitKey = key.split('.')
367 setKey(argv, splitKey, value)
369 // handle populating aliases of the full key
370 if (flags.aliases[key]) {
371 flags.aliases[key].forEach(function (x) {
373 setKey(argv, x, value)
377 // handle populating aliases of the first element of the dot-notation key
378 if (splitKey.length > 1 && configuration['dot-notation']) {
379 ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
382 // expand alias with nested objects in key
383 var a = [].concat(splitKey)
384 a.shift() // nuke the old key.
387 setKey(argv, x, value)
391 // Set normalize getter and setter when key is in 'normalize' but isn't an array
392 if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
393 var keys = [key].concat(flags.aliases[key] || [])
394 keys.forEach(function (key) {
395 argv.__defineSetter__(key, function (v) {
396 val = path.normalize(v)
399 argv.__defineGetter__(key, function () {
400 return typeof val === 'string' ? path.normalize(val) : val
406 function addNewAlias (key, alias) {
407 if (!(flags.aliases[key] && flags.aliases[key].length)) {
408 flags.aliases[key] = [alias]
409 newAliases[alias] = true
411 if (!(flags.aliases[alias] && flags.aliases[alias].length)) {
412 addNewAlias(alias, key)
416 function processValue (key, val) {
417 // handle parsing boolean arguments --foo=true --bar false.
418 if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
419 if (typeof val === 'string') val = val === 'true'
422 var value = maybeCoerceNumber(key, val)
424 // increment a count given as arg (either no value or value parsed as boolean)
425 if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
429 // Set normalized value when key is in 'normalize' and in 'arrays'
430 if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
431 if (Array.isArray(val)) value = val.map(path.normalize)
432 else value = path.normalize(val)
437 function maybeCoerceNumber (key, value) {
438 if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.coercions)) {
439 const shouldCoerceNumber = isNumber(value) && configuration['parse-numbers'] && (
440 Number.isSafeInteger(Math.floor(value))
442 if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) value = Number(value)
447 // set args from config.json file, this should be
448 // applied last so that defaults can be applied.
449 function setConfig (argv) {
450 var configLookup = {}
452 // expand defaults/aliases, in-case any happen to reference
453 // the config.json file.
454 applyDefaultsAndAliases(configLookup, flags.aliases, defaults)
456 Object.keys(flags.configs).forEach(function (configKey) {
457 var configPath = argv[configKey] || configLookup[configKey]
461 var resolvedConfigPath = path.resolve(process.cwd(), configPath)
463 if (typeof flags.configs[configKey] === 'function') {
465 config = flags.configs[configKey](resolvedConfigPath)
469 if (config instanceof Error) {
474 config = require(resolvedConfigPath)
477 setConfigObject(config)
479 if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath))
485 // set args from config object.
486 // it recursively checks nested objects.
487 function setConfigObject (config, prev) {
488 Object.keys(config).forEach(function (key) {
489 var value = config[key]
490 var fullKey = prev ? prev + '.' + key : key
492 // if the value is an inner object and we have dot-notation
493 // enabled, treat inner objects in config the same as
494 // heavily nested dot notations (foo.bar.apple).
495 if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) {
496 // if the value is an object but not an array, check nested object
497 setConfigObject(value, fullKey)
499 // setting arguments via CLI takes precedence over
500 // values within the config file.
501 if (!hasKey(argv, fullKey.split('.')) || (flags.defaulted[fullKey]) || (flags.arrays[fullKey] && configuration['combine-arrays'])) {
502 setArg(fullKey, value)
508 // set all config objects passed in opts
509 function setConfigObjects () {
510 if (typeof configObjects === 'undefined') return
511 configObjects.forEach(function (configObject) {
512 setConfigObject(configObject)
516 function applyEnvVars (argv, configOnly) {
517 if (typeof envPrefix === 'undefined') return
519 var prefix = typeof envPrefix === 'string' ? envPrefix : ''
520 Object.keys(process.env).forEach(function (envVar) {
521 if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
522 // get array of nested keys and convert them to camel case
523 var keys = envVar.split('__').map(function (key, i) {
525 key = key.substring(prefix.length)
527 return camelCase(key)
530 if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
531 setArg(keys.join('.'), process.env[envVar])
537 function applyCoercions (argv) {
540 Object.keys(argv).forEach(function (key) {
541 if (!applied.hasOwnProperty(key)) { // If we haven't already coerced this option via one of its aliases
542 coerce = checkAllAliases(key, flags.coercions)
543 if (typeof coerce === 'function') {
545 var value = coerce(argv[key])
546 ;([].concat(flags.aliases[key] || [], key)).forEach(ali => {
547 applied[ali] = argv[ali] = value
557 function applyDefaultsAndAliases (obj, aliases, defaults) {
558 Object.keys(defaults).forEach(function (key) {
559 if (!hasKey(obj, key.split('.'))) {
560 setKey(obj, key.split('.'), defaults[key])
562 ;(aliases[key] || []).forEach(function (x) {
563 if (hasKey(obj, x.split('.'))) return
564 setKey(obj, x.split('.'), defaults[key])
570 function hasKey (obj, keys) {
573 if (!configuration['dot-notation']) keys = [keys.join('.')]
575 keys.slice(0, -1).forEach(function (key) {
579 var key = keys[keys.length - 1]
581 if (typeof o !== 'object') return false
585 function setKey (obj, keys, value) {
588 if (!configuration['dot-notation']) keys = [keys.join('.')]
590 keys.slice(0, -1).forEach(function (key, index) {
591 if (typeof o === 'object' && o[key] === undefined) {
595 if (typeof o[key] !== 'object' || Array.isArray(o[key])) {
596 // ensure that o[key] is an array, and that the last item is an empty object.
597 if (Array.isArray(o[key])) {
600 o[key] = [o[key], {}]
603 // we want to update the empty object at the end of the o[key] array, so set o to that object
604 o = o[key][o[key].length - 1]
610 var key = keys[keys.length - 1]
612 var isTypeArray = checkAllAliases(keys.join('.'), flags.arrays)
613 var isValueArray = Array.isArray(value)
614 var duplicate = configuration['duplicate-arguments-array']
616 if (value === increment) {
617 o[key] = increment(o[key])
618 } else if (Array.isArray(o[key])) {
619 if (duplicate && isTypeArray && isValueArray) {
620 o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : [o[key]].concat([value])
621 } else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) {
624 o[key] = o[key].concat([value])
626 } else if (o[key] === undefined && isTypeArray) {
627 o[key] = isValueArray ? value : [value]
628 } else if (duplicate && !(o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(keys.join('.'), flags.bools) || checkAllAliases(key, flags.counts))) {
629 o[key] = [ o[key], value ]
635 // extend the aliases list with inferred aliases.
636 function extendAliases () {
637 Array.prototype.slice.call(arguments).forEach(function (obj) {
638 Object.keys(obj || {}).forEach(function (key) {
639 // short-circuit if we've already added a key
640 // to the aliases array, for example it might
641 // exist in both 'opts.default' and 'opts.key'.
642 if (flags.aliases[key]) return
644 flags.aliases[key] = [].concat(aliases[key] || [])
645 // For "--option-name", also set argv.optionName
646 flags.aliases[key].concat(key).forEach(function (x) {
647 if (/-/.test(x) && configuration['camel-case-expansion']) {
649 if (c !== key && flags.aliases[key].indexOf(c) === -1) {
650 flags.aliases[key].push(c)
655 flags.aliases[key].forEach(function (x) {
656 flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) {
664 // check if a flag is set for any of a key's aliases.
665 function checkAllAliases (key, flag) {
667 var toCheck = [].concat(flags.aliases[key] || [], key)
669 toCheck.forEach(function (key) {
670 if (flag[key]) isSet = flag[key]
676 function setDefaulted (key) {
677 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
678 flags.defaulted[k] = true
682 function unsetDefaulted (key) {
683 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
684 delete flags.defaulted[k]
688 // return a default value, given the type of a flag.,
689 // e.g., key of type 'string' will default to '', rather than 'true'.
690 function defaultForType (type) {
701 // given a flag, enforce a default type.
702 function guessType (key, flags) {
705 if (checkAllAliases(key, flags.strings)) type = 'string'
706 else if (checkAllAliases(key, flags.numbers)) type = 'number'
707 else if (checkAllAliases(key, flags.arrays)) type = 'array'
712 function isNumber (x) {
713 if (typeof x === 'number') return true
714 if (/^0x[0-9a-f]+$/i.test(x)) return true
715 return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
718 function isUndefined (num) {
719 return num === undefined
725 aliases: flags.aliases,
726 newAliases: newAliases,
727 configuration: configuration
731 // if any aliases reference each other, we should
732 // merge them together.
733 function combineAliases (aliases) {
738 // turn alias lookup hash {key: ['alias1', 'alias2']} into
739 // a simple array ['key', 'alias1', 'alias2']
740 Object.keys(aliases).forEach(function (key) {
742 [].concat(aliases[key], key)
746 // combine arrays until zero changes are
747 // made in an iteration.
750 for (var i = 0; i < aliasArrays.length; i++) {
751 for (var ii = i + 1; ii < aliasArrays.length; ii++) {
752 var intersect = aliasArrays[i].filter(function (v) {
753 return aliasArrays[ii].indexOf(v) !== -1
756 if (intersect.length) {
757 aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii])
758 aliasArrays.splice(ii, 1)
766 // map arrays back to the hash-lookup (de-dupe while
768 aliasArrays.forEach(function (aliasArray) {
769 aliasArray = aliasArray.filter(function (v, i, self) {
770 return self.indexOf(v) === i
772 combined[aliasArray.pop()] = aliasArray
778 function assign (defaults, configuration) {
780 configuration = configuration || {}
782 Object.keys(defaults).forEach(function (k) {
785 Object.keys(configuration).forEach(function (k) {
786 o[k] = configuration[k]
792 // this function should only be called when a count is given as an arg
793 // it is NOT called to set a default value
794 // thus we can start the count at 1 instead of 0
795 function increment (orig) {
796 return orig !== undefined ? orig + 1 : 1
799 function Parser (args, opts) {
800 var result = parse(args.slice(), opts)
805 // parse arguments and return detailed
806 // meta information, aliases, etc.
807 Parser.detailed = function (args, opts) {
808 return parse(args.slice(), opts)
811 module.exports = Parser