index.js (13900B)
1 var path = require('path'); 2 var wordwrap = require('wordwrap'); 3 4 /* Hack an instance of Argv with process.argv into Argv 5 so people can do 6 require('optimist')(['--beeble=1','-z','zizzle']).argv 7 to parse a list of args and 8 require('optimist').argv 9 to get a parsed version of process.argv. 10 */ 11 12 var inst = Argv(process.argv.slice(2)); 13 Object.keys(inst).forEach(function (key) { 14 Argv[key] = typeof inst[key] == 'function' 15 ? inst[key].bind(inst) 16 : inst[key]; 17 }); 18 19 var exports = module.exports = Argv; 20 function Argv (args, cwd) { 21 var self = {}; 22 if (!cwd) cwd = process.cwd(); 23 24 self.$0 = process.argv 25 .slice(0,2) 26 .map(function (x) { 27 var b = rebase(cwd, x); 28 return x.match(/^\//) && b.length < x.length 29 ? b : x 30 }) 31 .join(' ') 32 ; 33 34 if (process.env._ != undefined && process.argv[1] == process.env._) { 35 self.$0 = process.env._.replace( 36 path.dirname(process.execPath) + '/', '' 37 ); 38 } 39 40 var flags = { bools : {}, strings : {} }; 41 42 self.boolean = function (bools) { 43 if (!Array.isArray(bools)) { 44 bools = [].slice.call(arguments); 45 } 46 47 bools.forEach(function (name) { 48 flags.bools[name] = true; 49 }); 50 51 return self; 52 }; 53 54 self.string = function (strings) { 55 if (!Array.isArray(strings)) { 56 strings = [].slice.call(arguments); 57 } 58 59 strings.forEach(function (name) { 60 flags.strings[name] = true; 61 }); 62 63 return self; 64 }; 65 66 var aliases = {}; 67 self.alias = function (x, y) { 68 if (typeof x === 'object') { 69 Object.keys(x).forEach(function (key) { 70 self.alias(key, x[key]); 71 }); 72 } 73 else if (Array.isArray(y)) { 74 y.forEach(function (yy) { 75 self.alias(x, yy); 76 }); 77 } 78 else { 79 var zs = (aliases[x] || []).concat(aliases[y] || []).concat(x, y); 80 aliases[x] = zs.filter(function (z) { return z != x }); 81 aliases[y] = zs.filter(function (z) { return z != y }); 82 } 83 84 return self; 85 }; 86 87 var demanded = {}; 88 self.demand = function (keys) { 89 if (typeof keys == 'number') { 90 if (!demanded._) demanded._ = 0; 91 demanded._ += keys; 92 } 93 else if (Array.isArray(keys)) { 94 keys.forEach(function (key) { 95 self.demand(key); 96 }); 97 } 98 else { 99 demanded[keys] = true; 100 } 101 102 return self; 103 }; 104 105 var usage; 106 self.usage = function (msg, opts) { 107 if (!opts && typeof msg === 'object') { 108 opts = msg; 109 msg = null; 110 } 111 112 usage = msg; 113 114 if (opts) self.options(opts); 115 116 return self; 117 }; 118 119 function fail (msg) { 120 self.showHelp(); 121 if (msg) console.error(msg); 122 process.exit(1); 123 } 124 125 var checks = []; 126 self.check = function (f) { 127 checks.push(f); 128 return self; 129 }; 130 131 var defaults = {}; 132 self.default = function (key, value) { 133 if (typeof key === 'object') { 134 Object.keys(key).forEach(function (k) { 135 self.default(k, key[k]); 136 }); 137 } 138 else { 139 defaults[key] = value; 140 } 141 142 return self; 143 }; 144 145 var descriptions = {}; 146 self.describe = function (key, desc) { 147 if (typeof key === 'object') { 148 Object.keys(key).forEach(function (k) { 149 self.describe(k, key[k]); 150 }); 151 } 152 else { 153 descriptions[key] = desc; 154 } 155 return self; 156 }; 157 158 self.parse = function (args) { 159 return Argv(args).argv; 160 }; 161 162 self.option = self.options = function (key, opt) { 163 if (typeof key === 'object') { 164 Object.keys(key).forEach(function (k) { 165 self.options(k, key[k]); 166 }); 167 } 168 else { 169 if (opt.alias) self.alias(key, opt.alias); 170 if (opt.demand) self.demand(key); 171 if (typeof opt.default !== 'undefined') { 172 self.default(key, opt.default); 173 } 174 175 if (opt.boolean || opt.type === 'boolean') { 176 self.boolean(key); 177 } 178 if (opt.string || opt.type === 'string') { 179 self.string(key); 180 } 181 182 var desc = opt.describe || opt.description || opt.desc; 183 if (desc) { 184 self.describe(key, desc); 185 } 186 } 187 188 return self; 189 }; 190 191 var wrap = null; 192 self.wrap = function (cols) { 193 wrap = cols; 194 return self; 195 }; 196 197 self.showHelp = function (fn) { 198 if (!fn) fn = console.error; 199 fn(self.help()); 200 }; 201 202 self.help = function () { 203 var keys = Object.keys( 204 Object.keys(descriptions) 205 .concat(Object.keys(demanded)) 206 .concat(Object.keys(defaults)) 207 .reduce(function (acc, key) { 208 if (key !== '_') acc[key] = true; 209 return acc; 210 }, {}) 211 ); 212 213 var help = keys.length ? [ 'Options:' ] : []; 214 215 if (usage) { 216 help.unshift(usage.replace(/\$0/g, self.$0), ''); 217 } 218 219 var switches = keys.reduce(function (acc, key) { 220 acc[key] = [ key ].concat(aliases[key] || []) 221 .map(function (sw) { 222 return (sw.length > 1 ? '--' : '-') + sw 223 }) 224 .join(', ') 225 ; 226 return acc; 227 }, {}); 228 229 var switchlen = longest(Object.keys(switches).map(function (s) { 230 return switches[s] || ''; 231 })); 232 233 var desclen = longest(Object.keys(descriptions).map(function (d) { 234 return descriptions[d] || ''; 235 })); 236 237 keys.forEach(function (key) { 238 var kswitch = switches[key]; 239 var desc = descriptions[key] || ''; 240 241 if (wrap) { 242 desc = wordwrap(switchlen + 4, wrap)(desc) 243 .slice(switchlen + 4) 244 ; 245 } 246 247 var spadding = new Array( 248 Math.max(switchlen - kswitch.length + 3, 0) 249 ).join(' '); 250 251 var dpadding = new Array( 252 Math.max(desclen - desc.length + 1, 0) 253 ).join(' '); 254 255 var type = null; 256 257 if (flags.bools[key]) type = '[boolean]'; 258 if (flags.strings[key]) type = '[string]'; 259 260 if (!wrap && dpadding.length > 0) { 261 desc += dpadding; 262 } 263 264 var prelude = ' ' + kswitch + spadding; 265 var extra = [ 266 type, 267 demanded[key] 268 ? '[required]' 269 : null 270 , 271 defaults[key] !== undefined 272 ? '[default: ' + JSON.stringify(defaults[key]) + ']' 273 : null 274 , 275 ].filter(Boolean).join(' '); 276 277 var body = [ desc, extra ].filter(Boolean).join(' '); 278 279 if (wrap) { 280 var dlines = desc.split('\n'); 281 var dlen = dlines.slice(-1)[0].length 282 + (dlines.length === 1 ? prelude.length : 0) 283 284 body = desc + (dlen + extra.length > wrap - 2 285 ? '\n' 286 + new Array(wrap - extra.length + 1).join(' ') 287 + extra 288 : new Array(wrap - extra.length - dlen + 1).join(' ') 289 + extra 290 ); 291 } 292 293 help.push(prelude + body); 294 }); 295 296 help.push(''); 297 return help.join('\n'); 298 }; 299 300 Object.defineProperty(self, 'argv', { 301 get : parseArgs, 302 enumerable : true, 303 }); 304 305 function parseArgs () { 306 var argv = { _ : [], $0 : self.$0 }; 307 Object.keys(flags.bools).forEach(function (key) { 308 setArg(key, defaults[key] || false); 309 }); 310 311 function setArg (key, val) { 312 var num = Number(val); 313 var value = typeof val !== 'string' || isNaN(num) ? val : num; 314 if (flags.strings[key]) value = val; 315 316 setKey(argv, key.split('.'), value); 317 318 (aliases[key] || []).forEach(function (x) { 319 argv[x] = argv[key]; 320 }); 321 } 322 323 for (var i = 0; i < args.length; i++) { 324 var arg = args[i]; 325 326 if (arg === '--') { 327 argv._.push.apply(argv._, args.slice(i + 1)); 328 break; 329 } 330 else if (arg.match(/^--.+=/)) { 331 // Using [\s\S] instead of . because js doesn't support the 332 // 'dotall' regex modifier. See: 333 // http://stackoverflow.com/a/1068308/13216 334 var m = arg.match(/^--([^=]+)=([\s\S]*)$/); 335 setArg(m[1], m[2]); 336 } 337 else if (arg.match(/^--no-.+/)) { 338 var key = arg.match(/^--no-(.+)/)[1]; 339 setArg(key, false); 340 } 341 else if (arg.match(/^--.+/)) { 342 var key = arg.match(/^--(.+)/)[1]; 343 var next = args[i + 1]; 344 if (next !== undefined && !next.match(/^-/) 345 && !flags.bools[key] 346 && (aliases[key] ? !flags.bools[aliases[key]] : true)) { 347 setArg(key, next); 348 i++; 349 } 350 else if (/^(true|false)$/.test(next)) { 351 setArg(key, next === 'true'); 352 i++; 353 } 354 else { 355 setArg(key, true); 356 } 357 } 358 else if (arg.match(/^-[^-]+/)) { 359 var letters = arg.slice(1,-1).split(''); 360 361 var broken = false; 362 for (var j = 0; j < letters.length; j++) { 363 if (letters[j+1] && letters[j+1].match(/\W/)) { 364 setArg(letters[j], arg.slice(j+2)); 365 broken = true; 366 break; 367 } 368 else { 369 setArg(letters[j], true); 370 } 371 } 372 373 if (!broken) { 374 var key = arg.slice(-1)[0]; 375 376 if (args[i+1] && !args[i+1].match(/^-/) 377 && !flags.bools[key] 378 && (aliases[key] ? !flags.bools[aliases[key]] : true)) { 379 setArg(key, args[i+1]); 380 i++; 381 } 382 else if (args[i+1] && /true|false/.test(args[i+1])) { 383 setArg(key, args[i+1] === 'true'); 384 i++; 385 } 386 else { 387 setArg(key, true); 388 } 389 } 390 } 391 else { 392 var n = Number(arg); 393 argv._.push(flags.strings['_'] || isNaN(n) ? arg : n); 394 } 395 } 396 397 Object.keys(defaults).forEach(function (key) { 398 if (!(key in argv)) { 399 argv[key] = defaults[key]; 400 if (key in aliases) { 401 argv[aliases[key]] = defaults[key]; 402 } 403 } 404 }); 405 406 if (demanded._ && argv._.length < demanded._) { 407 fail('Not enough non-option arguments: got ' 408 + argv._.length + ', need at least ' + demanded._ 409 ); 410 } 411 412 var missing = []; 413 Object.keys(demanded).forEach(function (key) { 414 if (!argv[key]) missing.push(key); 415 }); 416 417 if (missing.length) { 418 fail('Missing required arguments: ' + missing.join(', ')); 419 } 420 421 checks.forEach(function (f) { 422 try { 423 if (f(argv) === false) { 424 fail('Argument check failed: ' + f.toString()); 425 } 426 } 427 catch (err) { 428 fail(err) 429 } 430 }); 431 432 return argv; 433 } 434 435 function longest (xs) { 436 return Math.max.apply( 437 null, 438 xs.map(function (x) { return x.length }) 439 ); 440 } 441 442 return self; 443 }; 444 445 // rebase an absolute path to a relative one with respect to a base directory 446 // exported for tests 447 exports.rebase = rebase; 448 function rebase (base, dir) { 449 var ds = path.normalize(dir).split('/').slice(1); 450 var bs = path.normalize(base).split('/').slice(1); 451 452 for (var i = 0; ds[i] && ds[i] == bs[i]; i++); 453 ds.splice(0, i); bs.splice(0, i); 454 455 var p = path.normalize( 456 bs.map(function () { return '..' }).concat(ds).join('/') 457 ).replace(/\/$/,'').replace(/^$/, '.'); 458 return p.match(/^[.\/]/) ? p : './' + p; 459 }; 460 461 function setKey (obj, keys, value) { 462 var o = obj; 463 keys.slice(0,-1).forEach(function (key) { 464 if (o[key] === undefined) o[key] = {}; 465 o = o[key]; 466 }); 467 468 var key = keys[keys.length - 1]; 469 if (o[key] === undefined || typeof o[key] === 'boolean') { 470 o[key] = value; 471 } 472 else if (Array.isArray(o[key])) { 473 o[key].push(value); 474 } 475 else { 476 o[key] = [ o[key], value ]; 477 } 478 }