var assign = require('lodash.assign') var camelCase = require('camelcase') 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 = assign({}, { 'short-option-groups': true, 'camel-case-expansion': true, 'dot-notation': true, 'parse-numbers': true, 'boolean-negation': true }, opts.configuration) var defaults = opts.default || {} var configObjects = opts.configObjects || [] var envPrefix = opts.envPrefix var newAliases = {} // allow a i18n handler to be passed in, default to a fake one (util.format). var __ = opts.__ || function (str) { return util.format.apply(util, Array.prototype.slice.call(arguments)) } var error = null var flags = { aliases: {}, arrays: {}, bools: {}, strings: {}, numbers: {}, counts: {}, normalize: {}, configs: {}, defaulted: {}, nargs: {} } ;[].concat(opts.array).filter(Boolean).forEach(function (key) { flags.arrays[key] = true }) ;[].concat(opts.boolean).filter(Boolean).forEach(function (key) { flags.bools[key] = true }) ;[].concat(opts.string).filter(Boolean).forEach(function (key) { flags.strings[key] = true }) ;[].concat(opts.number).filter(Boolean).forEach(function (key) { flags.numbers[key] = true }) ;[].concat(opts.count).filter(Boolean).forEach(function (key) { flags.counts[key] = true }) ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) { flags.normalize[key] = true }) Object.keys(opts.narg || {}).forEach(function (k) { flags.nargs[k] = opts.narg[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) { setArg(key, !(key in defaults) ? false : defaults[key]) setDefaulted(key) }) var notFlags = [] if (args.indexOf('--') !== -1) { notFlags = args.slice(args.indexOf('--') + 1) args = args.slice(0, args.indexOf('--')) } for (var i = 0; i < args.length; i++) { var arg = args[i] var broken var key var letters var m var next var value // -- seperated 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(/^--no-.+/) && configuration['boolean-negation']) { key = arg.match(/^--no-(.+)/)[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 = args[i + 1] if (next !== undefined && !next.match(/^-/) && !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, defaultForType(guessType(key, flags))) } } // 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, defaultForType(guessType(key, flags))) } } else if (arg.match(/^-[^-]+/)) { 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 } 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], defaultForType(guessType(letters[j], flags))) } } 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) && !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, defaultForType(guessType(key, flags))) } } } } else { argv._.push( flags.strings['_'] || !isNumber(arg) ? arg : Number(arg) ) } } // order of precedence: // 1. command line arg // 2. value from config file // 3. value from config objects // 4. value from env var // 5. configured default value applyEnvVars(argv, true) // special case: check env vars that point to config file setConfig(argv) setConfigObjects() applyEnvVars(argv, false) applyDefaultsAndAliases(argv, flags.aliases, defaults) // 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) }) notFlags.forEach(function (key) { argv._.push(key) }) // how many arguments should we consume, based // on the nargs option? function eatNargs (i, key, args) { var toEat = checkAllAliases(key, flags.nargs) if (args.length - (i + 1) < toEat) error = Error(__('Not enough arguments following: %s', key)) for (var ii = i + 1; ii < (toEat + i + 1); ii++) { setArg(key, args[ii]) } return (i + toEat) } // 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 for (var ii = i + 1; ii < args.length; ii++) { if (/^-/.test(args[ii])) { if (ii === start) { setArg(key, defaultForType('array')) } break } i = ii setArg(key, args[ii]) } return i } function setArg (key, val) { unsetDefaulted(key) // handle parsing boolean arguments --foo=true --bar false. if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { if (typeof val === 'string') val = val === 'true' } if (/-/.test(key) && !(flags.aliases[key] && flags.aliases[key].length) && configuration['camel-case-expansion']) { var c = camelCase(key) flags.aliases[key] = [c] newAliases[c] = true } var value = val if (!checkAllAliases(key, flags.strings)) { if (isNumber(val)) value = Number(val) if (!isUndefined(val) && !isNumber(val) && checkAllAliases(key, flags.numbers)) value = NaN } // 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)) { value = path.normalize(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 }) }) } } // 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 (Object.prototype.toString.call(value) === '[object Object]') { // 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])) { 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 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) { if (o[key] === undefined) o[key] = {} o = o[key] }) var key = keys[keys.length - 1] if (value === increment) { o[key] = increment(o[key]) } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) { o[key] = Array.isArray(value) ? value : [value] } else if (o[key] === undefined || checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { o[key] = value } else if (Array.isArray(o[key])) { o[key].push(value) } else { o[key] = [ o[key], value ] } } // extend the aliases list with inferred aliases. function extendAliases () { Array.prototype.slice.call(arguments).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) 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] }) } // 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, flags) { var type = 'boolean' if (flags.strings && flags.strings[key]) type = 'string' else if (flags.numbers && flags.numbers[key]) type = 'number' else if (flags.arrays && flags.arrays[key]) type = 'array' return type } function isNumber (x) { if (!configuration['parse-numbers']) return false if (typeof x === 'number') return true if (/^0x[0-9a-f]+$/i.test(x)) return true 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