Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts Build Tailwind locally, add safelist; switch to assets/tailwind.css Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self) Extract inline handlers; move PWA scripts to external files Add local QR code generation (qrcode lib) and remove api.qrserver.com Improve SessionTypeSelector visual selection (highlighted background and ring) Keep PWA working with service worker and offline assets Refs: CSP hardening, offline-first, no external dependencies
1033 lines
33 KiB
JavaScript
1033 lines
33 KiB
JavaScript
const camelCase = require('camelcase')
|
|
const decamelize = require('decamelize')
|
|
const path = require('path')
|
|
const tokenizeArgString = require('./lib/tokenize-arg-string')
|
|
const util = require('util')
|
|
|
|
function parse (args, opts) {
|
|
opts = Object.assign(Object.create(null), opts)
|
|
// allow a string argument to be passed in rather
|
|
// than an argv array.
|
|
args = tokenizeArgString(args)
|
|
|
|
// aliases might have transitive relationships, normalize this.
|
|
const aliases = combineAliases(Object.assign(Object.create(null), opts.alias))
|
|
const configuration = Object.assign({
|
|
'boolean-negation': true,
|
|
'camel-case-expansion': true,
|
|
'combine-arrays': false,
|
|
'dot-notation': true,
|
|
'duplicate-arguments-array': true,
|
|
'flatten-duplicate-arrays': true,
|
|
'greedy-arrays': true,
|
|
'halt-at-non-option': false,
|
|
'nargs-eats-options': false,
|
|
'negation-prefix': 'no-',
|
|
'parse-numbers': true,
|
|
'populate--': false,
|
|
'set-placeholder-key': false,
|
|
'short-option-groups': true,
|
|
'strip-aliased': false,
|
|
'strip-dashed': false,
|
|
'unknown-options-as-args': false
|
|
}, opts.configuration)
|
|
const defaults = Object.assign(Object.create(null), opts.default)
|
|
const configObjects = opts.configObjects || []
|
|
const envPrefix = opts.envPrefix
|
|
const notFlagsOption = configuration['populate--']
|
|
const notFlagsArgv = notFlagsOption ? '--' : '_'
|
|
const newAliases = Object.create(null)
|
|
const defaulted = Object.create(null)
|
|
// allow a i18n handler to be passed in, default to a fake one (util.format).
|
|
const __ = opts.__ || util.format
|
|
const flags = {
|
|
aliases: Object.create(null),
|
|
arrays: Object.create(null),
|
|
bools: Object.create(null),
|
|
strings: Object.create(null),
|
|
numbers: Object.create(null),
|
|
counts: Object.create(null),
|
|
normalize: Object.create(null),
|
|
configs: Object.create(null),
|
|
nargs: Object.create(null),
|
|
coercions: Object.create(null),
|
|
keys: []
|
|
}
|
|
const negative = /^-([0-9]+(\.[0-9]+)?|\.[0-9]+)$/
|
|
const negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)')
|
|
|
|
;[].concat(opts.array).filter(Boolean).forEach(function (opt) {
|
|
const 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]
|
|
})
|
|
})
|
|
|
|
let error = null
|
|
checkConfiguration()
|
|
|
|
let notFlags = []
|
|
|
|
const argv = Object.assign(Object.create(null), { _: [] })
|
|
// TODO(bcoe): for the first pass at removing object prototype we didn't
|
|
// remove all prototypes from objects returned by this API, we might want
|
|
// to gradually move towards doing so.
|
|
const argvReturn = {}
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i]
|
|
let broken
|
|
let key
|
|
let letters
|
|
let m
|
|
let next
|
|
let value
|
|
|
|
// any unknown option (except for end-of-options, "--")
|
|
if (arg !== '--' && isUnknownOptionAsArg(arg)) {
|
|
argv._.push(arg)
|
|
// -- separated by =
|
|
} else 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]*)$/)
|
|
|
|
// arrays format = '--f=a b c'
|
|
if (checkAllAliases(m[1], flags.arrays)) {
|
|
i = eatArray(i, m[1], args, m[2])
|
|
} else if (checkAllAliases(m[1], flags.nargs) !== false) {
|
|
// nargs format = '--f=monkey washing cat'
|
|
i = eatNargs(i, m[1], args, m[2])
|
|
} else {
|
|
setArg(m[1], m[2])
|
|
}
|
|
} else if (arg.match(negatedBoolean) && configuration['boolean-negation']) {
|
|
key = arg.match(negatedBoolean)[1]
|
|
setArg(key, checkAllAliases(key, flags.arrays) ? [false] : false)
|
|
|
|
// -- separated by space.
|
|
} else if (arg.match(/^--.+/) || (
|
|
!configuration['short-option-groups'] && arg.match(/^-[^-]+/)
|
|
)) {
|
|
key = arg.match(/^--?(.+)/)[1]
|
|
|
|
if (checkAllAliases(key, flags.arrays)) {
|
|
// array format = '--foo a b c'
|
|
i = eatArray(i, key, args)
|
|
} else if (checkAllAliases(key, flags.nargs) !== false) {
|
|
// nargs format = '--foo a b c'
|
|
// should be truthy even if: flags.nargs[key] === 0
|
|
i = eatNargs(i, key, args)
|
|
} else {
|
|
next = 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 separated by '='.
|
|
} else if (arg.match(/^-.\..+=/)) {
|
|
m = arg.match(/^-([^=]+)=([\s\S]*)$/)
|
|
setArg(m[1], m[2])
|
|
|
|
// dot-notation flag separated by space.
|
|
} else if (arg.match(/^-.\..+/) && !arg.match(negative)) {
|
|
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 (let 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]
|
|
|
|
if (checkAllAliases(key, flags.arrays)) {
|
|
// array format = '-f=a b c'
|
|
i = eatArray(i, key, args, value)
|
|
} else if (checkAllAliases(key, flags.nargs) !== false) {
|
|
// nargs format = '-f=monkey washing cat'
|
|
i = eatNargs(i, key, args, value)
|
|
} 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 !== '-') {
|
|
if (checkAllAliases(key, flags.arrays)) {
|
|
// array format = '-f a b c'
|
|
i = eatArray(i, key, args)
|
|
} else if (checkAllAliases(key, flags.nargs) !== false) {
|
|
// nargs format = '-f a b c'
|
|
// should be truthy even if: flags.nargs[key] === 0
|
|
i = eatNargs(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.match(/^-[0-9]$/) &&
|
|
arg.match(negative) &&
|
|
checkAllAliases(arg.slice(1), flags.bools)) {
|
|
// single-digit boolean alias, e.g: xargs -0
|
|
key = arg.slice(1)
|
|
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, true)
|
|
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']) {
|
|
;[].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, argAfterEqualSign) {
|
|
let ii
|
|
let toEat = checkAllAliases(key, flags.nargs)
|
|
// NaN has a special meaning for the array type, indicating that one or
|
|
// more values are expected.
|
|
toEat = isNaN(toEat) ? 1 : toEat
|
|
|
|
if (toEat === 0) {
|
|
if (!isUndefined(argAfterEqualSign)) {
|
|
error = Error(__('Argument unexpected for: %s', key))
|
|
}
|
|
setArg(key, defaultValue(key))
|
|
return i
|
|
}
|
|
|
|
let available = isUndefined(argAfterEqualSign) ? 0 : 1
|
|
if (configuration['nargs-eats-options']) {
|
|
// classic behavior, yargs eats positional and dash arguments.
|
|
if (args.length - (i + 1) + available < toEat) {
|
|
error = Error(__('Not enough arguments following: %s', key))
|
|
}
|
|
available = toEat
|
|
} else {
|
|
// nargs will not consume flag arguments, e.g., -abc, --foo,
|
|
// and terminates when one is observed.
|
|
for (ii = i + 1; ii < args.length; ii++) {
|
|
if (!args[ii].match(/^-[^0-9]/) || args[ii].match(negative) || isUnknownOptionAsArg(args[ii])) available++
|
|
else break
|
|
}
|
|
if (available < toEat) error = Error(__('Not enough arguments following: %s', key))
|
|
}
|
|
|
|
let consumed = Math.min(available, toEat)
|
|
if (!isUndefined(argAfterEqualSign) && consumed > 0) {
|
|
setArg(key, argAfterEqualSign)
|
|
consumed--
|
|
}
|
|
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, argAfterEqualSign) {
|
|
let argsToSet = []
|
|
let next = argAfterEqualSign || args[i + 1]
|
|
// If both array and nargs are configured, enforce the nargs count:
|
|
const nargsCount = checkAllAliases(key, flags.nargs)
|
|
|
|
if (checkAllAliases(key, flags.bools) && !(/^(true|false)$/.test(next))) {
|
|
argsToSet.push(true)
|
|
} else if (isUndefined(next) ||
|
|
(isUndefined(argAfterEqualSign) && /^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next))) {
|
|
// for keys without value ==> argsToSet remains an empty []
|
|
// set user default value, if available
|
|
if (defaults[key] !== undefined) {
|
|
const defVal = defaults[key]
|
|
argsToSet = Array.isArray(defVal) ? defVal : [defVal]
|
|
}
|
|
} else {
|
|
// value in --option=value is eaten as is
|
|
if (!isUndefined(argAfterEqualSign)) {
|
|
argsToSet.push(processValue(key, argAfterEqualSign))
|
|
}
|
|
for (let ii = i + 1; ii < args.length; ii++) {
|
|
if ((!configuration['greedy-arrays'] && argsToSet.length > 0) ||
|
|
(nargsCount && argsToSet.length >= nargsCount)) break
|
|
next = args[ii]
|
|
if (/^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next)) break
|
|
i = ii
|
|
argsToSet.push(processValue(key, next))
|
|
}
|
|
}
|
|
|
|
// If both array and nargs are configured, create an error if less than
|
|
// nargs positionals were found. NaN has special meaning, indicating
|
|
// that at least one value is required (more are okay).
|
|
if ((nargsCount && argsToSet.length < nargsCount) ||
|
|
(isNaN(nargsCount) && argsToSet.length === 0)) {
|
|
error = Error(__('Not enough arguments following: %s', key))
|
|
}
|
|
|
|
setArg(key, argsToSet)
|
|
return i
|
|
}
|
|
|
|
function setArg (key, val) {
|
|
if (/-/.test(key) && configuration['camel-case-expansion']) {
|
|
const alias = key.split('.').map(function (prop) {
|
|
return camelCase(prop)
|
|
}).join('.')
|
|
addNewAlias(key, alias)
|
|
}
|
|
|
|
const value = processValue(key, val)
|
|
const 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
|
|
const a = [].concat(splitKey)
|
|
a.shift() // nuke the old key.
|
|
x = x.concat(a)
|
|
|
|
// populate alias only if is not already an alias of the full key
|
|
// (already populated above)
|
|
if (!(flags.aliases[key] || []).includes(x.join('.'))) {
|
|
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)) {
|
|
const keys = [key].concat(flags.aliases[key] || [])
|
|
keys.forEach(function (key) {
|
|
Object.defineProperty(argvReturn, key, {
|
|
enumerable: true,
|
|
get () {
|
|
return val
|
|
},
|
|
set (value) {
|
|
val = typeof value === 'string' ? path.normalize(value) : value
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
|
|
let value = Array.isArray(val)
|
|
? val.map(function (v) { return maybeCoerceNumber(key, v) })
|
|
: 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.bools) && !Array.isArray(value)) {
|
|
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) {
|
|
const configLookup = Object.create(null)
|
|
|
|
// 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) {
|
|
const configPath = argv[configKey] || configLookup[configKey]
|
|
if (configPath) {
|
|
try {
|
|
let config = null
|
|
const 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) {
|
|
const value = config[key]
|
|
const 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('.')) || (checkAllAliases(fullKey, flags.arrays) && 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
|
|
|
|
const 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
|
|
const 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)) {
|
|
setArg(keys.join('.'), process.env[envVar])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function applyCoercions (argv) {
|
|
let coerce
|
|
const applied = new Set()
|
|
Object.keys(argv).forEach(function (key) {
|
|
if (!applied.has(key)) { // If we haven't already coerced this option via one of its aliases
|
|
coerce = checkAllAliases(key, flags.coercions)
|
|
if (typeof coerce === 'function') {
|
|
try {
|
|
const value = maybeCoerceNumber(key, coerce(argv[key]))
|
|
;([].concat(flags.aliases[key] || [], key)).forEach(ali => {
|
|
applied.add(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, canLog = false) {
|
|
Object.keys(defaults).forEach(function (key) {
|
|
if (!hasKey(obj, key.split('.'))) {
|
|
setKey(obj, key.split('.'), defaults[key])
|
|
if (canLog) defaulted[key] = true
|
|
|
|
;(aliases[key] || []).forEach(function (x) {
|
|
if (hasKey(obj, x.split('.'))) return
|
|
setKey(obj, x.split('.'), defaults[key])
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
function hasKey (obj, keys) {
|
|
let o = obj
|
|
|
|
if (!configuration['dot-notation']) keys = [keys.join('.')]
|
|
|
|
keys.slice(0, -1).forEach(function (key) {
|
|
o = (o[key] || {})
|
|
})
|
|
|
|
const key = keys[keys.length - 1]
|
|
|
|
if (typeof o !== 'object') return false
|
|
else return key in o
|
|
}
|
|
|
|
function setKey (obj, keys, value) {
|
|
let o = obj
|
|
|
|
if (!configuration['dot-notation']) keys = [keys.join('.')]
|
|
|
|
keys.slice(0, -1).forEach(function (key, index) {
|
|
// TODO(bcoe): in the next major version of yargs, switch to
|
|
// Object.create(null) for dot notation:
|
|
key = sanitizeKey(key)
|
|
|
|
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]
|
|
}
|
|
})
|
|
|
|
// TODO(bcoe): in the next major version of yargs, switch to
|
|
// Object.create(null) for dot notation:
|
|
const key = sanitizeKey(keys[keys.length - 1])
|
|
|
|
const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays)
|
|
const isValueArray = Array.isArray(value)
|
|
let 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.counts) ||
|
|
checkAllAliases(key, flags.bools)
|
|
)) {
|
|
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']) {
|
|
const 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']) {
|
|
const 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
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
// return the 1st set flag for any of a key's aliases (or false if no flag set)
|
|
function checkAllAliases (key, flag) {
|
|
const toCheck = [].concat(flags.aliases[key] || [], key)
|
|
const keys = Object.keys(flag)
|
|
const setAlias = toCheck.find(key => keys.includes(key))
|
|
return setAlias ? flag[setAlias] : false
|
|
}
|
|
|
|
function hasAnyFlag (key) {
|
|
const toCheck = [].concat(Object.keys(flags).map(k => flags[k]))
|
|
return toCheck.some(function (flag) {
|
|
return Array.isArray(flag) ? flag.includes(key) : flag[key]
|
|
})
|
|
}
|
|
|
|
function hasFlagsMatching (arg, ...patterns) {
|
|
const toCheck = [].concat(...patterns)
|
|
return toCheck.some(function (pattern) {
|
|
const match = arg.match(pattern)
|
|
return match && hasAnyFlag(match[1])
|
|
})
|
|
}
|
|
|
|
// based on a simplified version of the short flag group parsing logic
|
|
function hasAllShortFlags (arg) {
|
|
// if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group
|
|
if (arg.match(negative) || !arg.match(/^-[^-]+/)) { return false }
|
|
let hasAllFlags = true
|
|
let next
|
|
const letters = arg.slice(1).split('')
|
|
for (let j = 0; j < letters.length; j++) {
|
|
next = arg.slice(j + 2)
|
|
|
|
if (!hasAnyFlag(letters[j])) {
|
|
hasAllFlags = false
|
|
break
|
|
}
|
|
|
|
if ((letters[j + 1] && letters[j + 1] === '=') ||
|
|
next === '-' ||
|
|
(/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) ||
|
|
(letters[j + 1] && letters[j + 1].match(/\W/))) {
|
|
break
|
|
}
|
|
}
|
|
return hasAllFlags
|
|
}
|
|
|
|
function isUnknownOptionAsArg (arg) {
|
|
return configuration['unknown-options-as-args'] && isUnknownOption(arg)
|
|
}
|
|
|
|
function isUnknownOption (arg) {
|
|
// ignore negative numbers
|
|
if (arg.match(negative)) { return false }
|
|
// if this is a short option group and all of them are configured, it isn't unknown
|
|
if (hasAllShortFlags(arg)) { return false }
|
|
// e.g. '--count=2'
|
|
const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/
|
|
// e.g. '-a' or '--arg'
|
|
const normalFlag = /^-+([^=]+?)$/
|
|
// e.g. '-a-'
|
|
const flagEndingInHyphen = /^-+([^=]+?)-$/
|
|
// e.g. '-abc123'
|
|
const flagEndingInDigits = /^-+([^=]+?\d+)$/
|
|
// e.g. '-a/usr/local'
|
|
const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/
|
|
// check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method
|
|
return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters)
|
|
}
|
|
|
|
// 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) {
|
|
const def = {
|
|
boolean: true,
|
|
string: '',
|
|
number: undefined,
|
|
array: []
|
|
}
|
|
|
|
return def[type]
|
|
}
|
|
|
|
// given a flag, enforce a default type.
|
|
function guessType (key) {
|
|
let type = 'boolean'
|
|
if (checkAllAliases(key, flags.strings)) type = 'string'
|
|
else if (checkAllAliases(key, flags.numbers)) type = 'number'
|
|
else if (checkAllAliases(key, flags.bools)) type = 'boolean'
|
|
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
|
|
}
|
|
|
|
// check user configuration settings for inconsistencies
|
|
function checkConfiguration () {
|
|
// count keys should not be set as array/narg
|
|
Object.keys(flags.counts).find(key => {
|
|
if (checkAllAliases(key, flags.arrays)) {
|
|
error = Error(__('Invalid configuration: %s, opts.count excludes opts.array.', key))
|
|
return true
|
|
} else if (checkAllAliases(key, flags.nargs)) {
|
|
error = Error(__('Invalid configuration: %s, opts.count excludes opts.narg.', key))
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
return {
|
|
argv: Object.assign(argvReturn, argv),
|
|
error: error,
|
|
aliases: Object.assign({}, flags.aliases),
|
|
newAliases: Object.assign({}, newAliases),
|
|
defaulted: Object.assign({}, defaulted),
|
|
configuration: configuration
|
|
}
|
|
}
|
|
|
|
// if any aliases reference each other, we should
|
|
// merge them together.
|
|
function combineAliases (aliases) {
|
|
const aliasArrays = []
|
|
const combined = Object.create(null)
|
|
let change = true
|
|
|
|
// 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 (let i = 0; i < aliasArrays.length; i++) {
|
|
for (let ii = i + 1; ii < aliasArrays.length; ii++) {
|
|
const 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) {
|
|
const 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)
|
|
}
|
|
|
|
// TODO(bcoe): in the next major version of yargs, switch to
|
|
// Object.create(null) for dot notation:
|
|
function sanitizeKey (key) {
|
|
if (key === '__proto__') return '___proto___'
|
|
return key
|
|
}
|
|
|
|
module.exports = Parser
|