// fancy-pants parsing of argv, originally forked // from minimist: https://www.npmjs.com/package/minimist var camelCase = require('camelcase'), fs = require('fs'), path = require('path'); 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]) { var c = camelCase(key); aliases[key] = aliases[key] || []; // don't allow the same key to be added multiple times. if (aliases[key].indexOf(c) === -1) { aliases[key] = (aliases[key] || []).concat(c); newAliases[c] = true; } } (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]; // -- 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 var m = arg.match(/^--([^=]+)=([\s\S]*)$/); setArg(m[1], m[2]); } else if (arg.match(/^--no-.+/)) { var key = arg.match(/^--no-(.+)/)[1]; setArg(key, false); } // -- seperated by space. else if (arg.match(/^--.+/)) { var key = arg.match(/^--(.+)/)[1]; if (checkAllAliases(key, opts.narg)) { i = eatNargs(i, key, args); } else { var 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(/^-.\..+=/)) { var m = arg.match(/^-([^=]+)=([\s\S]*)$/); setArg(m[1], m[2]); } // dot-notation flag seperated by space. else if (arg.match(/^-.\..+/)) { var key = arg.match(/^-(.\..+)/)[1]; var next = args[i + 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(/^-[^-]+/)) { var letters = arg.slice(1,-1).split(''); var broken = false; for (var j = 0; j < letters.length; j++) { var next = arg.slice(j+2); if (letters[j+1] && letters[j+1] === '=') { setArg(letters[j], arg.slice(j+3)); 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))); } } var key = arg.slice(-1)[0]; if (!broken && key !== '-') { if (checkAllAliases(key, opts.narg)) { i = eatNargs(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); } 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 = function(orig) { return orig !== undefined ? orig + 1 : 0; }; } 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 = JSON.parse(fs.readFileSync(configPath, 'utf8')); 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 (typeof value === 'function') { o[key] = value(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 }; };