var fs = require('fs'); var path = require('path'); var nopt = require('nopt'); var util = require('./util'); var events = require('events'); var collectable = require('./actions/collectable'); var commandable = require('./actions/commandable'); module.exports = noptify; noptify.Noptify = Noptify; // noptify is a little wrapper around `nopt` module adding a more expressive, // commander-like, API and few helpers. // // Examples // // var program = noptify(process.argv, { program: 'name' }) // .version('0.0.1') // .option('port', '-p', 'Port to listen on (default: 35729)', Number) // .option('pid', 'Path to the generated PID file', String) // // var opts = program.parse(); // // Returns an instance of `Noptify` function noptify(args, options) { return new Noptify(args, options); } // Noptify provides the API to parse out option, shorthands and generate the // proper generic help output. // // - args - The Array of arguments to parse (default: `process.argv`); // - options - An hash of options with the following properties // - program - The program name to use in usage output // // Every noptify instance is created with two options, `-h, --help` and `-v, // --version`. function Noptify(args, options) { events.EventEmitter.call(this); options = this.options = options || {}; this.args = args || process.argv; this._program = options.program || (path.basename(this.args[this.args[0] === 'node' ? 1 : 0])); this._shorthands = {}; this._commands = {}; this._routes = []; this._steps = []; this.nopt = {}; this.option('help', '-h', 'Show help usage'); this.option('version', '-v', 'Show package version'); } util.inherits(Noptify, events.EventEmitter); // Inherits from each actions' mixins // // XXX: consider making it optional? with a `.use()` method? util.extend(Noptify.prototype, collectable); util.extend(Noptify.prototype, commandable); // Parse the provided options and shorthands, pass them through `nopt` and // return the result. // // When `opts.help` is set, the help output is displayed and `help` // event is emitted. The process exists with `0` status, the help output is // automatically displayed and the `help` event is emitted. // // Examples // // var program = noptify(['foo', '--help']) // .on('help', function() { // console.log('Examples'); // console.log(''); // console.log(' foo bar --baz > foo.txt'); // }); // // var opts = program.parse(); // // ... Help output ... // // ... Custom help output ... // // ... Exit ... // // Noptify.prototype.parse = function parse(argv, position) { argv = argv || this.args; var options = this._options.reduce(function(opts, opt) { opts[opt.name] = opt.type; return opts; }, {}); this._options.forEach(function(opt) { if(!opt.shorthand) return; this.shorthand(opt.shorthand, '--' + opt.name); }, this); var opts = nopt(options, this._shorthands, argv, position); if(opts.version) { console.log(this._version); process.exit(0); } var registered = this.registered(opts.argv.remain); if(opts.help) { if(registered && registered.help) { registered.help().emit('help'); } else { this.help().emit('help'); } process.exit(0); } this.nopt = opts; // check remaining args and registered command, for each match, remove the // argument from remaining args and trigger the handler, ideally the handler // can be another subprogram, for now simple functions if(this.routeCommand) process.nextTick(this.routeCommand.bind(this)); if(this.stdin && this._readFromStdin) process.nextTick(this.stdin.bind(this)); return opts; }; // Define the program version. Noptify.prototype.version = function version(ver) { this._version = ver; return this; }; // Define the program property. Noptify.prototype.program = function program(value) { this._program = value || ''; return this; }; // Define `name` option with optional shorthands, optional description and optional type. Noptify.prototype.option = function option(name, shorthand, description, type) { this._options = this._options || []; if(!description) { description = shorthand; shorthand = ''; } if(!type) { if(typeof description === 'function') { type = description; description = shorthand; shorthand = ''; } else { type = String; } } shorthand = shorthand.replace(/^-*/, ''), this._options.push({ name: name, shorthand: shorthand.replace(/^-*/, ''), description: description || (name + ': ' + type.name), usage: (shorthand ? '-' + shorthand + ', ': '' ) + '--' + name, type: type }); return this; }; // Stores the given `shorthands` Hash of options to be `parse()`-d by nopt // later on. Noptify.prototype.shorthand = Noptify.prototype.shorthands = function shorthands(options, value) { if(typeof options === 'string' && value) { this._shorthands[options] = value; return this; } Object.keys(options).forEach(function(shorthand) { this._shorthands[shorthand] = options[shorthand]; }, this); return this; }; // Simply output to stdout the Usage and Help output. Noptify.prototype.help = function help() { var buf = ''; buf += '\n Usage: ' + this._program + ' [options]'; buf += '\n'; buf += '\n Options:\n'; var maxln = Math.max.apply(Math, this._options.map(function(opts) { return opts.usage.length; })); var options = this._options.map(function(opts) { return ' ' + pad(opts.usage, maxln + 5) + '\t- ' + opts.description; }); buf += options.join('\n'); // part of help input ? --list-shorthands ? var shorthands = Object.keys(this._shorthands); if(shorthands.length) { buf += '\n\n Shorthands:\n'; maxln = Math.max.apply(Math, Object.keys(this._shorthands).map(function(key) { return key.length; })); buf += Object.keys(this._shorthands).map(function(shorthand) { return ' --' + pad(shorthand, maxln + 1) + '\t\t' + this._shorthands[shorthand]; }, this).join('\n'); } buf += '\n'; console.log(buf); return this; }; // Helpers // command API Noptify.prototype.run = function run(fn) { if(fn) { this._steps.push(fn); return this; } if(!this.nopt.argv) return this.parse(); var steps = this._steps; var self = this; (function next(step) { if(!step) return; var async = /function\s*\(\w+/.test(step + ''); if(!async) { step(); return next(steps.shift()); } step(function(err) { if(err) return self.emit('error', err); next(steps.shift()); }); })(steps.shift()); }; function pad(str, max) { var ln = max - str.length; return ln > 0 ? str + new Array(ln).join(' ') : str; }