Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
904 lines
27 KiB
JavaScript
904 lines
27 KiB
JavaScript
var camelCase = require('camelcase')
|
|
var decamelize = require('decamelize')
|
|
var path = require('path')
|
|
var tokenizeArgString = require('./lib/tokenize-arg-string')
|
|
var util = require('util')
|
|
|
|
function parse (args, opts) {
|
|
if (!opts) opts = {}
|
|
// allow a string argument to be passed in rather
|
|
// than an argv array.
|
|
args = tokenizeArgString(args)
|
|
|
|
// aliases might have transitive relationships, normalize this.
|
|
var aliases = combineAliases(opts.alias || {})
|
|
var configuration = Object.assign({
|
|
'short-option-groups': true,
|
|
'camel-case-expansion': true,
|
|
'dot-notation': true,
|
|
'parse-numbers': true,
|
|
'boolean-negation': true,
|
|
'negation-prefix': 'no-',
|
|
'duplicate-arguments-array': true,
|
|
'flatten-duplicate-arrays': true,
|
|
'populate--': false,
|
|
'combine-arrays': false,
|
|
'set-placeholder-key': false,
|
|
'halt-at-non-option': false,
|
|
'strip-aliased': false,
|
|
'strip-dashed': false
|
|
}, opts.configuration)
|
|
var defaults = opts.default || {}
|
|
var configObjects = opts.configObjects || []
|
|
var envPrefix = opts.envPrefix
|
|
var notFlagsOption = configuration['populate--']
|
|
var notFlagsArgv = notFlagsOption ? '--' : '_'
|
|
var newAliases = {}
|
|
// allow a i18n handler to be passed in, default to a fake one (util.format).
|
|
var __ = opts.__ || util.format
|
|
var error = null
|
|
var flags = {
|
|
aliases: {},
|
|
arrays: {},
|
|
bools: {},
|
|
strings: {},
|
|
numbers: {},
|
|
counts: {},
|
|
normalize: {},
|
|
configs: {},
|
|
defaulted: {},
|
|
nargs: {},
|
|
coercions: {},
|
|
keys: []
|
|
}
|
|
var negative = /^-[0-9]+(\.[0-9]+)?/
|
|
var negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)')
|
|
|
|
;[].concat(opts.array).filter(Boolean).forEach(function (opt) {
|
|
var key = opt.key || opt
|
|
|
|
// assign to flags[bools|strings|numbers]
|
|
const assignment = Object.keys(opt).map(function (key) {
|
|
return ({
|
|
boolean: 'bools',
|
|
string: 'strings',
|
|
number: 'numbers'
|
|
})[key]
|
|
}).filter(Boolean).pop()
|
|
|
|
// assign key to be coerced
|
|
if (assignment) {
|
|
flags[assignment][key] = true
|
|
}
|
|
|
|
flags.arrays[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
;[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
|
|
flags.bools[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
;[].concat(opts.string).filter(Boolean).forEach(function (key) {
|
|
flags.strings[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
;[].concat(opts.number).filter(Boolean).forEach(function (key) {
|
|
flags.numbers[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
;[].concat(opts.count).filter(Boolean).forEach(function (key) {
|
|
flags.counts[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
|
|
flags.normalize[key] = true
|
|
flags.keys.push(key)
|
|
})
|
|
|
|
Object.keys(opts.narg || {}).forEach(function (k) {
|
|
flags.nargs[k] = opts.narg[k]
|
|
flags.keys.push(k)
|
|
})
|
|
|
|
Object.keys(opts.coerce || {}).forEach(function (k) {
|
|
flags.coercions[k] = opts.coerce[k]
|
|
flags.keys.push(k)
|
|
})
|
|
|
|
if (Array.isArray(opts.config) || typeof opts.config === 'string') {
|
|
;[].concat(opts.config).filter(Boolean).forEach(function (key) {
|
|
flags.configs[key] = true
|
|
})
|
|
} else {
|
|
Object.keys(opts.config || {}).forEach(function (k) {
|
|
flags.configs[k] = opts.config[k]
|
|
})
|
|
}
|
|
|
|
// create a lookup table that takes into account all
|
|
// combinations of aliases: {f: ['foo'], foo: ['f']}
|
|
extendAliases(opts.key, aliases, opts.default, flags.arrays)
|
|
|
|
// apply default values to all aliases.
|
|
Object.keys(defaults).forEach(function (key) {
|
|
(flags.aliases[key] || []).forEach(function (alias) {
|
|
defaults[alias] = defaults[key]
|
|
})
|
|
})
|
|
|
|
var argv = { _: [] }
|
|
|
|
Object.keys(flags.bools).forEach(function (key) {
|
|
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
|
|
setArg(key, defaults[key])
|
|
setDefaulted(key)
|
|
}
|
|
})
|
|
|
|
var notFlags = []
|
|
|
|
for (var i = 0; i < args.length; i++) {
|
|
var arg = args[i]
|
|
var broken
|
|
var key
|
|
var letters
|
|
var m
|
|
var next
|
|
var value
|
|
|
|
// -- separated by =
|
|
if (arg.match(/^--.+=/) || (
|
|
!configuration['short-option-groups'] && arg.match(/^-.+=/)
|
|
)) {
|
|
// Using [\s\S] instead of . because js doesn't support the
|
|
// 'dotall' regex modifier. See:
|
|
// http://stackoverflow.com/a/1068308/13216
|
|
m = arg.match(/^--?([^=]+)=([\s\S]*)$/)
|
|
|
|
// nargs format = '--f=monkey washing cat'
|
|
if (checkAllAliases(m[1], flags.nargs)) {
|
|
args.splice(i + 1, 0, m[2])
|
|
i = eatNargs(i, m[1], args)
|
|
// arrays format = '--f=a b c'
|
|
} else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
|
|
args.splice(i + 1, 0, m[2])
|
|
i = eatArray(i, m[1], args)
|
|
} else {
|
|
setArg(m[1], m[2])
|
|
}
|
|
} else if (arg.match(negatedBoolean) && configuration['boolean-negation']) {
|
|
key = arg.match(negatedBoolean)[1]
|
|
setArg(key, false)
|
|
|
|
// -- seperated by space.
|
|
} else if (arg.match(/^--.+/) || (
|
|
!configuration['short-option-groups'] && arg.match(/^-[^-]+/)
|
|
)) {
|
|
key = arg.match(/^--?(.+)/)[1]
|
|
|
|
// nargs format = '--foo a b c'
|
|
if (checkAllAliases(key, flags.nargs)) {
|
|
i = eatNargs(i, key, args)
|
|
// array format = '--foo a b c'
|
|
} else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
|
|
i = eatArray(i, key, args)
|
|
} else {
|
|
next = flags.nargs[key] === 0 ? undefined : args[i + 1]
|
|
|
|
if (next !== undefined && (!next.match(/^-/) ||
|
|
next.match(negative)) &&
|
|
!checkAllAliases(key, flags.bools) &&
|
|
!checkAllAliases(key, flags.counts)) {
|
|
setArg(key, next)
|
|
i++
|
|
} else if (/^(true|false)$/.test(next)) {
|
|
setArg(key, next)
|
|
i++
|
|
} else {
|
|
setArg(key, defaultValue(key))
|
|
}
|
|
}
|
|
|
|
// dot-notation flag seperated by '='.
|
|
} else if (arg.match(/^-.\..+=/)) {
|
|
m = arg.match(/^-([^=]+)=([\s\S]*)$/)
|
|
setArg(m[1], m[2])
|
|
|
|
// dot-notation flag seperated by space.
|
|
} else if (arg.match(/^-.\..+/)) {
|
|
next = args[i + 1]
|
|
key = arg.match(/^-(.\..+)/)[1]
|
|
|
|
if (next !== undefined && !next.match(/^-/) &&
|
|
!checkAllAliases(key, flags.bools) &&
|
|
!checkAllAliases(key, flags.counts)) {
|
|
setArg(key, next)
|
|
i++
|
|
} else {
|
|
setArg(key, defaultValue(key))
|
|
}
|
|
} else if (arg.match(/^-[^-]+/) && !arg.match(negative)) {
|
|
letters = arg.slice(1, -1).split('')
|
|
broken = false
|
|
|
|
for (var j = 0; j < letters.length; j++) {
|
|
next = arg.slice(j + 2)
|
|
|
|
if (letters[j + 1] && letters[j + 1] === '=') {
|
|
value = arg.slice(j + 3)
|
|
key = letters[j]
|
|
|
|
// nargs format = '-f=monkey washing cat'
|
|
if (checkAllAliases(key, flags.nargs)) {
|
|
args.splice(i + 1, 0, value)
|
|
i = eatNargs(i, key, args)
|
|
// array format = '-f=a b c'
|
|
} else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
|
|
args.splice(i + 1, 0, value)
|
|
i = eatArray(i, key, args)
|
|
} else {
|
|
setArg(key, value)
|
|
}
|
|
|
|
broken = true
|
|
break
|
|
}
|
|
|
|
if (next === '-') {
|
|
setArg(letters[j], next)
|
|
continue
|
|
}
|
|
|
|
// current letter is an alphabetic character and next value is a number
|
|
if (/[A-Za-z]/.test(letters[j]) &&
|
|
/^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
|
|
setArg(letters[j], next)
|
|
broken = true
|
|
break
|
|
}
|
|
|
|
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
|
setArg(letters[j], next)
|
|
broken = true
|
|
break
|
|
} else {
|
|
setArg(letters[j], defaultValue(letters[j]))
|
|
}
|
|
}
|
|
|
|
key = arg.slice(-1)[0]
|
|
|
|
if (!broken && key !== '-') {
|
|
// nargs format = '-f a b c'
|
|
if (checkAllAliases(key, flags.nargs)) {
|
|
i = eatNargs(i, key, args)
|
|
// array format = '-f a b c'
|
|
} else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
|
|
i = eatArray(i, key, args)
|
|
} else {
|
|
next = args[i + 1]
|
|
|
|
if (next !== undefined && (!/^(-|--)[^-]/.test(next) ||
|
|
next.match(negative)) &&
|
|
!checkAllAliases(key, flags.bools) &&
|
|
!checkAllAliases(key, flags.counts)) {
|
|
setArg(key, next)
|
|
i++
|
|
} else if (/^(true|false)$/.test(next)) {
|
|
setArg(key, next)
|
|
i++
|
|
} else {
|
|
setArg(key, defaultValue(key))
|
|
}
|
|
}
|
|
}
|
|
} else if (arg === '--') {
|
|
notFlags = args.slice(i + 1)
|
|
break
|
|
} else if (configuration['halt-at-non-option']) {
|
|
notFlags = args.slice(i)
|
|
break
|
|
} else {
|
|
argv._.push(maybeCoerceNumber('_', arg))
|
|
}
|
|
}
|
|
|
|
// order of precedence:
|
|
// 1. command line arg
|
|
// 2. value from env var
|
|
// 3. value from config file
|
|
// 4. value from config objects
|
|
// 5. configured default value
|
|
applyEnvVars(argv, true) // special case: check env vars that point to config file
|
|
applyEnvVars(argv, false)
|
|
setConfig(argv)
|
|
setConfigObjects()
|
|
applyDefaultsAndAliases(argv, flags.aliases, defaults)
|
|
applyCoercions(argv)
|
|
if (configuration['set-placeholder-key']) setPlaceholderKeys(argv)
|
|
|
|
// for any counts either not in args or without an explicit default, set to 0
|
|
Object.keys(flags.counts).forEach(function (key) {
|
|
if (!hasKey(argv, key.split('.'))) setArg(key, 0)
|
|
})
|
|
|
|
// '--' defaults to undefined.
|
|
if (notFlagsOption && notFlags.length) argv[notFlagsArgv] = []
|
|
notFlags.forEach(function (key) {
|
|
argv[notFlagsArgv].push(key)
|
|
})
|
|
|
|
if (configuration['camel-case-expansion'] && configuration['strip-dashed']) {
|
|
Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => {
|
|
delete argv[key]
|
|
})
|
|
}
|
|
|
|
if (configuration['strip-aliased']) {
|
|
// XXX Switch to [].concat(...Object.values(aliases)) once node.js 6 is dropped
|
|
;[].concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => {
|
|
if (configuration['camel-case-expansion']) {
|
|
delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')]
|
|
}
|
|
|
|
delete argv[alias]
|
|
})
|
|
}
|
|
|
|
// how many arguments should we consume, based
|
|
// on the nargs option?
|
|
function eatNargs (i, key, args) {
|
|
var ii
|
|
const toEat = checkAllAliases(key, flags.nargs)
|
|
|
|
// nargs will not consume flag arguments, e.g., -abc, --foo,
|
|
// and terminates when one is observed.
|
|
var available = 0
|
|
for (ii = i + 1; ii < args.length; ii++) {
|
|
if (!args[ii].match(/^-[^0-9]/)) available++
|
|
else break
|
|
}
|
|
|
|
if (available < toEat) error = Error(__('Not enough arguments following: %s', key))
|
|
|
|
const consumed = Math.min(available, toEat)
|
|
for (ii = i + 1; ii < (consumed + i + 1); ii++) {
|
|
setArg(key, args[ii])
|
|
}
|
|
|
|
return (i + consumed)
|
|
}
|
|
|
|
// if an option is an array, eat all non-hyphenated arguments
|
|
// following it... YUM!
|
|
// e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
|
|
function eatArray (i, key, args) {
|
|
var start = i + 1
|
|
var argsToSet = []
|
|
var multipleArrayFlag = i > 0
|
|
for (var ii = i + 1; ii < args.length; ii++) {
|
|
if (/^-/.test(args[ii]) && !negative.test(args[ii])) {
|
|
if (ii === start) {
|
|
setArg(key, defaultForType('array'))
|
|
}
|
|
multipleArrayFlag = true
|
|
break
|
|
}
|
|
i = ii
|
|
argsToSet.push(args[ii])
|
|
}
|
|
if (multipleArrayFlag) {
|
|
setArg(key, argsToSet.map(function (arg) {
|
|
return processValue(key, arg)
|
|
}))
|
|
} else {
|
|
argsToSet.forEach(function (arg) {
|
|
setArg(key, arg)
|
|
})
|
|
}
|
|
|
|
return i
|
|
}
|
|
|
|
function setArg (key, val) {
|
|
unsetDefaulted(key)
|
|
|
|
if (/-/.test(key) && configuration['camel-case-expansion']) {
|
|
var alias = key.split('.').map(function (prop) {
|
|
return camelCase(prop)
|
|
}).join('.')
|
|
addNewAlias(key, alias)
|
|
}
|
|
|
|
var value = processValue(key, val)
|
|
|
|
var splitKey = key.split('.')
|
|
setKey(argv, splitKey, value)
|
|
|
|
// handle populating aliases of the full key
|
|
if (flags.aliases[key]) {
|
|
flags.aliases[key].forEach(function (x) {
|
|
x = x.split('.')
|
|
setKey(argv, x, value)
|
|
})
|
|
}
|
|
|
|
// handle populating aliases of the first element of the dot-notation key
|
|
if (splitKey.length > 1 && configuration['dot-notation']) {
|
|
;(flags.aliases[splitKey[0]] || []).forEach(function (x) {
|
|
x = x.split('.')
|
|
|
|
// expand alias with nested objects in key
|
|
var a = [].concat(splitKey)
|
|
a.shift() // nuke the old key.
|
|
x = x.concat(a)
|
|
|
|
setKey(argv, x, value)
|
|
})
|
|
}
|
|
|
|
// Set normalize getter and setter when key is in 'normalize' but isn't an array
|
|
if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) {
|
|
var keys = [key].concat(flags.aliases[key] || [])
|
|
keys.forEach(function (key) {
|
|
argv.__defineSetter__(key, function (v) {
|
|
val = path.normalize(v)
|
|
})
|
|
|
|
argv.__defineGetter__(key, function () {
|
|
return typeof val === 'string' ? path.normalize(val) : val
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
function addNewAlias (key, alias) {
|
|
if (!(flags.aliases[key] && flags.aliases[key].length)) {
|
|
flags.aliases[key] = [alias]
|
|
newAliases[alias] = true
|
|
}
|
|
if (!(flags.aliases[alias] && flags.aliases[alias].length)) {
|
|
addNewAlias(alias, key)
|
|
}
|
|
}
|
|
|
|
function processValue (key, val) {
|
|
// strings may be quoted, clean this up as we assign values.
|
|
if (typeof val === 'string' &&
|
|
(val[0] === "'" || val[0] === '"') &&
|
|
val[val.length - 1] === val[0]
|
|
) {
|
|
val = val.substring(1, val.length - 1)
|
|
}
|
|
|
|
// handle parsing boolean arguments --foo=true --bar false.
|
|
if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
|
|
if (typeof val === 'string') val = val === 'true'
|
|
}
|
|
|
|
var value = maybeCoerceNumber(key, val)
|
|
|
|
// increment a count given as arg (either no value or value parsed as boolean)
|
|
if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) {
|
|
value = increment
|
|
}
|
|
|
|
// Set normalized value when key is in 'normalize' and in 'arrays'
|
|
if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) {
|
|
if (Array.isArray(val)) value = val.map(path.normalize)
|
|
else value = path.normalize(val)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function maybeCoerceNumber (key, value) {
|
|
if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.coercions)) {
|
|
const shouldCoerceNumber = isNumber(value) && configuration['parse-numbers'] && (
|
|
Number.isSafeInteger(Math.floor(value))
|
|
)
|
|
if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) value = Number(value)
|
|
}
|
|
return value
|
|
}
|
|
|
|
// set args from config.json file, this should be
|
|
// applied last so that defaults can be applied.
|
|
function setConfig (argv) {
|
|
var configLookup = {}
|
|
|
|
// expand defaults/aliases, in-case any happen to reference
|
|
// the config.json file.
|
|
applyDefaultsAndAliases(configLookup, flags.aliases, defaults)
|
|
|
|
Object.keys(flags.configs).forEach(function (configKey) {
|
|
var configPath = argv[configKey] || configLookup[configKey]
|
|
if (configPath) {
|
|
try {
|
|
var config = null
|
|
var resolvedConfigPath = path.resolve(process.cwd(), configPath)
|
|
|
|
if (typeof flags.configs[configKey] === 'function') {
|
|
try {
|
|
config = flags.configs[configKey](resolvedConfigPath)
|
|
} catch (e) {
|
|
config = e
|
|
}
|
|
if (config instanceof Error) {
|
|
error = config
|
|
return
|
|
}
|
|
} else {
|
|
config = require(resolvedConfigPath)
|
|
}
|
|
|
|
setConfigObject(config)
|
|
} catch (ex) {
|
|
if (argv[configKey]) error = Error(__('Invalid JSON config file: %s', configPath))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// set args from config object.
|
|
// it recursively checks nested objects.
|
|
function setConfigObject (config, prev) {
|
|
Object.keys(config).forEach(function (key) {
|
|
var value = config[key]
|
|
var fullKey = prev ? prev + '.' + key : key
|
|
|
|
// if the value is an inner object and we have dot-notation
|
|
// enabled, treat inner objects in config the same as
|
|
// heavily nested dot notations (foo.bar.apple).
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) {
|
|
// if the value is an object but not an array, check nested object
|
|
setConfigObject(value, fullKey)
|
|
} else {
|
|
// setting arguments via CLI takes precedence over
|
|
// values within the config file.
|
|
if (!hasKey(argv, fullKey.split('.')) || (flags.defaulted[fullKey]) || (flags.arrays[fullKey] && configuration['combine-arrays'])) {
|
|
setArg(fullKey, value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// set all config objects passed in opts
|
|
function setConfigObjects () {
|
|
if (typeof configObjects === 'undefined') return
|
|
configObjects.forEach(function (configObject) {
|
|
setConfigObject(configObject)
|
|
})
|
|
}
|
|
|
|
function applyEnvVars (argv, configOnly) {
|
|
if (typeof envPrefix === 'undefined') return
|
|
|
|
var prefix = typeof envPrefix === 'string' ? envPrefix : ''
|
|
Object.keys(process.env).forEach(function (envVar) {
|
|
if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
|
|
// get array of nested keys and convert them to camel case
|
|
var keys = envVar.split('__').map(function (key, i) {
|
|
if (i === 0) {
|
|
key = key.substring(prefix.length)
|
|
}
|
|
return camelCase(key)
|
|
})
|
|
|
|
if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && (!hasKey(argv, keys) || flags.defaulted[keys.join('.')])) {
|
|
setArg(keys.join('.'), process.env[envVar])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function applyCoercions (argv) {
|
|
var coerce
|
|
var applied = {}
|
|
Object.keys(argv).forEach(function (key) {
|
|
if (!applied.hasOwnProperty(key)) { // If we haven't already coerced this option via one of its aliases
|
|
coerce = checkAllAliases(key, flags.coercions)
|
|
if (typeof coerce === 'function') {
|
|
try {
|
|
var value = coerce(argv[key])
|
|
;([].concat(flags.aliases[key] || [], key)).forEach(ali => {
|
|
applied[ali] = argv[ali] = value
|
|
})
|
|
} catch (err) {
|
|
error = err
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function setPlaceholderKeys (argv) {
|
|
flags.keys.forEach((key) => {
|
|
// don't set placeholder keys for dot notation options 'foo.bar'.
|
|
if (~key.indexOf('.')) return
|
|
if (typeof argv[key] === 'undefined') argv[key] = undefined
|
|
})
|
|
return argv
|
|
}
|
|
|
|
function applyDefaultsAndAliases (obj, aliases, defaults) {
|
|
Object.keys(defaults).forEach(function (key) {
|
|
if (!hasKey(obj, key.split('.'))) {
|
|
setKey(obj, key.split('.'), defaults[key])
|
|
|
|
;(aliases[key] || []).forEach(function (x) {
|
|
if (hasKey(obj, x.split('.'))) return
|
|
setKey(obj, x.split('.'), defaults[key])
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function hasKey (obj, keys) {
|
|
var o = obj
|
|
|
|
if (!configuration['dot-notation']) keys = [keys.join('.')]
|
|
|
|
keys.slice(0, -1).forEach(function (key) {
|
|
o = (o[key] || {})
|
|
})
|
|
|
|
var key = keys[keys.length - 1]
|
|
|
|
if (typeof o !== 'object') return false
|
|
else return key in o
|
|
}
|
|
|
|
function setKey (obj, keys, value) {
|
|
var o = obj
|
|
|
|
if (!configuration['dot-notation']) keys = [keys.join('.')]
|
|
|
|
keys.slice(0, -1).forEach(function (key, index) {
|
|
if (typeof o === 'object' && o[key] === undefined) {
|
|
o[key] = {}
|
|
}
|
|
|
|
if (typeof o[key] !== 'object' || Array.isArray(o[key])) {
|
|
// ensure that o[key] is an array, and that the last item is an empty object.
|
|
if (Array.isArray(o[key])) {
|
|
o[key].push({})
|
|
} else {
|
|
o[key] = [o[key], {}]
|
|
}
|
|
|
|
// we want to update the empty object at the end of the o[key] array, so set o to that object
|
|
o = o[key][o[key].length - 1]
|
|
} else {
|
|
o = o[key]
|
|
}
|
|
})
|
|
|
|
var key = keys[keys.length - 1]
|
|
|
|
var isTypeArray = checkAllAliases(keys.join('.'), flags.arrays)
|
|
var isValueArray = Array.isArray(value)
|
|
var duplicate = configuration['duplicate-arguments-array']
|
|
|
|
// nargs has higher priority than duplicate
|
|
if (!duplicate && checkAllAliases(key, flags.nargs)) {
|
|
duplicate = true
|
|
if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) {
|
|
o[key] = undefined
|
|
}
|
|
}
|
|
|
|
if (value === increment) {
|
|
o[key] = increment(o[key])
|
|
} else if (Array.isArray(o[key])) {
|
|
if (duplicate && isTypeArray && isValueArray) {
|
|
o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value])
|
|
} else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) {
|
|
o[key] = value
|
|
} else {
|
|
o[key] = o[key].concat([value])
|
|
}
|
|
} else if (o[key] === undefined && isTypeArray) {
|
|
o[key] = isValueArray ? value : [value]
|
|
} else if (duplicate && !(o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(keys.join('.'), flags.bools) || checkAllAliases(key, flags.counts))) {
|
|
o[key] = [ o[key], value ]
|
|
} else {
|
|
o[key] = value
|
|
}
|
|
}
|
|
|
|
// extend the aliases list with inferred aliases.
|
|
function extendAliases (...args) {
|
|
args.forEach(function (obj) {
|
|
Object.keys(obj || {}).forEach(function (key) {
|
|
// short-circuit if we've already added a key
|
|
// to the aliases array, for example it might
|
|
// exist in both 'opts.default' and 'opts.key'.
|
|
if (flags.aliases[key]) return
|
|
|
|
flags.aliases[key] = [].concat(aliases[key] || [])
|
|
// For "--option-name", also set argv.optionName
|
|
flags.aliases[key].concat(key).forEach(function (x) {
|
|
if (/-/.test(x) && configuration['camel-case-expansion']) {
|
|
var c = camelCase(x)
|
|
if (c !== key && flags.aliases[key].indexOf(c) === -1) {
|
|
flags.aliases[key].push(c)
|
|
newAliases[c] = true
|
|
}
|
|
}
|
|
})
|
|
// For "--optionName", also set argv['option-name']
|
|
flags.aliases[key].concat(key).forEach(function (x) {
|
|
if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) {
|
|
var c = decamelize(x, '-')
|
|
if (c !== key && flags.aliases[key].indexOf(c) === -1) {
|
|
flags.aliases[key].push(c)
|
|
newAliases[c] = true
|
|
}
|
|
}
|
|
})
|
|
flags.aliases[key].forEach(function (x) {
|
|
flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) {
|
|
return x !== y
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
// check if a flag is set for any of a key's aliases.
|
|
function checkAllAliases (key, flag) {
|
|
var isSet = false
|
|
var toCheck = [].concat(flags.aliases[key] || [], key)
|
|
|
|
toCheck.forEach(function (key) {
|
|
if (flag[key]) isSet = flag[key]
|
|
})
|
|
|
|
return isSet
|
|
}
|
|
|
|
function setDefaulted (key) {
|
|
[].concat(flags.aliases[key] || [], key).forEach(function (k) {
|
|
flags.defaulted[k] = true
|
|
})
|
|
}
|
|
|
|
function unsetDefaulted (key) {
|
|
[].concat(flags.aliases[key] || [], key).forEach(function (k) {
|
|
delete flags.defaulted[k]
|
|
})
|
|
}
|
|
|
|
// make a best effor to pick a default value
|
|
// for an option based on name and type.
|
|
function defaultValue (key) {
|
|
if (!checkAllAliases(key, flags.bools) &&
|
|
!checkAllAliases(key, flags.counts) &&
|
|
`${key}` in defaults) {
|
|
return defaults[key]
|
|
} else {
|
|
return defaultForType(guessType(key))
|
|
}
|
|
}
|
|
|
|
// return a default value, given the type of a flag.,
|
|
// e.g., key of type 'string' will default to '', rather than 'true'.
|
|
function defaultForType (type) {
|
|
var def = {
|
|
boolean: true,
|
|
string: '',
|
|
number: undefined,
|
|
array: []
|
|
}
|
|
|
|
return def[type]
|
|
}
|
|
|
|
// given a flag, enforce a default type.
|
|
function guessType (key) {
|
|
var type = 'boolean'
|
|
|
|
if (checkAllAliases(key, flags.strings)) type = 'string'
|
|
else if (checkAllAliases(key, flags.numbers)) type = 'number'
|
|
else if (checkAllAliases(key, flags.arrays)) type = 'array'
|
|
|
|
return type
|
|
}
|
|
|
|
function isNumber (x) {
|
|
if (x === null || x === undefined) return false
|
|
// if loaded from config, may already be a number.
|
|
if (typeof x === 'number') return true
|
|
// hexadecimal.
|
|
if (/^0x[0-9a-f]+$/i.test(x)) return true
|
|
// don't treat 0123 as a number; as it drops the leading '0'.
|
|
if (x.length > 1 && x[0] === '0') return false
|
|
return /^[-]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
|
|
}
|
|
|
|
function isUndefined (num) {
|
|
return num === undefined
|
|
}
|
|
|
|
return {
|
|
argv: argv,
|
|
error: error,
|
|
aliases: flags.aliases,
|
|
newAliases: newAliases,
|
|
configuration: configuration
|
|
}
|
|
}
|
|
|
|
// if any aliases reference each other, we should
|
|
// merge them together.
|
|
function combineAliases (aliases) {
|
|
var aliasArrays = []
|
|
var change = true
|
|
var combined = {}
|
|
|
|
// turn alias lookup hash {key: ['alias1', 'alias2']} into
|
|
// a simple array ['key', 'alias1', 'alias2']
|
|
Object.keys(aliases).forEach(function (key) {
|
|
aliasArrays.push(
|
|
[].concat(aliases[key], key)
|
|
)
|
|
})
|
|
|
|
// combine arrays until zero changes are
|
|
// made in an iteration.
|
|
while (change) {
|
|
change = false
|
|
for (var i = 0; i < aliasArrays.length; i++) {
|
|
for (var ii = i + 1; ii < aliasArrays.length; ii++) {
|
|
var intersect = aliasArrays[i].filter(function (v) {
|
|
return aliasArrays[ii].indexOf(v) !== -1
|
|
})
|
|
|
|
if (intersect.length) {
|
|
aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii])
|
|
aliasArrays.splice(ii, 1)
|
|
change = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// map arrays back to the hash-lookup (de-dupe while
|
|
// we're at it).
|
|
aliasArrays.forEach(function (aliasArray) {
|
|
aliasArray = aliasArray.filter(function (v, i, self) {
|
|
return self.indexOf(v) === i
|
|
})
|
|
combined[aliasArray.pop()] = aliasArray
|
|
})
|
|
|
|
return combined
|
|
}
|
|
|
|
// this function should only be called when a count is given as an arg
|
|
// it is NOT called to set a default value
|
|
// thus we can start the count at 1 instead of 0
|
|
function increment (orig) {
|
|
return orig !== undefined ? orig + 1 : 1
|
|
}
|
|
|
|
function Parser (args, opts) {
|
|
var result = parse(args.slice(), opts)
|
|
|
|
return result.argv
|
|
}
|
|
|
|
// parse arguments and return detailed
|
|
// meta information, aliases, etc.
|
|
Parser.detailed = function (args, opts) {
|
|
return parse(args.slice(), opts)
|
|
}
|
|
|
|
module.exports = Parser
|