blob: 45eb6ae22dfcbada911dfc43e89b12cbc11c97e3 [file] [log] [blame]
/**
* Copyright (c) 2010 Chris O'Hara <cohara87@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//Note: cli includes kof/node-natives and creationix/stack. I couldn't find
//license information for either - contact me if you want your license added
var cli = exports,
argv, curr_opt, curr_val, full_opt, is_long,
short_tags = [], opt_list, parsed = {},
usage, argv_parsed, command_list, commands,
daemon, daemon_arg, no_color, show_debug;
cli.app = null;
cli.version = null;
cli.argv = [];
cli.argc = 0;
cli.options = {};
cli.args = [];
cli.command = null;
cli.width = 70;
cli.option_width = 25;
/**
* Bind kof's node-natives (https://github.com/kof/node-natives) to `cli.native`
*
* Rather than requiring node natives (e.g. var fs = require('fs')), all
* native modules can be accessed like `cli.native.fs`
*/
cli.native = {};
var define_native = function (module) {
Object.defineProperty(cli.native, module, {
enumerable: true,
configurable: true,
get: function() {
delete cli.native[module];
return (cli.native[module] = require(module));
}
});
};
var natives = process.binding('natives');
for (var module in natives) {
define_native(module);
}
cli.output = console.log;
cli.exit = require('exit');
/**
* Define plugins. Plugins can be enabled and disabled by calling:
*
* `cli.enable(plugin1, [plugin2, ...])`
* `cli.disable(plugin1, [plugin2, ...])`
*
* Methods are chainable - `cli.enable(plugin).disable(plugin2)`.
*
* The 'help' plugin is enabled by default.
*/
var enable = {
help: true, //Adds -h, --help
version: false, //Adds -v,--version => gets version by parsing a nearby package.json
daemon: false, //Adds -d,--daemon [ARG] => (see cli.daemon() below)
status: false, //Adds -k,--no-color & --debug => display plain status messages /display debug messages
timeout: false, //Adds -t,--timeout N => timeout the process after N seconds
catchall: false, //Adds -c,--catch => catch and output uncaughtExceptions
glob: false //Adds glob matching => use cli.glob(arg)
}
cli.enable = function (/*plugins*/) {
Array.prototype.slice.call(arguments).forEach(function (plugin) {
switch (plugin) {
case 'daemon':
try {
daemon = require('daemon');
if (typeof daemon.daemonize !== 'function') {
throw 'Invalid module';
}
} catch (e) {
cli.fatal('daemon.node not installed. Please run `npm install daemon`');
}
break;
case 'catchall':
process.on('uncaughtException', function (err) {
cli.error('Uncaught exception: ' + (err.msg || err));
});
break;
case 'help': case 'version': case 'status':
case 'autocomplete': case 'timeout':
//Just add switches.
break;
case 'glob':
cli.glob = require('glob');
break;
default:
cli.fatal('Unknown plugin "' + plugin + '"');
break;
}
enable[plugin] = true;
});
return cli;
}
cli.disable = function (/*plugins*/) {
Array.prototype.slice.call(arguments).forEach(function (plugin) {
if (enable[plugin]) {
enable[plugin] = false;
}
});
return cli;
}
/**
* Sets argv (default is process.argv).
*
* @param {Array|String} argv
* @param {Boolean} keep_arg0 (optional - default is false)
* @api public
*/
cli.setArgv = function (arr, keep_arg0) {
if (typeof arr == 'string') {
arr = arr.split(' ');
} else {
arr = arr.slice();
}
cli.app = arr.shift();
// Strip off argv[0] if it's a node binary
// So this is still broken and will break if you are calling node through a
// symlink, unless you are lucky enough to have it as 'node' literal. Latter
// is a hack, but resolving abspaths/symlinks is an unportable can of worms.
if (!keep_arg0 && (['node', 'node.exe'].indexOf(cli.native.path.basename(cli.app)) !== -1
|| cli.native.path.basename(process.execPath) === cli.app
|| process.execPath === cli.app)) {
cli.app = arr.shift();
}
cli.app = cli.native.path.basename(cli.app);
argv_parsed = false;
cli.args = cli.argv = argv = arr;
cli.argc = argv.length;
cli.options = {};
cli.command = null;
};
cli.setArgv(process.argv);
/**
* Returns the next opt, or false if no opts are found.
*
* @return {String} opt
* @api public
*/
cli.next = function () {
if (!argv_parsed) {
cli.args = [];
argv_parsed = true;
}
curr_val = null;
//If we're currently in a group of short opts (e.g. -abc), return the next opt
if (short_tags.length) {
curr_opt = short_tags.shift();
full_opt = '-' + curr_opt;
return curr_opt;
}
if (!argv.length) {
return false;
}
curr_opt = argv.shift();
//If an escape sequence is found (- or --), subsequent opts are ignored
if (curr_opt === '-' || curr_opt === '--') {
while (argv.length) {
cli.args.push(argv.shift());
}
return false;
}
//If the next element in argv isn't an opt, add it to the list of args
if (curr_opt[0] !== '-') {
cli.args.push(curr_opt);
return cli.next();
} else {
//Check if the opt is short/long
is_long = curr_opt[1] === '-';
curr_opt = curr_opt.substr(is_long ? 2 : 1);
}
//Accept grouped short opts, e.g. -abc => -a -b -c
if (!is_long && curr_opt.length > 1) {
short_tags = curr_opt.split('');
return cli.next();
}
var eq, len;
//Check if the long opt is in the form --option=VALUE
if (is_long && (eq = curr_opt.indexOf('=')) >= 0) {
curr_val = curr_opt.substr(eq + 1);
curr_opt = curr_opt.substr(0, eq);
len = curr_val.length;
//Allow values to be quoted
if ((curr_val[0] === '"' && curr_val[len - 1] === '"') ||
(curr_val[0] === "'" && curr_val[len - 1] === "'"))
{
curr_val = curr_val.substr(1, len-2);
}
if (curr_val.match(/^[0-9]+$/)) {
curr_val = parseInt(curr_val, 10);
}
}
//Save the opt representation for later
full_opt = (is_long ? '--' : '-') + curr_opt;
return curr_opt;
};
/**
* Parses command line opts.
*
* `opts` must be an object with opts defined like:
* long_tag: [short_tag, description, value_type, default_value];
*
* `commands` is an optional array or object for apps that are of the form
* my_app [OPTIONS] <command> [ARGS]
* The command list is output with usage information + there is bundled
* support for auto-completion, etc.
*
* See README.md for more information.
*
* @param {Object} opts
* @param {Object} commands (optional)
* @return {Object} opts (parsed)
* @api public
*/
cli.parse = function (opts, command_def) {
var default_val, i, parsed = cli.options, seen,
catch_all = !opts;
opt_list = opts || {};
commands = command_def;
command_list = commands || [];
if (commands && !Array.isArray(commands)) {
command_list = Object.keys(commands);
}
while ((o = cli.next())) {
seen = false;
for (var opt in opt_list) {
if (!(opt_list[opt] instanceof Array)) {
continue;
}
if (!opt_list[opt][0]) {
opt_list[opt][0] = opt;
}
if (o === opt || o === opt_list[opt][0]) {
seen = true;
if (opt_list[opt].length === 2) {
parsed[opt] = true;
break;
}
default_val = null;
if (opt_list[opt].length === 4) {
default_val = opt_list[opt][3];
}
if (opt_list[opt][2] instanceof Array) {
for (i = 0, l = opt_list[opt][2].length; i < l; i++) {
if (typeof opt_list[opt][2][i] === 'number') {
opt_list[opt][2][i] += '';
}
}
parsed[opt] = cli.getArrayValue(opt_list[opt][2], is_long ? null : default_val);
break;
}
if (opt_list[opt][2].toLowerCase) {
opt_list[opt][2] = opt_list[opt][2].toLowerCase();
}
switch (opt_list[opt][2]) {
case 'string': case 1: case true:
parsed[opt] = cli.getValue(default_val);
break;
case 'int': case 'number': case 'num':
case 'time': case 'seconds': case 'secs': case 'minutes': case 'mins':
case 'x': case 'n':
parsed[opt] = cli.getInt(default_val);
break;
case 'float': case 'decimal':
parsed[opt] = cli.getFloat(default_val);
break;
case 'path': case 'file': case 'directory': case 'dir':
parsed[opt] = cli.getPath(default_val, opt_list[opt][2]);
break;
case 'email':
parsed[opt] = cli.getEmail(default_val);
break;
case 'url': case 'uri': case 'domain': case 'host':
parsed[opt] = cli.getUrl(default_val, opt_list[opt][2]);
break;
case 'ip':
parsed[opt] = cli.getIp(default_val);
break;
case 'bool': case 'boolean': case 'on':
parsed[opt] = true;
break;
case 'false': case 'off': case false: case 0:
parsed[opt] = false;
break;
default:
cli.fatal('Unknown opt type "' + opt_list[opt][2] + '"');
}
break;
}
}
if (process.env.NODE_DISABLE_COLORS) {
no_color = true;
}
if (!seen) {
if (enable.help && (o === 'h' || o === 'help')) {
cli.getUsage();
} else if (enable.version && (o === 'v' || o === 'version')) {
if (cli.version == null) {
cli.parsePackageJson();
}
console.error(cli.app + ' v' + cli.version);
cli.exit();
break;
} else if (enable.daemon && (o === 'd' || o === 'daemon')) {
daemon_arg = cli.getArrayValue(['start','stop','restart','pid','log'], is_long ? null : 'start');
continue;
} else if (enable.catchall && (o === 'c' || o === 'catch')) {
continue;
} else if (enable.status && (o === 'k' || o === 'no-color' || o === 'debug')) {
no_color = (o === 'k' || o === 'no-color');
show_debug = o === 'debug';
continue;
} else if (enable.timeout && (o === 't' || o === 'timeout')) {
var secs = cli.getInt();
setTimeout(function () {
cli.fatal('Process timed out after ' + secs + 's');
}, secs * 1000);
continue;
} else if (catch_all) {
parsed[o] = curr_val || true;
continue;
}
cli.fatal('Unknown option ' + full_opt);
}
}
//Fill the remaining options with their default value or null
for (var opt in opt_list) {
default_val = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
if (!(opt_list[opt] instanceof Array)) {
parsed[opt] = opt_list[opt];
continue;
} else if (typeof parsed[opt] === 'undefined') {
parsed[opt] = default_val;
}
}
if (command_list.length) {
if (cli.args.length === 0) {
if (enable.help) {
cli.getUsage();
} else {
cli.fatal('A command is required (' + command_list.join(', ') + ').');
}
return cli.exit(1);
} else {
cli.command = cli.autocompleteCommand(cli.args.shift());
}
}
cli.argc = cli.args.length;
return parsed;
};
/**
* Helper method for matching a command from the command list.
*
* @param {String} command
* @return {String} full_command
* @api public
*/
cli.autocompleteCommand = function (command) {
var list;
if (!(command_list instanceof Array)) {
list = Object.keys(command_list);
} else {
list = command_list;
}
var i, j = 0, c = command.length, tmp_list;
if (list.length === 0 || list.indexOf(command) !== -1) {
return command;
}
for (i = 0; i < c; i++) {
tmp_list = [];
l = list.length;
if (l <= 1) break;
for (j = 0; j < l; j++)
if (list[j].length >= i && list[j][i] === command[i])
tmp_list.push(list[j]);
list = tmp_list;
}
l = list.length;
if (l === 1) {
return list[0];
} else if (l === 0) {
cli.fatal('Unknown command "' + command + '"' + (enable.help ? '. Please see --help for more information' : ''));
} else {
list.sort();
cli.fatal('The command "' + command + '" is ambiguous and could mean "' + list.join('", "') + '"');
}
};
/**
* Adds methods to output styled status messages to stderr.
*
* Added methods are cli.info(msg), cli.error(msg), cli.ok(msg), and
* cli.debug(msg).
*
* To control status messages, use the 'status' plugin
* 1) debug() messages are hidden by default. Display them with
* the --debug opt.
* 2) to hide all status messages, use the -s or --silent opt.
*
* @api private
*/
cli.status = function (msg, type) {
var pre;
switch (type) {
case 'info':
pre = no_color ? 'INFO:' : '\x1B[33mINFO\x1B[0m:';
break;
case 'debug':
pre = no_color ? 'DEBUG:' : '\x1B[36mDEBUG\x1B[0m:';
break;
case 'error':
case 'fatal':
pre = no_color ? 'ERROR:' : '\x1B[31mERROR\x1B[0m:';
break;
case 'ok':
pre = no_color ? 'OK:' : '\x1B[32mOK\x1B[0m:';
break;
}
msg = pre + ' ' + msg;
if (type === 'fatal') {
console.error(msg);
return cli.exit(1);
}
if (enable.status && !show_debug && type === 'debug') {
return;
}
console.error(msg);
};
['info','error','ok','debug','fatal'].forEach(function (type) {
cli[type] = function (msg) {
cli.status(msg, type);
};
});
/**
* Sets the app name and version.
*
* Usage:
* setApp('myapp', '0.1.0');
* setApp('./package.json'); //Pull name/version from package.json
*
* @param {String} name
* @return cli (for chaining)
* @api public
*/
cli.setApp = function (name, version) {
if (name.indexOf('package.json') !== -1) {
cli.parsePackageJson(name);
} else {
cli.app = name;
cli.version = version;
}
return cli;
};
/**
* Parses the version number from package.json. If no path is specified, cli
* will attempt to locate a package.json in ./, ../ or ../../
*
* @param {String} path (optional)
* @api public
*/
cli.parsePackageJson = function (path) {
var parse_packagejson = function (path) {
var packagejson = JSON.parse(cli.native.fs.readFileSync(path, 'utf8'));
cli.version = packagejson.version;
cli.app = packagejson.name;
};
var try_all = function (arr, func, err) {
for (var i = 0, l = arr.length; i < l; i++) {
try {
func(arr[i]);
return;
} catch (e) {
if (i === l-1) {
cli.fatal(err);
}
}
}
};
try {
if (path) {
return parse_packagejson(path);
}
try_all([
__dirname + '/package.json',
__dirname + '/../package.json',
__dirname + '/../../package.json'
], parse_packagejson);
} catch (e) {
cli.fatal('Could not detect ' + cli.app + ' version');
}
};
/**
* Sets the usage string - default is `app [OPTIONS] [ARGS]`.
*
* @param {String} u
* @return cli (for chaining)
* @api public
*/
cli.setUsage = function (u) {
usage = u;
return cli;
};
var pad = function (str, len) {
if (typeof len === 'undefined') {
len = str;
str = '';
}
if (str.length < len) {
len -= str.length;
while (len--) str += ' ';
}
return str;
};
/**
* Automatically build usage information from the opts list. If the help
* plugin is enabled (default), this info is displayed with -h, --help.
*
* @api public
*/
cli.getUsage = function (code) {
var short, desc, optional, line, seen_opts = [],
switch_pad = cli.option_width;
var trunc_desc = function (pref, desc, len) {
var pref_len = pref.length,
desc_len = cli.width - pref_len,
truncated = '';
if (desc.length <= desc_len) {
return desc;
}
var desc_words = (desc+'').split(' '), chars = 0, word;
while (desc_words.length) {
truncated += (word = desc_words.shift()) + ' ';
chars += word.length;
if (desc_words.length && chars + desc_words[0].length > desc_len) {
truncated += '\n' + pad(pref_len);
chars = 0;
}
}
return truncated;
};
usage = usage || cli.app + ' [OPTIONS]' + (command_list.length ? ' <command>' : '') + ' [ARGS]';
if (no_color) {
console.error('Usage:\n ' + usage);
console.error('Options: ');
} else {
console.error('\x1b[1mUsage\x1b[0m:\n ' + usage);
console.error('\n\x1b[1mOptions\x1b[0m: ');
}
for (var opt in opt_list) {
if (opt.length === 1) {
long = opt_list[opt][0];
short = opt;
} else {
long = opt;
short = opt_list[opt][0];
}
//Parse opt_list
desc = opt_list[opt][1].trim();
type = opt_list[opt].length >= 3 ? opt_list[opt][2] : null;
optional = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
//Build usage line
if (short === long) {
if (short.length === 1) {
line = ' -' + short;
} else {
line = ' --' + long;
}
} else if (short) {
line = ' -' + short + ', --' + long;
} else {
line = ' --' + long;
}
line += ' ';
if (type) {
if (type instanceof Array) {
desc += '. VALUE must be either [' + type.join('|') + ']';
type = 'VALUE';
}
if (type === true || type === 1) {
type = long.toUpperCase();
}
type = type.toUpperCase();
if (type === 'FLOAT' || type === 'INT') {
type = 'NUMBER';
}
line += optional ? '[' + type + ']' : type;
}
line = pad(line, switch_pad);
line += trunc_desc(line, desc);
line += optional ? ' (Default is ' + optional + ')' : '';
console.error(line.replace('%s', '%\0s'));
seen_opts.push(short);
seen_opts.push(long);
}
if (enable.timeout && seen_opts.indexOf('t') === -1 && seen_opts.indexOf('timeout') === -1) {
console.error(pad(' -t, --timeout N', switch_pad) + 'Exit if the process takes longer than N seconds');
}
if (enable.status) {
if (seen_opts.indexOf('k') === -1 && seen_opts.indexOf('no-color') === -1) {
console.error(pad(' -k, --no-color', switch_pad) + 'Omit color from output');
}
if (seen_opts.indexOf('debug') === -1) {
console.error(pad(' --debug', switch_pad) + 'Show debug information');
}
}
if (enable.catchall && seen_opts.indexOf('c') === -1 && seen_opts.indexOf('catch') === -1) {
console.error(pad(' -c, --catch', switch_pad) + 'Catch unanticipated errors');
}
if (enable.daemon && seen_opts.indexOf('d') === -1 && seen_opts.indexOf('daemon') === -1) {
console.error(pad(' -d, --daemon [ARG]', switch_pad) + 'Daemonize the process. Control the daemon using [start, stop, restart, log, pid]');
}
if (enable.version && seen_opts.indexOf('v') === -1 && seen_opts.indexOf('version') === -1) {
console.error(pad(' -v, --version', switch_pad) + 'Display the current version');
}
if (enable.help && seen_opts.indexOf('h') === -1 && seen_opts.indexOf('help') === -1) {
console.error(pad(' -h, --help', switch_pad) + 'Display help and usage details');
}
if (command_list.length) {
console.error('\n\x1b[1mCommands\x1b[0m: ');
if (!Array.isArray(commands)) {
for (var c in commands) {
line = ' ' + pad(c, switch_pad - 2);
line += trunc_desc(line, commands[c]);
console.error(line);
}
} else {
command_list.sort();
console.error(' ' + trunc_desc(' ', command_list.join(', ')));
}
}
return cli.exit(code);
};
/**
* Generates an error message when an opt is incorrectly used.
*
* @param {String} expects (e.g. 'a value')
* @param {String} type (e.g. 'VALUE')
* @api public
*/
cli.getOptError = function (expects, type) {
var err = full_opt + ' expects ' + expects
+ '. Use `' + cli.app + ' ' + full_opt + (is_long ? '=' : ' ') + type + '`';
return err;
};
/**
* Gets the next opt value and validates it with an optional validation
* function. If validation fails or no value can be obtained, this method
* will return the default value (if specified) or exit with err_msg.
*
* @param {String} default_val
* @param {Function} validate_func
* @param {String} err_msg
* @api public
*/
cli.getValue = function (default_val, validate_func, err_msg) {
err_msg = err_msg || cli.getOptError('a value', 'VALUE');
var value;
try {
if (curr_val) {
if (validate_func) {
curr_val = validate_func(curr_val);
}
return curr_val;
}
//Grouped short opts aren't allowed to have values
if (short_tags.length) {
throw 'Short tags';
}
//If there's no args left or the next arg is an opt, return the
//default value (if specified) - otherwise fail
if (!argv.length || (argv[0].length === 1 && argv[0][0] === '-')) {
throw 'No value';
}
value = argv.shift();
if (value.match(/^[0-9]+$/)) {
value = parseInt(value, 10);
}
//Run the value through a validation/transformation function if specified
if (validate_func) {
value = validate_func(value);
}
} catch (e) {
//The value didn't pass the validation/transformation. Unshift the value and
//return the default value (if specified)
if (value) {
argv.unshift(value);
}
return default_val != null ? default_val : cli.fatal(err_msg);
}
return value;
};
cli.getInt = function (default_val) {
return cli.getValue(default_val, function (value) {
if (typeof value === 'number') return value;
if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))$/)) {
throw 'Invalid int';
}
return parseInt(value);
}, cli.getOptError('a number', 'NUMBER'));
}
cli.getFloat = function (default_val) {
return cli.getValue(default_val, function (value) {
if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))?(?:\.[0-9]*)?$/)) {
throw 'Invalid float';
}
return parseFloat(value, 10);
}, cli.getOptError('a number', 'NUMBER'));
}
cli.getUrl = function (default_val, identifier) {
identifier = identifier || 'url';
return cli.getValue(default_val, function (value) {
if (!value.match(/^(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?:\w+:\w+@)?((?:(?:[-\w\d{1-3}]+\.)+(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|edu|co\.uk|ac\.uk|it|fr|tv|museum|asia|local|travel|[a-z]{2})?)|((\b25[0-5]\b|\b[2][0-4][0-9]\b|\b[0-1]?[0-9]?[0-9]\b)(\.(\b25[0-5]\b|\b[2][0-4][0-9]\b|\b[0-1]?[0-9]?[0-9]\b)){3}))(?::[\d]{1,5})?(?:(?:(?:\/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?:#(?:[-\w~!$ |\/.,*:;=]|%[a-f\d]{2})*)?$/i)) {
throw 'Invalid URL';
}
return value;
}, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
}
cli.getEmail = function (default_val) {
return cli.getValue(default_val, function (value) {
if (!value.match(/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/)) {
throw 'Invalid email';
}
return value;
}, cli.getOptError('an email', 'EMAIL'));
}
cli.getIp = function (default_val) {
return cli.getValue(default_val, function (value) {
if (!value.match(/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/)) {
throw 'Invalid IP';
}
return value;
}, cli.getOptError('an IP', 'IP'));
}
cli.getPath = function (default_val, identifier) {
identifier = identifier || 'path';
return cli.getValue(default_val, function (value) {
if (value.match(/[?*;{}]/)) {
throw 'Invalid path';
}
return value;
}, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
}
cli.getArrayValue = function (arr, default_val) {
return cli.getValue(default_val, function (value) {
if (arr.indexOf(value) === -1) {
throw 'Unexpected value';
}
return value;
}, cli.getOptError('either [' + arr.join('|') + ']', 'VALUE'));
}
/**
* Gets all data from STDIN (with optional encoding) and sends it to callback.
*
* @param {String} encoding (optional - default is 'utf8')
* @param {Function} callback
* @api public
*/
cli.withStdin = function (encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = 'utf8';
}
var stream = process.openStdin(), data = '';
stream.setEncoding(encoding);
stream.on('data', function (chunk) {
data += chunk;
});
stream.on('end', function () {
callback.apply(cli, [data]);
});
};
/**
* Gets all data from STDIN, splits the data into lines and sends it
* to callback (callback isn't called until all of STDIN is read. To
* process each line as it's received, see the method below
*
* @param {Function} callback
* @api public
*/
cli.withStdinLines = function (callback) {
cli.withStdin(function (data) {
var sep = data.indexOf('\r\n') !== -1 ? '\r\n' : '\n';
callback.apply(cli, [data.split(sep), sep]);
});
};
/**
* Asynchronously reads a file line by line. When a line is received,
* callback is called with (line, sep) - when EOF is reached, callback
* receives (null, null, true)
*
* @param {String} file (optional - default is 'stdin')
* @param {String} encoding (optional - default is 'utf8')
* @param {Function} callback (line, sep, eof)
* @api public
*/
cli.withInput = function (file, encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = 'utf8';
} else if (typeof file === 'function') {
callback = file;
encoding = 'utf8';
file = 'stdin';
}
if (file === 'stdin') {
file = process.openStdin();
} else {
try {
file = cli.native.fs.createReadStream(file);
file.on('error', cli.fatal);
} catch (e) {
return cli.fatal(e);
}
}
file.setEncoding(encoding);
var lines = [], data = '', eof, sep;
file.on('data', function (chunk) {
if (eof) return;
data += chunk;
if (!sep) {
if (data.indexOf('\r\n') !== -1) {
sep = '\r\n';
} else if (data.indexOf('\n') !== -1) {
sep = '\n';
} else {
last_line = data;
return;
}
}
lines = data.split(sep);
data = eof ? null : lines.pop();
while (lines.length) {
callback.apply(cli, [lines.shift(), sep, false]);
}
});
file.on('end', function () {
eof = true;
if (data.length) {
callback.apply(cli, [data, sep || '', false]);
}
callback.apply(cli, [null, null, true]);
});
};
/**
* A method for creating and controlling a daemon.
*
* `arg` can be:
* start = daemonizes the process
* stop = stops the daemon if it is running
* restart = alias for stop -> start
* pid = outputs the daemon's PID if it is running
* log = outputs the daemon's log file (stdout + stderr)
*
* @param {String} arg (Optional - default is 'start')
* @param {Function} callback
* @api public
*/
cli.daemon = function (arg, callback) {
if (typeof daemon === 'undefined') {
cli.fatal('Daemon is not initialized');
}
if (typeof arg === 'function') {
callback = arg;
arg = 'start';
}
var lock_file = '/tmp/' + cli.app + '.pid',
log_file = '/tmp/' + cli.app + '.log';
var start = function () {
daemon.daemonize(log_file, lock_file, function (err) {
if (err) return cli.error('Error starting daemon: ' + err);
callback();
});
};
var stop = function () {
try {
cli.native.fs.readFileSync(lock_file);
} catch (e) {
return cli.error('Daemon is not running');
}
daemon.kill(lock_file, function (err, pid) {
if (err && err.errno === 3) {
return cli.error('Daemon is not running');
} else if (err) {
return cli.error('Error stopping daemon: ' + err.errno);
}
cli.ok('Successfully stopped daemon with pid: ' + pid);
});
};
switch(arg) {
case 'stop':
stop();
break;
case 'restart':
daemon.stop(lock_file, function () {
start();
});
break;
case 'log':
try {
cli.native.fs.createReadStream(log_file, {encoding: 'utf8'}).pipe(process.stdout);
} catch (e) {
return cli.error('No daemon log file');
}
break;
case 'pid':
try {
var pid = cli.native.fs.readFileSync(lock_file, 'utf8');
cli.native.fs.statSync('/proc/' + pid);
cli.info(pid);
} catch (e) {
return cli.error('Daemon is not running');
}
break;
default:
start();
break;
}
}
/**
* The main entry method. Calling cli.main() is only necessary in
* scripts that have daemon support enabled. `callback` receives (args, options)
*
* @param {Function} callback
* @api public
*/
cli.main = function (callback) {
var after = function () {
callback.apply(cli, [cli.args, cli.options]);
};
if (enable.daemon && daemon_arg) {
cli.daemon(daemon_arg, after);
} else {
after();
}
}
/**
* Bind creationix's stack (https://github.com/creationix/stack).
*
* Create a simple middleware stack by calling:
*
* cli.createServer(middleware).listen(port);
*
* @return {Server} server
* @api public
*/
cli.createServer = function(/*layers*/) {
var defaultStackErrorHandler = function (req, res, err) {
if (err) {
console.error(err.stack);
res.writeHead(500, {"Content-Type": "text/plain"});
return res.end(err.stack + "\n");
}
res.writeHead(404, {"Content-Type": "text/plain"});
res.end("Not Found\n");
};
var handle, error;
handle = error = defaultStackErrorHandler;
var layers = Array.prototype.slice.call(arguments);
//Allow createServer(a,b,c) and createServer([a,b,c])
if (layers.length && layers[0] instanceof Array) {
layers = layers[0];
}
layers.reverse().forEach(function (layer) {
var child = handle;
handle = function (req, res) {
try {
layer(req, res, function (err) {
if (err) return error(req, res, err);
child(req, res);
});
} catch (err) {
error(req, res, err);
}
};
});
return cli.native.http.createServer(handle);
};
/**
* A wrapper for child_process.exec().
*
* If the child_process exits successfully, `callback` receives an array of
* stdout lines. The current process exits if the child process has an error
* and `errback` isn't defined.
*
* @param {String} cmd
* @param {Function} callback (optional)
* @param {Function} errback (optional)
* @api public
*/
cli.exec = function (cmd, callback, errback) {
cli.native.child_process.exec(cmd, function (err, stdout, stderr) {
err = err || stderr;
if (err) {
if (errback) {
return errback(err, stdout);
}
return cli.fatal('exec() failed\n' + err);
}
if (callback) {
callback(stdout.split('\n'));
}
});
};
/**
* Helper method for outputting a progress bar to the console.
*
* @param {Number} progress (0 <= progress <= 1)
* @api public
*/
var last_progress_call, progress_len = 74;
cli.progress = function (progress, decimals, stream) {
stream = stream || process.stdout;
if (progress < 0 || progress > 1 || isNaN(progress)) return;
if (!decimals) decimals = 0;
var now = (new Date()).getTime();
if (last_progress_call && (now - last_progress_call) < 100 && progress !== 1) {
return; //Throttle progress calls
}
last_progress_call = now;
var barLength = Math.floor(progress_len * progress),
str = '';
if (barLength == 0 && progress > 0) {
barLength = 1;
}
for (var i = 1; i <= progress_len; i++) {
str += i <= barLength ? '#' : ' ';
}
var pwr = Math.pow(10, decimals);
var percentage = Math.floor(progress * 100 * pwr) / pwr + '%';
for (i = 0; i < decimals; i++) {
percentage += ' ';
}
stream.clearLine();
stream.write('[' + str + '] ' + percentage);
if (progress === 1) {
stream.write('\n');
} else {
stream.cursorTo(0);
}
};
/**
* Helper method for outputting a spinner to the console.
*
* @param {String|Boolean} prefix (optional)
* @api public
*/
var spinnerInterval;
cli.spinner = function (prefix, end, stream) {
stream = stream || process.stdout;
if (end) {
stream.clearLine();
stream.cursorTo(0);
stream.write(prefix + '\n');
return clearInterval(spinnerInterval);
}
prefix = prefix + ' ' || '';
var spinner = ['-','\\','|','/'], i = 0, l = spinner.length;
spinnerInterval = setInterval(function () {
stream.clearLine();
stream.cursorTo(0);
stream.write(prefix + spinner[i++]);
if (i == l) i = 0;
}, 200);
};