var path = require('path'); var wordwrap = require('wordwrap'); /* Hack an instance of Argv with process.argv into Argv so people can do require('optimist')(['--beeble=1','-z','zizzle']).argv to parse a list of args and require('optimist').argv to get a parsed version of process.argv. */ var inst = Argv(process.argv.slice(2)); Object.keys(inst).forEach(function (key) { Argv[key] = typeof inst[key] == 'function' ? inst[key].bind(inst) : inst[key]; }); var exports = module.exports = Argv; function Argv (args, cwd) { var self = {}; if (!cwd) cwd = process.cwd(); self.$0 = process.argv .slice(0,2) .map(function (x) { var b = rebase(cwd, x); return x.match(/^\//) && b.length < x.length ? b : x }) .join(' ') ; if (process.env._ != undefined && process.argv[1] == process.env._) { self.$0 = process.env._.replace( path.dirname(process.execPath) + '/', '' ); } var flags = { bools : {}, strings : {} }; self.boolean = function (bools) { if (!Array.isArray(bools)) { bools = [].slice.call(arguments); } bools.forEach(function (name) { flags.bools[name] = true; }); return self; }; self.string = function (strings) { if (!Array.isArray(strings)) { strings = [].slice.call(arguments); } strings.forEach(function (name) { flags.strings[name] = true; }); return self; }; var aliases = {}; self.alias = function (x, y) { if (typeof x === 'object') { Object.keys(x).forEach(function (key) { self.alias(key, x[key]); }); } else if (Array.isArray(y)) { y.forEach(function (yy) { self.alias(x, yy); }); } else { var zs = (aliases[x] || []).concat(aliases[y] || []).concat(x, y); aliases[x] = zs.filter(function (z) { return z != x }); aliases[y] = zs.filter(function (z) { return z != y }); } return self; }; var demanded = {}; self.demand = function (keys) { if (typeof keys == 'number') { if (!demanded._) demanded._ = 0; demanded._ += keys; } else if (Array.isArray(keys)) { keys.forEach(function (key) { self.demand(key); }); } else { demanded[keys] = true; } return self; }; var usage; self.usage = function (msg, opts) { if (!opts && typeof msg === 'object') { opts = msg; msg = null; } usage = msg; if (opts) self.options(opts); return self; }; function fail (msg) { self.showHelp(); if (msg) console.error(msg); process.exit(1); } var checks = []; self.check = function (f) { checks.push(f); return self; }; var defaults = {}; self.default = function (key, value) { if (typeof key === 'object') { Object.keys(key).forEach(function (k) { self.default(k, key[k]); }); } else { defaults[key] = value; } return self; }; var descriptions = {}; self.describe = function (key, desc) { if (typeof key === 'object') { Object.keys(key).forEach(function (k) { self.describe(k, key[k]); }); } else { descriptions[key] = desc; } return self; }; self.parse = function (args) { return Argv(args).argv; }; self.option = self.options = function (key, opt) { if (typeof key === 'object') { Object.keys(key).forEach(function (k) { self.options(k, key[k]); }); } else { if (opt.alias) self.alias(key, opt.alias); if (opt.demand) self.demand(key); if (typeof opt.default !== 'undefined') { self.default(key, opt.default); } if (opt.boolean || opt.type === 'boolean') { self.boolean(key); } if (opt.string || opt.type === 'string') { self.string(key); } var desc = opt.describe || opt.description || opt.desc; if (desc) { self.describe(key, desc); } } return self; }; var wrap = null; self.wrap = function (cols) { wrap = cols; return self; }; self.showHelp = function (fn) { if (!fn) fn = console.error; fn(self.help()); }; self.help = function () { var keys = Object.keys( Object.keys(descriptions) .concat(Object.keys(demanded)) .concat(Object.keys(defaults)) .reduce(function (acc, key) { if (key !== '_') acc[key] = true; return acc; }, {}) ); var help = keys.length ? [ 'Options:' ] : []; if (usage) { help.unshift(usage.replace(/\$0/g, self.$0), ''); } var switches = keys.reduce(function (acc, key) { acc[key] = [ key ].concat(aliases[key] || []) .map(function (sw) { return (sw.length > 1 ? '--' : '-') + sw }) .join(', ') ; return acc; }, {}); var switchlen = longest(Object.keys(switches).map(function (s) { return switches[s] || ''; })); var desclen = longest(Object.keys(descriptions).map(function (d) { return descriptions[d] || ''; })); keys.forEach(function (key) { var kswitch = switches[key]; var desc = descriptions[key] || ''; if (wrap) { desc = wordwrap(switchlen + 4, wrap)(desc) .slice(switchlen + 4) ; } var spadding = new Array( Math.max(switchlen - kswitch.length + 3, 0) ).join(' '); var dpadding = new Array( Math.max(desclen - desc.length + 1, 0) ).join(' '); var type = null; if (flags.bools[key]) type = '[boolean]'; if (flags.strings[key]) type = '[string]'; if (!wrap && dpadding.length > 0) { desc += dpadding; } var prelude = ' ' + kswitch + spadding; var extra = [ type, demanded[key] ? '[required]' : null , defaults[key] !== undefined ? '[default: ' + JSON.stringify(defaults[key]) + ']' : null , ].filter(Boolean).join(' '); var body = [ desc, extra ].filter(Boolean).join(' '); if (wrap) { var dlines = desc.split('\n'); var dlen = dlines.slice(-1)[0].length + (dlines.length === 1 ? prelude.length : 0) body = desc + (dlen + extra.length > wrap - 2 ? '\n' + new Array(wrap - extra.length + 1).join(' ') + extra : new Array(wrap - extra.length - dlen + 1).join(' ') + extra ); } help.push(prelude + body); }); help.push(''); return help.join('\n'); }; Object.defineProperty(self, 'argv', { get : parseArgs, enumerable : true, }); function parseArgs () { var argv = { _ : [], $0 : self.$0 }; Object.keys(flags.bools).forEach(function (key) { setArg(key, defaults[key] || false); }); function setArg (key, val) { var num = Number(val); var value = typeof val !== 'string' || isNaN(num) ? val : num; if (flags.strings[key]) value = val; setKey(argv, key.split('.'), value); (aliases[key] || []).forEach(function (x) { argv[x] = argv[key]; }); } for (var i = 0; i < args.length; i++) { var arg = args[i]; if (arg === '--') { argv._.push.apply(argv._, args.slice(i + 1)); break; } else 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); } else if (arg.match(/^--.+/)) { var key = arg.match(/^--(.+)/)[1]; var next = args[i + 1]; if (next !== undefined && !next.match(/^-/) && !flags.bools[key] && (aliases[key] ? !flags.bools[aliases[key]] : true)) { setArg(key, next); i++; } else if (/^(true|false)$/.test(next)) { setArg(key, next === 'true'); i++; } else { setArg(key, true); } } else if (arg.match(/^-[^-]+/)) { var letters = arg.slice(1,-1).split(''); var broken = false; for (var j = 0; j < letters.length; j++) { if (letters[j+1] && letters[j+1].match(/\W/)) { setArg(letters[j], arg.slice(j+2)); broken = true; break; } else { setArg(letters[j], true); } } if (!broken) { var key = arg.slice(-1)[0]; if (args[i+1] && !args[i+1].match(/^-/) && !flags.bools[key] && (aliases[key] ? !flags.bools[aliases[key]] : true)) { setArg(key, args[i+1]); i++; } else if (args[i+1] && /true|false/.test(args[i+1])) { setArg(key, args[i+1] === 'true'); i++; } else { setArg(key, true); } } } else { var n = Number(arg); argv._.push(flags.strings['_'] || isNaN(n) ? arg : n); } } Object.keys(defaults).forEach(function (key) { if (!(key in argv)) { argv[key] = defaults[key]; if (key in aliases) { argv[aliases[key]] = defaults[key]; } } }); if (demanded._ && argv._.length < demanded._) { fail('Not enough non-option arguments: got ' + argv._.length + ', need at least ' + demanded._ ); } var missing = []; Object.keys(demanded).forEach(function (key) { if (!argv[key]) missing.push(key); }); if (missing.length) { fail('Missing required arguments: ' + missing.join(', ')); } checks.forEach(function (f) { try { if (f(argv) === false) { fail('Argument check failed: ' + f.toString()); } } catch (err) { fail(err) } }); return argv; } function longest (xs) { return Math.max.apply( null, xs.map(function (x) { return x.length }) ); } return self; }; // rebase an absolute path to a relative one with respect to a base directory // exported for tests exports.rebase = rebase; function rebase (base, dir) { var ds = path.normalize(dir).split('/').slice(1); var bs = path.normalize(base).split('/').slice(1); for (var i = 0; ds[i] && ds[i] == bs[i]; i++); ds.splice(0, i); bs.splice(0, i); var p = path.normalize( bs.map(function () { return '..' }).concat(ds).join('/') ).replace(/\/$/,'').replace(/^$/, '.'); return p.match(/^[.\/]/) ? p : './' + p; }; 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 (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 ]; } }