1 var camelCase = require('camelcase')
2 var decamelize = require('decamelize')
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)
13 // aliases might have transitive relationships, normalize this.
14 var aliases = combineAliases(opts.alias || {})
15 var configuration = Object.assign({
16 'short-option-groups': true,
17 'camel-case-expansion': true,
19 'parse-numbers': true,
20 'boolean-negation': true,
21 'negation-prefix': 'no-',
22 'duplicate-arguments-array': true,
23 'flatten-duplicate-arrays': true,
25 'combine-arrays': false,
26 'set-placeholder-key': false,
27 'halt-at-non-option': false,
28 'strip-aliased': false,
30 }, opts.configuration)
31 var defaults = opts.default || {}
32 var configObjects = opts.configObjects || []
33 var envPrefix = opts.envPrefix
34 var notFlagsOption = configuration['populate--']
35 var notFlagsArgv = notFlagsOption ? '--' : '_'
37 // allow a i18n handler to be passed in, default to a fake one (util.format).
38 var __ = opts.__ || util.format
54 var negative = /^-[0-9]+(\.[0-9]+)?/
55 var negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)')
57 ;[].concat(opts.array).filter(Boolean).forEach(function (opt) {
58 var key = opt.key || opt
60 // assign to flags[bools|strings|numbers]
61 const assignment = Object.keys(opt).map(function (key) {
67 }).filter(Boolean).pop()
69 // assign key to be coerced
71 flags[assignment][key] = true
74 flags.arrays[key] = true
78 ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
79 flags.bools[key] = true
83 ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
84 flags.strings[key] = true
88 ;[].concat(opts.number).filter(Boolean).forEach(function (key) {
89 flags.numbers[key] = true
93 ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
94 flags.counts[key] = true
98 ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
99 flags.normalize[key] = true
103 Object.keys(opts.narg || {}).forEach(function (k) {
104 flags.nargs[k] = opts.narg[k]
108 Object.keys(opts.coerce || {}).forEach(function (k) {
109 flags.coercions[k] = opts.coerce[k]
113 if (Array.isArray(opts.config) || typeof opts.config === 'string') {
114 ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
115 flags.configs[key] = true
118 Object.keys(opts.config || {}).forEach(function (k) {
119 flags.configs[k] = opts.config[k]
123 // create a lookup table that takes into account all
124 // combinations of aliases: {f: ['foo'], foo: ['f']}
125 extendAliases(opts.key, aliases, opts.default, flags.arrays)
127 // apply default values to all aliases.
128 Object.keys(defaults).forEach(function (key) {
129 (flags.aliases[key] || []).forEach(function (alias) {
130 defaults[alias] = defaults[key]
136 Object.keys(flags.bools).forEach(function (key) {
137 if (Object.prototype.hasOwnProperty.call(defaults, key)) {
138 setArg(key, defaults[key])
145 for (var i = 0; i < args.length; i++) {
155 if (arg.match(/^--.+=/) || (
156 !configuration['short-option-groups'] && arg.match(/^-.+=/)
158 // Using [\s\S] instead of . because js doesn't support the
159 // 'dotall' regex modifier. See:
160 // http://stackoverflow.com/a/1068308/13216
161 m = arg.match(/^--?([^=]+)=([\s\S]*)$/)
163 // nargs format = '--f=monkey washing cat'
164 if (checkAllAliases(m[1], flags.nargs)) {
165 args.splice(i + 1, 0, m[2])
166 i = eatNargs(i, m[1], args)
167 // arrays format = '--f=a b c'
168 } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
169 args.splice(i + 1, 0, m[2])
170 i = eatArray(i, m[1], args)
174 } else if (arg.match(negatedBoolean) && configuration['boolean-negation']) {
175 key = arg.match(negatedBoolean)[1]
178 // -- seperated by space.
179 } else if (arg.match(/^--.+/) || (
180 !configuration['short-option-groups'] && arg.match(/^-[^-]+/)
182 key = arg.match(/^--?(.+)/)[1]
184 // nargs format = '--foo a b c'
185 if (checkAllAliases(key, flags.nargs)) {
186 i = eatNargs(i, key, args)
187 // array format = '--foo a b c'
188 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
189 i = eatArray(i, key, args)
191 next = flags.nargs[key] === 0 ? undefined : args[i + 1]
193 if (next !== undefined && (!next.match(/^-/) ||
194 next.match(negative)) &&
195 !checkAllAliases(key, flags.bools) &&
196 !checkAllAliases(key, flags.counts)) {
199 } else if (/^(true|false)$/.test(next)) {
203 setArg(key, defaultValue(key))
207 // dot-notation flag seperated by '='.
208 } else if (arg.match(/^-.\..+=/)) {
209 m = arg.match(/^-([^=]+)=([\s\S]*)$/)
212 // dot-notation flag seperated by space.
213 } else if (arg.match(/^-.\..+/)) {
215 key = arg.match(/^-(.\..+)/)[1]
217 if (next !== undefined && !next.match(/^-/) &&
218 !checkAllAliases(key, flags.bools) &&
219 !checkAllAliases(key, flags.counts)) {
223 setArg(key, defaultValue(key))
225 } else if (arg.match(/^-[^-]+/) && !arg.match(negative)) {
226 letters = arg.slice(1, -1).split('')
229 for (var j = 0; j < letters.length; j++) {
230 next = arg.slice(j + 2)
232 if (letters[j + 1] && letters[j + 1] === '=') {
233 value = arg.slice(j + 3)
236 // nargs format = '-f=monkey washing cat'
237 if (checkAllAliases(key, flags.nargs)) {
238 args.splice(i + 1, 0, value)
239 i = eatNargs(i, key, args)
240 // array format = '-f=a b c'
241 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
242 args.splice(i + 1, 0, value)
243 i = eatArray(i, key, args)
253 setArg(letters[j], next)
257 // current letter is an alphabetic character and next value is a number
258 if (/[A-Za-z]/.test(letters[j]) &&
259 /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
260 setArg(letters[j], next)
265 if (letters[j + 1] && letters[j + 1].match(/\W/)) {
266 setArg(letters[j], next)
270 setArg(letters[j], defaultValue(letters[j]))
274 key = arg.slice(-1)[0]
276 if (!broken && key !== '-') {
277 // nargs format = '-f a b c'
278 if (checkAllAliases(key, flags.nargs)) {
279 i = eatNargs(i, key, args)
280 // array format = '-f a b c'
281 } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
282 i = eatArray(i, key, args)
286 if (next !== undefined && (!/^(-|--)[^-]/.test(next) ||
287 next.match(negative)) &&
288 !checkAllAliases(key, flags.bools) &&
289 !checkAllAliases(key, flags.counts)) {
292 } else if (/^(true|false)$/.test(next)) {
296 setArg(key, defaultValue(key))
300 } else if (arg === '--') {
301 notFlags = args.slice(i + 1)
303 } else if (configuration['halt-at-non-option']) {
304 notFlags = args.slice(i)
307 argv._.push(maybeCoerceNumber('_', arg))
311 // order of precedence:
312 // 1. command line arg
313 // 2. value from env var
314 // 3. value from config file
315 // 4. value from config objects
316 // 5. configured default value
317 applyEnvVars(argv, true) // special case: check env vars that point to config file
318 applyEnvVars(argv, false)
321 applyDefaultsAndAliases(argv, flags.aliases, defaults)
323 if (configuration['set-placeholder-key']) setPlaceholderKeys(argv)
325 // for any counts either not in args or without an explicit default, set to 0
326 Object.keys(flags.counts).forEach(function (key) {
327 if (!hasKey(argv, key.split('.'))) setArg(key, 0)
330 // '--' defaults to undefined.
331 if (notFlagsOption && notFlags.length) argv[notFlagsArgv] = []
332 notFlags.forEach(function (key) {
333 argv[notFlagsArgv].push(key)
336 if (configuration['camel-case-expansion'] && configuration['strip-dashed']) {
337 Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => {
342 if (configuration['strip-aliased']) {
343 // XXX Switch to [].concat(...Object.values(aliases)) once node.js 6 is dropped
344 ;[].concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => {
345 if (configuration['camel-case-expansion']) {
346 delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')]
353 // how many arguments should we consume, based
354 // on the nargs option?
355 function eatNargs (i, key, args) {
357 const toEat = checkAllAliases(key, flags.nargs)
359 // nargs will not consume flag arguments, e.g., -abc, --foo,
360 // and terminates when one is observed.
362 for (ii = i + 1; ii < args.length; ii++) {
363 if (!args[ii].match(/^-[^0-9]/)) available++
367 if (available < toEat) error = Error(__('Not enough arguments following: %s', key))
369 const consumed = Math.min(available, toEat)
370 for (ii = i + 1; ii < (consumed + i + 1); ii++) {
371 setArg(key, args[ii])
374 return (i + consumed)
377 // if an option is an array, eat all non-hyphenated arguments
378 // following it... YUM!
379 // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
380 function eatArray (i, key, args) {
383 var multipleArrayFlag = i > 0
384 for (var ii = i + 1; ii < args.length; ii++) {
385 if (/^-/.test(args[ii]) && !negative.test(args[ii])) {
387 setArg(key, defaultForType('array'))
389 multipleArrayFlag = true
393 argsToSet.push(args[ii])
395 if (multipleArrayFlag) {
396 setArg(key, argsToSet.map(function (arg) {
397 return processValue(key, arg)
400 argsToSet.forEach(function (arg) {
408 function setArg (key, val) {
411 if (/-/.test(key) && configuration['camel-case-expansion']) {
412 var alias = key.split('.').map(function (prop) {
413 return camelCase(prop)
415 addNewAlias(key, alias)
418 var value = processValue(key, val)
420 var splitKey = key.split('.')
421 setKey(argv, splitKey, value)
423 // handle populating aliases of the full key
424 if (flags.aliases[key] && flags.aliases[key].forEach) {
425 flags.aliases[key].forEach(function (x) {
427 setKey(argv, x, value)
431 // handle populating aliases of the first element of the dot-notation key
432 if (splitKey.length > 1 && configuration['dot-notation']) {
433 ;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
436 // expand alias with nested objects in key
437 var a = [].concat(splitKey)
438 a.shift() // nuke the old key.
441 setKey(argv, x, value)
445 // Set normalize getter and setter when key is in 'normalize' but isn't an array
446 if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
447 var keys = [key].concat(flags.aliases[key] || [])
448 keys.forEach(function (key) {
449 argv.__defineSetter__(key, function (v) {
450 val = path.normalize(v)
453 argv.__defineGetter__(key, function () {
454 return typeof val === 'string' ? path.normalize(val) : val
460 function addNewAlias (key, alias) {
461 if (!(flags.aliases[key] && flags.aliases[key].length)) {
462 flags.aliases[key] = [alias]
463 newAliases[alias] = true
465 if (!(flags.aliases[alias] && flags.aliases[alias].length)) {
466 addNewAlias(alias, key)
470 function processValue (key, val) {
471 // strings may be quoted, clean this up as we assign values.
472 if (typeof val === 'string' &&
473 (val[0] === "'" || val[0] === '"') &&
474 val[val.length - 1] === val[0]
476 val = val.substring(1, val.length - 1)
479 // handle parsing boolean arguments --foo=true --bar false.
480 if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
481 if (typeof val === 'string') val = val === 'true'
484 var value = maybeCoerceNumber(key, val)
486 // increment a count given as arg (either no value or value parsed as boolean)
487 if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
491 // Set normalized value when key is in 'normalize' and in 'arrays'
492 if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
493 if (Array.isArray(val)) value = val.map(path.normalize)
494 else value = path.normalize(val)
499 function maybeCoerceNumber (key, value) {
500 if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.coercions)) {
501 const shouldCoerceNumber = isNumber(value) && configuration['parse-numbers'] && (
502 Number.isSafeInteger(Math.floor(value))
504 if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) value = Number(value)
509 // set args from config.json file, this should be
510 // applied last so that defaults can be applied.
511 function setConfig (argv) {
512 var configLookup = {}
514 // expand defaults/aliases, in-case any happen to reference
515 // the config.json file.
516 applyDefaultsAndAliases(configLookup, flags.aliases, defaults)
518 Object.keys(flags.configs).forEach(function (configKey) {
519 var configPath = argv[configKey] || configLookup[configKey]
523 var resolvedConfigPath = path.resolve(process.cwd(), configPath)
525 if (typeof flags.configs[configKey] === 'function') {
527 config = flags.configs[configKey](resolvedConfigPath)
531 if (config instanceof Error) {
536 config = require(resolvedConfigPath)
539 setConfigObject(config)
541 if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath))
547 // set args from config object.
548 // it recursively checks nested objects.
549 function setConfigObject (config, prev) {
550 Object.keys(config).forEach(function (key) {
551 var value = config[key]
552 var fullKey = prev ? prev + '.' + key : key
554 // if the value is an inner object and we have dot-notation
555 // enabled, treat inner objects in config the same as
556 // heavily nested dot notations (foo.bar.apple).
557 if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) {
558 // if the value is an object but not an array, check nested object
559 setConfigObject(value, fullKey)
561 // setting arguments via CLI takes precedence over
562 // values within the config file.
563 if (!hasKey(argv, fullKey.split('.')) || (flags.defaulted[fullKey]) || (flags.arrays[fullKey] && configuration['combine-arrays'])) {
564 setArg(fullKey, value)
570 // set all config objects passed in opts
571 function setConfigObjects () {
572 if (typeof configObjects === 'undefined') return
573 configObjects.forEach(function (configObject) {
574 setConfigObject(configObject)
578 function applyEnvVars (argv, configOnly) {
579 if (typeof envPrefix === 'undefined') return
581 var prefix = typeof envPrefix === 'string' ? envPrefix : ''
582 Object.keys(process.env).forEach(function (envVar) {
583 if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
584 // get array of nested keys and convert them to camel case
585 var keys = envVar.split('__').map(function (key, i) {
587 key = key.substring(prefix.length)
589 return camelCase(key)
592 if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
593 setArg(keys.join('.'), process.env[envVar])
599 function applyCoercions (argv) {
602 Object.keys(argv).forEach(function (key) {
603 if (!applied.hasOwnProperty(key)) { // If we haven't already coerced this option via one of its aliases
604 coerce = checkAllAliases(key, flags.coercions)
605 if (typeof coerce === 'function') {
607 var value = coerce(argv[key])
608 ;([].concat(flags.aliases[key] || [], key)).forEach(ali => {
609 applied[ali] = argv[ali] = value
619 function setPlaceholderKeys (argv) {
620 flags.keys.forEach((key) => {
621 // don't set placeholder keys for dot notation options 'foo.bar'.
622 if (~key.indexOf('.')) return
623 if (typeof argv[key] === 'undefined') argv[key] = undefined
628 function applyDefaultsAndAliases (obj, aliases, defaults) {
629 Object.keys(defaults).forEach(function (key) {
630 if (!hasKey(obj, key.split('.'))) {
631 setKey(obj, key.split('.'), defaults[key])
633 ;(aliases[key] || []).forEach(function (x) {
634 if (hasKey(obj, x.split('.'))) return
635 setKey(obj, x.split('.'), defaults[key])
641 function hasKey (obj, keys) {
644 if (!configuration['dot-notation']) keys = [keys.join('.')]
646 keys.slice(0, -1).forEach(function (key) {
650 var key = keys[keys.length - 1]
652 if (typeof o !== 'object') return false
656 function setKey (obj, keys, value) {
659 if (!configuration['dot-notation']) keys = [keys.join('.')]
661 keys.slice(0, -1).forEach(function (key, index) {
662 // TODO(bcoe): in the next major version of yargs, switch to
663 // Object.create(null) for dot notation:
664 key = sanitizeKey(key)
666 if (typeof o === 'object' && o[key] === undefined) {
670 if (typeof o[key] !== 'object' || Array.isArray(o[key])) {
671 // ensure that o[key] is an array, and that the last item is an empty object.
672 if (Array.isArray(o[key])) {
675 o[key] = [o[key], {}]
678 // we want to update the empty object at the end of the o[key] array, so set o to that object
679 o = o[key][o[key].length - 1]
685 // TODO(bcoe): in the next major version of yargs, switch to
686 // Object.create(null) for dot notation:
687 const key = sanitizeKey(keys[keys.length - 1])
689 const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays)
690 const isValueArray = Array.isArray(value)
691 let duplicate = configuration['duplicate-arguments-array']
693 // nargs has higher priority than duplicate
694 if (!duplicate && checkAllAliases(key, flags.nargs)) {
696 if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) {
701 if (value === increment) {
702 o[key] = increment(o[key])
703 } else if (Array.isArray(o[key])) {
704 if (duplicate && isTypeArray && isValueArray) {
705 o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value])
706 } else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) {
709 o[key] = o[key].concat([value])
711 } else if (o[key] === undefined && isTypeArray) {
712 o[key] = isValueArray ? value : [value]
713 } else if (duplicate && !(o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(keys.join('.'), flags.bools) || checkAllAliases(key, flags.counts))) {
714 o[key] = [ o[key], value ]
720 // extend the aliases list with inferred aliases.
721 function extendAliases (...args) {
722 args.forEach(function (obj) {
723 Object.keys(obj || {}).forEach(function (key) {
724 // short-circuit if we've already added a key
725 // to the aliases array, for example it might
726 // exist in both 'opts.default' and 'opts.key'.
727 if (flags.aliases[key]) return
729 flags.aliases[key] = [].concat(aliases[key] || [])
730 // For "--option-name", also set argv.optionName
731 flags.aliases[key].concat(key).forEach(function (x) {
732 if (/-/.test(x) && configuration['camel-case-expansion']) {
734 if (c !== key && flags.aliases[key].indexOf(c) === -1) {
735 flags.aliases[key].push(c)
740 // For "--optionName", also set argv['option-name']
741 flags.aliases[key].concat(key).forEach(function (x) {
742 if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) {
743 var c = decamelize(x, '-')
744 if (c !== key && flags.aliases[key].indexOf(c) === -1) {
745 flags.aliases[key].push(c)
750 flags.aliases[key].forEach(function (x) {
751 flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) {
759 // check if a flag is set for any of a key's aliases.
760 function checkAllAliases (key, flag) {
762 var toCheck = [].concat(flags.aliases[key] || [], key)
764 toCheck.forEach(function (key) {
765 if (flag[key]) isSet = flag[key]
771 function setDefaulted (key) {
772 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
773 flags.defaulted[k] = true
777 function unsetDefaulted (key) {
778 [].concat(flags.aliases[key] || [], key).forEach(function (k) {
779 delete flags.defaulted[k]
783 // make a best effor to pick a default value
784 // for an option based on name and type.
785 function defaultValue (key) {
786 if (!checkAllAliases(key, flags.bools) &&
787 !checkAllAliases(key, flags.counts) &&
788 `${key}` in defaults) {
791 return defaultForType(guessType(key))
795 // return a default value, given the type of a flag.,
796 // e.g., key of type 'string' will default to '', rather than 'true'.
797 function defaultForType (type) {
808 // given a flag, enforce a default type.
809 function guessType (key) {
812 if (checkAllAliases(key, flags.strings)) type = 'string'
813 else if (checkAllAliases(key, flags.numbers)) type = 'number'
814 else if (checkAllAliases(key, flags.arrays)) type = 'array'
819 function isNumber (x) {
820 if (x === null || x === undefined) return false
821 // if loaded from config, may already be a number.
822 if (typeof x === 'number') return true
824 if (/^0x[0-9a-f]+$/i.test(x)) return true
825 // don't treat 0123 as a number; as it drops the leading '0'.
826 if (x.length > 1 && x[0] === '0') return false
827 return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
830 function isUndefined (num) {
831 return num === undefined
837 aliases: flags.aliases,
838 newAliases: newAliases,
839 configuration: configuration
843 // if any aliases reference each other, we should
844 // merge them together.
845 function combineAliases (aliases) {
850 // turn alias lookup hash {key: ['alias1', 'alias2']} into
851 // a simple array ['key', 'alias1', 'alias2']
852 Object.keys(aliases).forEach(function (key) {
854 [].concat(aliases[key], key)
858 // combine arrays until zero changes are
859 // made in an iteration.
862 for (var i = 0; i < aliasArrays.length; i++) {
863 for (var ii = i + 1; ii < aliasArrays.length; ii++) {
864 var intersect = aliasArrays[i].filter(function (v) {
865 return aliasArrays[ii].indexOf(v) !== -1
868 if (intersect.length) {
869 aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii])
870 aliasArrays.splice(ii, 1)
878 // map arrays back to the hash-lookup (de-dupe while
880 aliasArrays.forEach(function (aliasArray) {
881 aliasArray = aliasArray.filter(function (v, i, self) {
882 return self.indexOf(v) === i
884 combined[aliasArray.pop()] = aliasArray
890 // this function should only be called when a count is given as an arg
891 // it is NOT called to set a default value
892 // thus we can start the count at 1 instead of 0
893 function increment (orig) {
894 return orig !== undefined ? orig + 1 : 1
897 function Parser (args, opts) {
898 var result = parse(args.slice(), opts)
903 // parse arguments and return detailed
904 // meta information, aliases, etc.
905 Parser.detailed = function (args, opts) {
906 return parse(args.slice(), opts)
909 // TODO(bcoe): in the next major version of yargs, switch to
910 // Object.create(null) for dot notation:
911 function sanitizeKey (key) {
912 if (key === '__proto__') return '___proto___'
916 module.exports = Parser