| // fancy-pants parsing of argv, originally forked |
| // from minimist: https://www.npmjs.com/package/minimist |
| var camelCase = require('camelcase'), |
| path = require('path') |
| |
| function increment (orig) { |
| return orig !== undefined ? orig + 1 : 0 |
| } |
| |
| module.exports = function (args, opts) { |
| if (!opts) opts = {} |
| var flags = { arrays: {}, bools: {}, strings: {}, counts: {}, normalize: {}, configs: {} } |
| |
| ;[].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.count).filter(Boolean).forEach(function (key) { |
| flags.counts[key] = true |
| }) |
| |
| ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) { |
| flags.normalize[key] = true |
| }) |
| |
| ;[].concat(opts.config).filter(Boolean).forEach(function (key) { |
| flags.configs[key] = true |
| }) |
| |
| var aliases = {}, |
| newAliases = {} |
| |
| extendAliases(opts.key) |
| extendAliases(opts.alias) |
| |
| var defaults = opts['default'] || {} |
| Object.keys(defaults).forEach(function (key) { |
| if (/-/.test(key) && !opts.alias[key]) { |
| aliases[key] = aliases[key] || [] |
| } |
| (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]) |
| }) |
| |
| 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], |
| broken, |
| key, |
| letters, |
| m, |
| next, |
| value |
| |
| // -- seperated by = |
| if (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], opts.narg)) { |
| args.splice(i + 1, m[1], 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, m[1], m[2]) |
| i = eatArray(i, m[1], args) |
| } else { |
| setArg(m[1], m[2]) |
| } |
| } else if (arg.match(/^--no-.+/)) { |
| key = arg.match(/^--no-(.+)/)[1] |
| setArg(key, false) |
| |
| // -- seperated by space. |
| } else if (arg.match(/^--.+/)) { |
| key = arg.match(/^--(.+)/)[1] |
| |
| // nargs format = '--foo a b c' |
| if (checkAllAliases(key, opts.narg)) { |
| 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(letters[j], opts.narg)) { |
| 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], arg.slice(j + 2)) |
| 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, opts.narg)) { |
| 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 { |
| if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) |
| && !checkAllAliases(key, flags.bools) |
| && !checkAllAliases(key, flags.counts)) { |
| setArg(key, args[i + 1]) |
| i++ |
| } else if (args[i + 1] && /true|false/.test(args[i + 1])) { |
| setArg(key, args[i + 1]) |
| i++ |
| } else { |
| setArg(key, defaultForType(guessType(key, flags))) |
| } |
| } |
| } |
| } else { |
| argv._.push( |
| flags.strings['_'] || !isNumber(arg) ? arg : Number(arg) |
| ) |
| } |
| } |
| |
| setConfig(argv) |
| applyDefaultsAndAliases(argv, aliases, defaults) |
| |
| Object.keys(flags.counts).forEach(function (key) { |
| setArg(key, defaults[key]) |
| }) |
| |
| 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, opts.narg) |
| |
| if (args.length - (i + 1) < toEat) throw Error('not enough arguments following: ' + 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) { |
| for (var ii = i + 1; ii < args.length; ii++) { |
| if (/^-/.test(args[ii])) break |
| i = ii |
| setArg(key, args[ii]) |
| } |
| |
| return i |
| } |
| |
| function setArg (key, val) { |
| // 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) && !(aliases[key] && aliases[key].length)) { |
| var c = camelCase(key) |
| aliases[key] = [c] |
| newAliases[c] = true |
| } |
| |
| var value = !checkAllAliases(key, flags.strings) && isNumber(val) ? Number(val) : val |
| |
| if (checkAllAliases(key, flags.counts)) { |
| value = increment |
| } |
| |
| var splitKey = key.split('.') |
| setKey(argv, splitKey, value) |
| |
| ;(aliases[splitKey[0]] || []).forEach(function (x) { |
| x = x.split('.') |
| |
| // handle populating dot notation for both |
| // the key and its aliases. |
| if (splitKey.length > 1) { |
| var a = [].concat(splitKey) |
| a.shift() // nuke the old key. |
| x = x.concat(a) |
| } |
| |
| setKey(argv, x, value) |
| }) |
| |
| var keys = [key].concat(aliases[key] || []) |
| for (var i = 0, l = keys.length; i < l; i++) { |
| if (flags.normalize[keys[i]]) { |
| 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 |
| }) |
| }) |
| break |
| } |
| } |
| } |
| |
| // 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, aliases, defaults) |
| |
| Object.keys(flags.configs).forEach(function (configKey) { |
| var configPath = argv[configKey] || configLookup[configKey] |
| if (configPath) { |
| try { |
| var config = require(path.resolve(process.cwd(), configPath)) |
| |
| Object.keys(config).forEach(function (key) { |
| // setting arguments via CLI takes precedence over |
| // values within the config file. |
| if (argv[key] === undefined) { |
| delete argv[key] |
| setArg(key, config[key]) |
| } |
| }) |
| } catch (ex) { |
| throw Error('invalid json config file: ' + configPath) |
| } |
| } |
| }) |
| } |
| |
| 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) { |
| setKey(obj, x.split('.'), defaults[key]) |
| }) |
| } |
| }) |
| } |
| |
| function hasKey (obj, keys) { |
| var o = obj |
| keys.slice(0, -1).forEach(function (key) { |
| o = (o[key] || {}) |
| }) |
| |
| var key = keys[keys.length - 1] |
| return key in o |
| } |
| |
| function setKey (obj, keys, value) { |
| var o = obj |
| 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 || typeof o[key] === 'boolean') { |
| 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 (obj) { |
| Object.keys(obj || {}).forEach(function (key) { |
| aliases[key] = [].concat(opts.alias[key] || []) |
| // For "--option-name", also set argv.optionName |
| aliases[key].concat(key).forEach(function (x) { |
| if (/-/.test(x)) { |
| var c = camelCase(x) |
| aliases[key].push(c) |
| newAliases[c] = true |
| } |
| }) |
| aliases[key].forEach(function (x) { |
| aliases[x] = [key].concat(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, |
| toCheck = [].concat(aliases[key] || [], key) |
| |
| toCheck.forEach(function (key) { |
| if (flag[key]) isSet = flag[key] |
| }) |
| |
| return isSet |
| } |
| |
| // 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: '', |
| 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.arrays && flags.arrays[key]) type = 'array' |
| |
| return type |
| } |
| |
| function isNumber (x) { |
| if (typeof x === 'number') return true |
| if (/^0x[0-9a-f]+$/i.test(x)) return true |
| return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x) |
| } |
| |
| return { |
| argv: argv, |
| aliases: aliases, |
| newAliases: newAliases |
| } |
| } |