UNPKG

36.7 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2010 Chris O'Hara <cohara87@gmail.com>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23
24 //Note: cli includes kof/node-natives and creationix/stack. I couldn't find
25 //license information for either - contact me if you want your license added
26
27var cli = exports,
28 argv, curr_opt, curr_val, full_opt, is_long,
29 short_tags = [], opt_list, parsed = {},
30 usage, argv_parsed, command_list, commands,
31 daemon, daemon_arg, no_color, show_debug;
32
33cli.app = null;
34cli.version = null;
35cli.argv = [];
36cli.argc = 0;
37
38cli.options = {};
39cli.args = [];
40cli.command;
41
42cli.width = 70;
43cli.option_width = 25;
44
45/**
46 * Bind kof's node-natives (https://github.com/kof/node-natives) to `cli.native`
47 *
48 * Rather than requiring node natives (e.g. var fs = require('fs')), all
49 * native modules can be accessed like `cli.native.fs`
50 */
51cli.native = {};
52var define_native = function (module) {
53 Object.defineProperty(cli.native, module, {
54 enumerable: true,
55 configurable: true,
56 get: function() {
57 delete cli.native[module];
58 return cli.native[module] = require(module);
59 }
60 });
61};
62var natives = process.binding('natives');
63for (var module in natives) {
64 define_native(module);
65}
66
67cli.output = cli.native.util.print;
68cli.exit = require('exit');
69
70/**
71 * Define plugins. Plugins can be enabled and disabled by calling:
72 *
73 * `cli.enable(plugin1, [plugin2, ...])`
74 * `cli.disable(plugin1, [plugin2, ...])`
75 *
76 * Methods are chainable - `cli.enable(plugin).disable(plugin2)`.
77 *
78 * The 'help' plugin is enabled by default.
79 */
80var enable = {
81 help: true, //Adds -h, --help
82 version: false, //Adds -v,--version => gets version by parsing a nearby package.json
83 daemon: false, //Adds -d,--daemon [ARG] => (see cli.daemon() below)
84 status: false, //Adds -k,--no-color & --debug => display plain status messages /display debug messages
85 timeout: false, //Adds -t,--timeout N => timeout the process after N seconds
86 catchall: false, //Adds -c,--catch => catch and output uncaughtExceptions
87 glob: false //Adds glob matching => use cli.glob(arg)
88}
89cli.enable = function (/*plugins*/) {
90 Array.prototype.slice.call(arguments).forEach(function (plugin) {
91 switch (plugin) {
92 case 'daemon':
93 try {
94 daemon = require('daemon');
95 if (typeof daemon.daemonize !== 'function') {
96 throw 'Invalid module';
97 }
98 } catch (e) {
99 cli.fatal('daemon.node not installed. Please run `npm install daemon`');
100 }
101 break;
102 case 'catchall':
103 process.on('uncaughtException', function (err) {
104 cli.error('Uncaught exception: ' + (err.msg || err));
105 });
106 break;
107 case 'help': case 'version': case 'status':
108 case 'autocomplete': case 'timeout':
109 //Just add switches.
110 break;
111 case 'glob':
112 cli.glob = require('glob');
113 break;
114 default:
115 cli.fatal('Unknown plugin "' + plugin + '"');
116 break;
117 }
118 enable[plugin] = true;
119 });
120 return cli;
121}
122cli.disable = function (/*plugins*/) {
123 Array.prototype.slice.call(arguments).forEach(function (plugin) {
124 if (enable[plugin]) {
125 enable[plugin] = false;
126 }
127 });
128 return cli;
129}
130
131/**
132 * Sets argv (default is process.argv).
133 *
134 * @param {Array|String} argv
135 * @param {Boolean} keep_arg0 (optional - default is false)
136 * @api public
137 */
138cli.setArgv = function (arr, keep_arg0) {
139 if (typeof arr == 'string') {
140 arr = arr.split(' ');
141 } else {
142 arr = arr.slice();
143 }
144 cli.app = arr.shift();
145 // Strip off argv[0] if it's a node binary
146 // So this is still broken and will break if you are calling node through a
147 // symlink, unless you are lucky enough to have it as 'node' literal. Latter
148 // is a hack, but resolving abspaths/symlinks is an unportable can of worms.
149 if (!keep_arg0 && ('node' === cli.native.path.basename(cli.app)
150 || cli.native.path.basename(process.execPath) === cli.app
151 || process.execPath === cli.app)) {
152 cli.app = arr.shift();
153 }
154 cli.app = cli.native.path.basename(cli.app);
155 argv_parsed = false;
156 cli.args = cli.argv = argv = arr;
157 cli.argc = argv.length;
158 cli.options = {};
159 cli.command = null;
160};
161cli.setArgv(process.argv);
162
163/**
164 * Returns the next opt, or false if no opts are found.
165 *
166 * @return {String} opt
167 * @api public
168 */
169cli.next = function () {
170 if (!argv_parsed) {
171 cli.args = [];
172 argv_parsed = true;
173 }
174
175 curr_val = null;
176
177 //If we're currently in a group of short opts (e.g. -abc), return the next opt
178 if (short_tags.length) {
179 curr_opt = short_tags.shift();
180 full_opt = '-' + curr_opt;
181 return curr_opt;
182 }
183
184 if (!argv.length) {
185 return false;
186 }
187
188 curr_opt = argv.shift();
189
190 //If an escape sequence is found (- or --), subsequent opts are ignored
191 if (curr_opt === '-' || curr_opt === '--') {
192 while (argv.length) {
193 cli.args.push(argv.shift());
194 }
195 return false;
196 }
197
198 //If the next element in argv isn't an opt, add it to the list of args
199 if (curr_opt[0] !== '-') {
200 cli.args.push(curr_opt);
201 return cli.next();
202 } else {
203 //Check if the opt is short/long
204 is_long = curr_opt[1] === '-';
205 curr_opt = curr_opt.substr(is_long ? 2 : 1);
206 }
207
208 //Accept grouped short opts, e.g. -abc => -a -b -c
209 if (!is_long && curr_opt.length > 1) {
210 short_tags = curr_opt.split('');
211 return cli.next();
212 }
213
214 var eq, len;
215
216 //Check if the long opt is in the form --option=VALUE
217 if (is_long && (eq = curr_opt.indexOf('=')) >= 0) {
218 curr_val = curr_opt.substr(eq + 1);
219 curr_opt = curr_opt.substr(0, eq);
220 len = curr_val.length;
221 //Allow values to be quoted
222 if ((curr_val[0] === '"' && curr_val[len - 1] === '"') ||
223 (curr_val[0] === "'" && curr_val[len - 1] === "'"))
224 {
225 curr_val = curr_val.substr(1, len-2);
226 }
227 if (curr_val.match(/^[0-9]+$/)) {
228 curr_val = parseInt(curr_val, 10);
229 }
230 }
231
232 //Save the opt representation for later
233 full_opt = (is_long ? '--' : '-') + curr_opt;
234
235 return curr_opt;
236};
237
238/**
239 * Parses command line opts.
240 *
241 * `opts` must be an object with opts defined like:
242 * long_tag: [short_tag, description, value_type, default_value];
243 *
244 * `commands` is an optional array or object for apps that are of the form
245 * my_app [OPTIONS] <command> [ARGS]
246 * The command list is output with usage information + there is bundled
247 * support for auto-completion, etc.
248 *
249 * See README.md for more information.
250 *
251 * @param {Object} opts
252 * @param {Object} commands (optional)
253 * @return {Object} opts (parsed)
254 * @api public
255 */
256cli.parse = function (opts, command_def) {
257 var default_val, i, parsed = cli.options, seen,
258 catch_all = !opts;
259 opt_list = opts || {};
260 commands = command_def;
261 command_list = commands || [];
262 if (commands && !Array.isArray(commands)) {
263 command_list = Object.keys(commands);
264 }
265 while (o = cli.next()) {
266 seen = false;
267 for (opt in opt_list) {
268 if (!(opt_list[opt] instanceof Array)) {
269 continue;
270 }
271 if (!opt_list[opt][0]) {
272 opt_list[opt][0] = opt;
273 }
274 if (o === opt || o === opt_list[opt][0]) {
275 seen = true;
276 if (opt_list[opt].length === 2) {
277 parsed[opt] = true;
278 break;
279 }
280 default_val = null;
281 if (opt_list[opt].length === 4) {
282 default_val = opt_list[opt][3];
283 }
284 if (opt_list[opt][2] instanceof Array) {
285 for (i = 0, l = opt_list[opt][2].length; i < l; i++) {
286 if (typeof opt_list[opt][2][i] === 'number') {
287 opt_list[opt][2][i] += '';
288 }
289 }
290 parsed[opt] = cli.getArrayValue(opt_list[opt][2], is_long ? null : default_val);
291 break;
292 }
293 if (opt_list[opt][2].toLowerCase) {
294 opt_list[opt][2] = opt_list[opt][2].toLowerCase();
295 }
296 switch (opt_list[opt][2]) {
297 case 'string': case 1: case true:
298 parsed[opt] = cli.getValue(default_val);
299 break;
300 case 'int': case 'number': case 'num':
301 case 'time': case 'seconds': case 'secs': case 'minutes': case 'mins':
302 case 'x': case 'n':
303 parsed[opt] = cli.getInt(default_val);
304 break;
305 case 'float': case 'decimal':
306 parsed[opt] = cli.getFloat(default_val);
307 break;
308 case 'path': case 'file': case 'directory': case 'dir':
309 parsed[opt] = cli.getPath(default_val, opt_list[opt][2]);
310 break;
311 case 'email':
312 parsed[opt] = cli.getEmail(default_val);
313 break;
314 case 'url': case 'uri': case 'domain': case 'host':
315 parsed[opt] = cli.getUrl(default_val, opt_list[opt][2]);
316 break;
317 case 'ip':
318 parsed[opt] = cli.getIp(default_val);
319 break;
320 case 'bool': case 'boolean': case 'on':
321 parsed[opt] = true;
322 break;
323 case 'false': case 'off': case false: case 0:
324 parsed[opt] = false;
325 break;
326 default:
327 cli.fatal('Unknown opt type "' + opt_list[opt][2] + '"');
328 }
329 break;
330 }
331 }
332 if (process.env.NODE_DISABLE_COLORS) {
333 no_color = true;
334 }
335 if (!seen) {
336 if (enable.help && (o === 'h' || o === 'help')) {
337 cli.getUsage();
338 } else if (enable.version && (o === 'v' || o === 'version')) {
339 if (cli.version == null) {
340 cli.parsePackageJson();
341 }
342 console.error(cli.app + ' v' + cli.version);
343 cli.exit();
344 break;
345 } else if (enable.daemon && (o === 'd' || o === 'daemon')) {
346 daemon_arg = cli.getArrayValue(['start','stop','restart','pid','log'], is_long ? null : 'start');
347 continue;
348 } else if (enable.catchall && (o === 'c' || o === 'catch')) {
349 continue;
350 } else if (enable.status && (o === 'k' || o === 'no-color' || o === 'debug')) {
351 no_color = (o === 'k' || o === 'no-color');
352 show_debug = o === 'debug';
353 continue;
354 } else if (enable.timeout && (o === 't' || o === 'timeout')) {
355 var secs = cli.getInt();
356 setTimeout(function () {
357 cli.fatal('Process timed out after ' + secs + 's');
358 }, secs * 1000);
359 continue;
360 } else if (catch_all) {
361 parsed[o] = curr_val || true;
362 continue;
363 }
364 cli.fatal('Unknown option ' + full_opt);
365 }
366 }
367 //Fill the remaining options with their default value or null
368 for (opt in opt_list) {
369 default_val = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
370 if (!(opt_list[opt] instanceof Array)) {
371 parsed[opt] = opt_list[opt];
372 continue;
373 } else if (typeof parsed[opt] === 'undefined') {
374 parsed[opt] = default_val;
375 }
376 }
377 if (command_list.length) {
378 if (cli.args.length === 0) {
379 if (enable.help) {
380 cli.getUsage();
381 } else {
382 cli.fatal('A command is required (' + command_list.join(', ') + ').');
383 }
384 return cli.exit(1);
385 } else {
386 cli.command = cli.autocompleteCommand(cli.args.shift());
387 }
388 }
389 cli.argc = cli.args.length;
390 return parsed;
391};
392
393/**
394 * Helper method for matching a command from the command list.
395 *
396 * @param {String} command
397 * @return {String} full_command
398 * @api public
399 */
400cli.autocompleteCommand = function (command) {
401 var list;
402 if (!(command_list instanceof Array)) {
403 list = Object.keys(command_list);
404 } else {
405 list = command_list;
406 }
407 var i, j = 0, c = command.length, tmp_list;
408 if (list.length === 0 || list.indexOf(command) !== -1) {
409 return command;
410 }
411 for (i = 0; i < c; i++) {
412 tmp_list = [];
413 l = list.length;
414 if (l <= 1) break;
415 for (j = 0; j < l; j++)
416 if (list[j].length >= i && list[j][i] === command[i])
417 tmp_list.push(list[j]);
418 list = tmp_list;
419 }
420 l = list.length;
421 if (l === 1) {
422 return list[0];
423 } else if (l === 0) {
424 cli.fatal('Unknown command "' + command + '"' + (enable.help ? '. Please see --help for more information' : ''));
425 } else {
426 list.sort();
427 cli.fatal('The command "' + command + '" is ambiguous and could mean "' + list.join('", "') + '"');
428 }
429};
430
431/**
432 * Adds methods to output styled status messages to stderr.
433 *
434 * Added methods are cli.info(msg), cli.error(msg), cli.ok(msg), and
435 * cli.debug(msg).
436 *
437 * To control status messages, use the 'status' plugin
438 * 1) debug() messages are hidden by default. Display them with
439 * the --debug opt.
440 * 2) to hide all status messages, use the -s or --silent opt.
441 *
442 * @api private
443 */
444cli.status = function (msg, type) {
445 var pre;
446 switch (type) {
447 case 'info':
448 pre = no_color ? 'INFO:' : '\x1B[33mINFO\x1B[0m:';
449 break;
450 case 'debug':
451 pre = no_color ? 'DEBUG:' : '\x1B[36mDEBUG\x1B[0m:';
452 break;
453 case 'error':
454 case 'fatal':
455 pre = no_color ? 'ERROR:' : '\x1B[31mERROR\x1B[0m:';
456 break;
457 case 'ok':
458 pre = no_color ? 'OK:' : '\x1B[32mOK\x1B[0m:';
459 break;
460 }
461 msg = pre + ' ' + msg;
462 if (type === 'fatal') {
463 console.error(msg);
464 return cli.exit(1);
465 }
466 if (enable.status && !show_debug && type === 'debug') {
467 return;
468 }
469 console.error(msg);
470};
471['info','error','ok','debug','fatal'].forEach(function (type) {
472 cli[type] = function (msg) {
473 cli.status(msg, type);
474 };
475});
476
477/**
478 * Sets the app name and version.
479 *
480 * Usage:
481 * setApp('myapp', '0.1.0');
482 * setApp('./package.json'); //Pull name/version from package.json
483 *
484 * @param {String} name
485 * @return cli (for chaining)
486 * @api public
487 */
488cli.setApp = function (name, version) {
489 if (name.indexOf('package.json') !== -1) {
490 cli.parsePackageJson(name);
491 } else {
492 cli.app = name;
493 cli.version = version;
494 }
495 return cli;
496};
497
498/**
499 * Parses the version number from package.json. If no path is specified, cli
500 * will attempt to locate a package.json in ./, ../ or ../../
501 *
502 * @param {String} path (optional)
503 * @api public
504 */
505cli.parsePackageJson = function (path) {
506 var parse_packagejson = function (path) {
507 var packagejson = JSON.parse(cli.native.fs.readFileSync(path, 'utf8'));
508 cli.version = packagejson.version;
509 cli.app = packagejson.name;
510 };
511 var try_all = function (arr, func, err) {
512 for (var i = 0, l = arr.length; i < l; i++) {
513 try {
514 func(arr[i]);
515 return;
516 } catch (e) {
517 if (i === l-1) {
518 cli.fatal(err);
519 }
520 }
521 }
522 };
523 try {
524 if (path) {
525 return parse_packagejson(path);
526 }
527 try_all([
528 __dirname + '/package.json',
529 __dirname + '/../package.json',
530 __dirname + '/../../package.json'
531 ], parse_packagejson);
532 } catch (e) {
533 cli.fatal('Could not detect ' + cli.app + ' version');
534 }
535};
536
537/**
538 * Sets the usage string - default is `app [OPTIONS] [ARGS]`.
539 *
540 * @param {String} u
541 * @return cli (for chaining)
542 * @api public
543 */
544cli.setUsage = function (u) {
545 usage = u;
546 return cli;
547};
548
549var pad = function (str, len) {
550 if (typeof len === 'undefined') {
551 len = str;
552 str = '';
553 }
554 if (str.length < len) {
555 len -= str.length;
556 while (len--) str += ' ';
557 }
558 return str;
559};
560
561/**
562 * Automatically build usage information from the opts list. If the help
563 * plugin is enabled (default), this info is displayed with -h, --help.
564 *
565 * @api public
566 */
567cli.getUsage = function (code) {
568 var short, desc, optional, line, seen_opts = [],
569 switch_pad = cli.option_width;
570
571 var trunc_desc = function (pref, desc, len) {
572 var pref_len = pref.length,
573 desc_len = cli.width - pref_len,
574 truncated = '';
575 if (desc.length <= desc_len) {
576 return desc;
577 }
578 var desc_words = (desc+'').split(' '), chars = 0, word;
579 while (desc_words.length) {
580 truncated += (word = desc_words.shift()) + ' ';
581 chars += word.length;
582 if (desc_words.length && chars + desc_words[0].length > desc_len) {
583 truncated += '\n' + pad(pref_len);
584 chars = 0;
585 }
586 }
587 return truncated;
588 };
589
590 usage = usage || cli.app + ' [OPTIONS]' + (command_list.length ? ' <command>' : '') + ' [ARGS]';
591 if (no_color) {
592 console.error('Usage:\n ' + usage);
593 console.error('Options: ');
594 } else {
595 console.error('\x1b[1mUsage\x1b[0m:\n ' + usage);
596 console.error('\n\x1b[1mOptions\x1b[0m: ');
597 }
598 for (opt in opt_list) {
599
600 if (opt.length === 1) {
601 long = opt_list[opt][0];
602 short = opt;
603 } else {
604 long = opt;
605 short = opt_list[opt][0];
606 }
607
608 //Parse opt_list
609 desc = opt_list[opt][1].trim();
610 type = opt_list[opt].length >= 3 ? opt_list[opt][2] : null;
611 optional = opt_list[opt].length === 4 ? opt_list[opt][3] : null;
612
613 //Build usage line
614 if (short === long) {
615 if (short.length === 1) {
616 line = ' -' + short;
617 } else {
618 line = ' --' + long;
619 }
620 } else if (short) {
621 line = ' -' + short + ', --' + long;
622 } else {
623 line = ' --' + long;
624 }
625 line += ' ';
626
627 if (type) {
628 if (type instanceof Array) {
629 desc += '. VALUE must be either [' + type.join('|') + ']';
630 type = 'VALUE';
631 }
632 if (type === true || type === 1) {
633 type = long.toUpperCase();
634 }
635 type = type.toUpperCase();
636 if (type === 'FLOAT' || type === 'INT') {
637 type = 'NUMBER';
638 }
639 line += optional ? '[' + type + ']' : type;
640 }
641 line = pad(line, switch_pad);
642 line += trunc_desc(line, desc);
643 line += optional ? ' (Default is ' + optional + ')' : '';
644 console.error(line.replace('%s', '%\0s'));
645
646 seen_opts.push(short);
647 seen_opts.push(long);
648 }
649 if (enable.timeout && seen_opts.indexOf('t') === -1 && seen_opts.indexOf('timeout') === -1) {
650 console.error(pad(' -t, --timeout N', switch_pad) + 'Exit if the process takes longer than N seconds');
651 }
652 if (enable.status) {
653 if (seen_opts.indexOf('k') === -1 && seen_opts.indexOf('no-color') === -1) {
654 console.error(pad(' -k, --no-color', switch_pad) + 'Omit color from output');
655 }
656 if (seen_opts.indexOf('debug') === -1) {
657 console.error(pad(' --debug', switch_pad) + 'Show debug information');
658 }
659 }
660 if (enable.catchall && seen_opts.indexOf('c') === -1 && seen_opts.indexOf('catch') === -1) {
661 console.error(pad(' -c, --catch', switch_pad) + 'Catch unanticipated errors');
662 }
663 if (enable.daemon && seen_opts.indexOf('d') === -1 && seen_opts.indexOf('daemon') === -1) {
664 console.error(pad(' -d, --daemon [ARG]', switch_pad) + 'Daemonize the process. Control the daemon using [start, stop, restart, log, pid]');
665 }
666 if (enable.version && seen_opts.indexOf('v') === -1 && seen_opts.indexOf('version') === -1) {
667 console.error(pad(' -v, --version', switch_pad) + 'Display the current version');
668 }
669 if (enable.help && seen_opts.indexOf('h') === -1 && seen_opts.indexOf('help') === -1) {
670 console.error(pad(' -h, --help', switch_pad) + 'Display help and usage details');
671 }
672 if (command_list.length) {
673 console.error('\n\x1b[1mCommands\x1b[0m: ');
674 if (!Array.isArray(commands)) {
675 for (var c in commands) {
676 line = ' ' + pad(c, switch_pad - 2);
677 line += trunc_desc(line, commands[c]);
678 console.error(line);
679 }
680 } else {
681 command_list.sort();
682 console.error(' ' + trunc_desc(' ', command_list.join(', ')));
683 }
684 }
685 return cli.exit(code);
686};
687
688/**
689 * Generates an error message when an opt is incorrectly used.
690 *
691 * @param {String} expects (e.g. 'a value')
692 * @param {String} type (e.g. 'VALUE')
693 * @api public
694 */
695cli.getOptError = function (expects, type) {
696 var err = full_opt + ' expects ' + expects
697 + '. Use `' + cli.app + ' ' + full_opt + (is_long ? '=' : ' ') + type + '`';
698 return err;
699};
700
701/**
702 * Gets the next opt value and validates it with an optional validation
703 * function. If validation fails or no value can be obtained, this method
704 * will return the default value (if specified) or exit with err_msg.
705 *
706 * @param {String} default_val
707 * @param {Function} validate_func
708 * @param {String} err_msg
709 * @api public
710 */
711cli.getValue = function (default_val, validate_func, err_msg) {
712 err_msg = err_msg || cli.getOptError('a value', 'VALUE');
713
714 var value;
715
716 try {
717 if (curr_val) {
718 if (validate_func) {
719 curr_val = validate_func(curr_val);
720 }
721 return curr_val;
722 }
723
724 //Grouped short opts aren't allowed to have values
725 if (short_tags.length) {
726 throw 'Short tags';
727 }
728
729 //If there's no args left or the next arg is an opt, return the
730 //default value (if specified) - otherwise fail
731 if (!argv.length || (argv[0].length === 1 && argv[0][0] === '-')) {
732 throw 'No value';
733 }
734
735 value = argv.shift();
736
737 if (value.match(/^[0-9]+$/)) {
738 value = parseInt(value, 10);
739 }
740
741 //Run the value through a validation/transformation function if specified
742 if (validate_func) {
743 value = validate_func(value);
744 }
745 } catch (e) {
746
747 //The value didn't pass the validation/transformation. Unshift the value and
748 //return the default value (if specified)
749 if (value) {
750 argv.unshift(value);
751 }
752 return default_val != null ? default_val : cli.fatal(err_msg);
753 }
754 return value;
755};
756
757cli.getInt = function (default_val) {
758 return cli.getValue(default_val, function (value) {
759 if (typeof value === 'number') return value;
760 if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))$/)) {
761 throw 'Invalid int';
762 }
763 return parseInt(value);
764 }, cli.getOptError('a number', 'NUMBER'));
765}
766
767cli.getFloat = function (default_val) {
768 return cli.getValue(default_val, function (value) {
769 if (!value.match(/^(?:-?(?:0|[1-9][0-9]*))?(?:\.[0-9]*)?$/)) {
770 throw 'Invalid float';
771 }
772 return parseFloat(value, 10);
773 }, cli.getOptError('a number', 'NUMBER'));
774}
775
776cli.getUrl = function (default_val, identifier) {
777 identifier = identifier || 'url';
778 return cli.getValue(default_val, function (value) {
779 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)) {
780 throw 'Invalid URL';
781 }
782 return value;
783 }, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
784}
785
786cli.getEmail = function (default_val) {
787 return cli.getValue(default_val, function (value) {
788 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])\]))$/)) {
789 throw 'Invalid email';
790 }
791 return value;
792 }, cli.getOptError('an email', 'EMAIL'));
793}
794
795cli.getIp = function (default_val) {
796 return cli.getValue(default_val, function (value) {
797 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]?)$/)) {
798 throw 'Invalid IP';
799 }
800 return value;
801 }, cli.getOptError('an IP', 'IP'));
802}
803
804cli.getPath = function (default_val, identifier) {
805 identifier = identifier || 'path';
806 return cli.getValue(default_val, function (value) {
807 if (value.match(/[?*;{}]/)) {
808 throw 'Invalid path';
809 }
810 return value;
811 }, cli.getOptError('a ' + identifier, identifier.toUpperCase()));
812}
813
814cli.getArrayValue = function (arr, default_val) {
815 return cli.getValue(default_val, function (value) {
816 if (arr.indexOf(value) === -1) {
817 throw 'Unexpected value';
818 }
819 return value;
820 }, cli.getOptError('either [' + arr.join('|') + ']', 'VALUE'));
821}
822
823/**
824 * Gets all data from STDIN (with optional encoding) and sends it to callback.
825 *
826 * @param {String} encoding (optional - default is 'utf8')
827 * @param {Function} callback
828 * @api public
829 */
830cli.withStdin = function (encoding, callback) {
831 if (typeof encoding === 'function') {
832 callback = encoding;
833 encoding = 'utf8';
834 }
835 var stream = process.openStdin(), data = '';
836 stream.setEncoding(encoding);
837 stream.on('data', function (chunk) {
838 data += chunk;
839 });
840 stream.on('end', function () {
841 callback.apply(cli, [data]);
842 });
843};
844
845/**
846 * Gets all data from STDIN, splits the data into lines and sends it
847 * to callback (callback isn't called until all of STDIN is read. To
848 * process each line as it's received, see the method below
849 *
850 * @param {Function} callback
851 * @api public
852 */
853cli.withStdinLines = function (callback) {
854 cli.withStdin(function (data) {
855 var sep = data.indexOf('\r\n') !== -1 ? '\r\n' : '\n';
856 callback.apply(cli, [data.split(sep), sep]);
857 });
858};
859
860/**
861 * Asynchronously reads a file line by line. When a line is received,
862 * callback is called with (line, sep) - when EOF is reached, callback
863 * receives (null, null, true)
864 *
865 * @param {String} file (optional - default is 'stdin')
866 * @param {String} encoding (optional - default is 'utf8')
867 * @param {Function} callback (line, sep, eof)
868 * @api public
869 */
870cli.withInput = function (file, encoding, callback) {
871 if (typeof encoding === 'function') {
872 callback = encoding;
873 encoding = 'utf8';
874 } else if (typeof file === 'function') {
875 callback = file;
876 encoding = 'utf8';
877 file = 'stdin';
878 }
879 if (file === 'stdin') {
880 file = process.openStdin();
881 } else {
882 try {
883 file = cli.native.fs.createReadStream(file);
884 file.on('error', cli.fatal);
885 } catch (e) {
886 return cli.fatal(e);
887 }
888 }
889 file.setEncoding(encoding);
890 var lines = [], data = '', eof, sep;
891 file.on('data', function (chunk) {
892 if (eof) return;
893 data += chunk;
894 if (!sep) {
895 if (data.indexOf('\r\n') !== -1) {
896 sep = '\r\n';
897 } else if (data.indexOf('\n') !== -1) {
898 sep = '\n';
899 } else {
900 last_line = data;
901 return;
902 }
903 }
904 lines = data.split(sep);
905 data = eof ? null : lines.pop();
906 while (lines.length) {
907 callback.apply(cli, [lines.shift(), sep, false]);
908 }
909 });
910 file.on('end', function () {
911 eof = true;
912 if (data.length) {
913 callback.apply(cli, [data, sep || '', false]);
914 }
915 callback.apply(cli, [null, null, true]);
916 });
917};
918
919/**
920 * A method for creating and controlling a daemon.
921 *
922 * `arg` can be:
923 * start = daemonizes the process
924 * stop = stops the daemon if it is running
925 * restart = alias for stop -> start
926 * pid = outputs the daemon's PID if it is running
927 * log = outputs the daemon's log file (stdout + stderr)
928 *
929 * @param {String} arg (Optional - default is 'start')
930 * @param {Function} callback
931 * @api public
932 */
933cli.daemon = function (arg, callback) {
934 if (typeof daemon === 'undefined') {
935 cli.fatal('Daemon is not initialized');
936 }
937
938 if (typeof arg === 'function') {
939 callback = arg;
940 arg = 'start';
941 }
942
943 var lock_file = '/tmp/' + cli.app + '.pid',
944 log_file = '/tmp/' + cli.app + '.log';
945
946 var start = function () {
947 daemon.daemonize(log_file, lock_file, function (err) {
948 if (err) return cli.error('Error starting daemon: ' + err);
949 callback();
950 });
951 };
952
953 var stop = function () {
954 try {
955 cli.native.fs.readFileSync(lock_file);
956 } catch (e) {
957 return cli.error('Daemon is not running');
958 };
959 daemon.kill(lock_file, function (err, pid) {
960 if (err && err.errno === 3) {
961 return cli.error('Daemon is not running');
962 } else if (err) {
963 return cli.error('Error stopping daemon: ' + err.errno);
964 }
965 cli.ok('Successfully stopped daemon with pid: ' + pid);
966 });
967 };
968
969 switch(arg) {
970 case 'stop':
971 stop();
972 break;
973 case 'restart':
974 daemon.stop(lock_file, function () {
975 start();
976 });
977 break;
978 case 'log':
979 try {
980 cli.native.fs.createReadStream(log_file, {encoding: 'utf8'}).pipe(process.stdout);
981 } catch (e) {
982 return cli.error('No daemon log file');
983 };
984 break;
985 case 'pid':
986 try {
987 var pid = cli.native.fs.readFileSync(lock_file, 'utf8');
988 cli.native.fs.statSync('/proc/' + pid);
989 cli.info(pid);
990 } catch (e) {
991 return cli.error('Daemon is not running');
992 };
993 break;
994 default:
995 start();
996 break;
997 }
998}
999
1000/**
1001 * The main entry method. Calling cli.main() is only necessary in
1002 * scripts that have daemon support enabled. `callback` receives (args, options)
1003 *
1004 * @param {Function} callback
1005 * @api public
1006 */
1007cli.main = function (callback) {
1008 var after = function () {
1009 callback.apply(cli, [cli.args, cli.options]);
1010 };
1011 if (enable.daemon && daemon_arg) {
1012 cli.daemon(daemon_arg, after);
1013 } else {
1014 after();
1015 }
1016}
1017
1018/**
1019 * Bind creationix's stack (https://github.com/creationix/stack).
1020 *
1021 * Create a simple middleware stack by calling:
1022 *
1023 * cli.createServer(middleware).listen(port);
1024 *
1025 * @return {Server} server
1026 * @api public
1027 */
1028cli.createServer = function(/*layers*/) {
1029 var defaultStackErrorHandler = function (req, res, err) {
1030 if (err) {
1031 console.error(err.stack);
1032 res.writeHead(500, {"Content-Type": "text/plain"});
1033 return res.end(err.stack + "\n");
1034 }
1035 res.writeHead(404, {"Content-Type": "text/plain"});
1036 res.end("Not Found\n");
1037 };
1038 var handle = error = defaultStackErrorHandler,
1039 layers = Array.prototype.slice.call(arguments);
1040
1041 //Allow createServer(a,b,c) and createServer([a,b,c])
1042 if (layers.length && layers[0] instanceof Array) {
1043 layers = layers[0];
1044 }
1045 layers.reverse().forEach(function (layer) {
1046 var child = handle;
1047 handle = function (req, res) {
1048 try {
1049 layer(req, res, function (err) {
1050 if (err) return error(req, res, err);
1051 child(req, res);
1052 });
1053 } catch (err) {
1054 error(req, res, err);
1055 }
1056 };
1057 });
1058 return cli.native.http.createServer(handle);
1059};
1060
1061/**
1062 * A wrapper for child_process.exec().
1063 *
1064 * If the child_process exits successfully, `callback` receives an array of
1065 * stdout lines. The current process exits if the child process has an error
1066 * and `errback` isn't defined.
1067 *
1068 * @param {String} cmd
1069 * @param {Function} callback (optional)
1070 * @param {Function} errback (optional)
1071 * @api public
1072 */
1073cli.exec = function (cmd, callback, errback) {
1074 cli.native.child_process.exec(cmd, function (err, stdout, stderr) {
1075 err = err || stderr;
1076 if (err) {
1077 if (errback) {
1078 return errback(err, stdout);
1079 }
1080 return cli.fatal('exec() failed\n' + err);
1081 }
1082 if (callback) {
1083 callback(stdout.split('\n'));
1084 }
1085 });
1086};
1087
1088/**
1089 * Helper method for outputting a progress bar to the console.
1090 *
1091 * @param {Number} progress (0 <= progress <= 1)
1092 * @api public
1093 */
1094var last_progress_call, progress_len = 74;
1095cli.progress = function (progress, decimals) {
1096 if (progress < 0 || progress > 1 || isNaN(progress)) return;
1097 if (!decimals) decimals = 0;
1098 var now = (new Date()).getTime();
1099 if (last_progress_call && (now - last_progress_call) < 100 && progress !== 1) {
1100 return; //Throttle progress calls
1101 }
1102 last_progress_call = now;
1103
1104
1105 var barLength = Math.floor(progress_len * progress),
1106 str = '';
1107 if (barLength == 0 && progress > 0) {
1108 barLength = 1;
1109 }
1110 for (var i = 1; i <= progress_len; i++) {
1111 str += i <= barLength ? '#' : ' ';
1112 }
1113 var pwr = Math.pow(10, decimals);
1114 var percentage = Math.floor(progress * 100 * pwr) / pwr + '%';
1115 for (i = 0; i < decimals; i++) {
1116 percentage += ' ';
1117 }
1118 cli.native.util.print('[' + str + '] ' + percentage + (progress === 1 ? '\n' : '\u000D'));
1119};
1120
1121/**
1122 * Helper method for outputting a spinner to the console.
1123 *
1124 * @param {String|Boolean} prefix (optional)
1125 * @api public
1126 */
1127var spinnerInterval;
1128cli.spinner = function (prefix, end) {
1129 if (end) {
1130 cli.native.util.print('\u000D' + prefix);
1131 return clearInterval(spinnerInterval);
1132 }
1133 prefix = prefix + ' ' || '';
1134 var spinner = ['-','\\','|','/'], i = 0, l = spinner.length;
1135 spinnerInterval = setInterval(function () {
1136 cli.native.util.print('\u000D' + prefix + spinner[i++]);
1137 if (i == l) i = 0;
1138 }, 200);
1139};