UNPKG

11.9 kBJavaScriptView Raw
1'use strict';
2
3// ### design
4// commander({
5// commands: './lib/command',
6// options: './opt/',
7// name: 'cortex'
8// }).cli();
9module.exports = comfort;
10
11function comfort(options) {
12 options || (options = {});
13 return new Comfort(options);
14}
15
16
17var node_path = require('path');
18var EE = require('events').EventEmitter;
19var util = require('util');
20var spawn = require('spawns');
21var fs = require('fs');
22var expand = require('fs-expand');
23var async = require('async');
24var clean = require('clean');
25var mix = require('mix2');
26
27var BUILTIN_COMMAND_ROOT = node_path.join(__dirname, 'lib', 'built-in', 'command');
28var BUILTIN_OPTION_ROOT = node_path.join(__dirname, 'lib', 'built-in', 'option');
29
30
31function Comfort(options) {
32 this.options = options;
33
34 this.commands = options.commands;
35 options.offset = 'offset' in options ? options.offset : 3;
36
37 this._context = {};
38 this._commander = {};
39}
40
41util.inherits(Comfort, EE);
42
43
44Comfort.prototype.context = function (context) {
45 mix(this._context, context);
46 return this;
47};
48
49
50Comfort.prototype.setup = function(setup) {
51 if (this.pending) {
52 return this;
53 }
54
55 if (typeof setup === 'function') {
56 this.pending = true;
57 var done = function () {
58 this.pending = false;
59 this.emit('setup');
60 }.bind(this);
61
62 setup.call(this, done);
63 }
64
65 return this;
66};
67
68
69// 'abc.js' -> 'abc'
70// 'abc.js.js' -> 'abc'
71var REGEX_REPLACE_EXTENSION = /\..+$/;
72
73// Get all commands
74Comfort.prototype._get_commands = function(callback) {
75 if (this.commands) {
76 return callback(null, this.commands);
77 }
78
79 var self = this;
80 expand('*.js', {
81 cwd: this.options.command_root
82 }, function (err, files) {
83 if (err) {
84 return callback(err);
85 }
86
87 var commands = files.map(function(command) {
88 return command.replace(REGEX_REPLACE_EXTENSION, '');
89 });
90 // Cache it
91 self.commands = commands;
92 callback(null, commands);
93 });
94};
95
96
97Comfort.prototype.parse = function(argv, callback) {
98 var self = this;
99 this._get_commands(function (err) {
100 if (err) {
101 return callback(err);
102 }
103
104 self._parse(argv, callback);
105 });
106};
107
108
109var BUILTIN_COMMANDS = [
110 'help',
111 'version'
112];
113
114// parse a specified argument vector
115// @param {Array} argv process.argv or something like that
116// @param {function(err, result, details)} callback
117// @param {boolean} strict if strict,
118Comfort.prototype._parse = function(argv, callback, strict) {
119 // argv ->
120 // ['node', __dirname, '<command>', ...]
121 var command = argv[2];
122 var is_entry = !command;
123
124 var is_command = this._is_command(command);
125 var is_normal = is_command && this._is_normal(command);
126 var is_builtin = is_command && this._is_builtin(command);
127
128 // Plugins
129 ////////////////////////////////////////////////////////////////////////////////////
130 // cortex <plugin> --version
131 // cortex <plugin> -v
132 // Plugin command needs special treatment.
133 if (is_command && !is_normal && !is_builtin) {
134 if (strict) {
135 return this._command_not_found(command, callback);
136 }
137
138 return callback(null, {
139 argv: argv,
140 command: command,
141
142 // 'normal', 'builtin', 'plugin'
143 type: 'plugin'
144 });
145 }
146
147 // Version
148 ////////////////////////////////////////////////////////////////////////////////////
149 if (
150 command === 'version' ||
151 ~argv.indexOf('-v') ||
152 ~argv.indexOf('--version')
153 ) {
154 command = 'version';
155 return callback(null, {
156 argv: argv,
157 command: command,
158 options: {
159 root: this.options.root
160 },
161 type: this._is_builtin(command)
162 ? 'builtin'
163 : 'normal'
164 });
165 }
166
167 // Help -h, --help
168 ////////////////////////////////////////////////////////////////////////////////////
169 var index_h = argv.indexOf('-h');
170 var index_help = argv.indexOf('--help');
171
172 var help_extra_options = {
173 commands : this.commands,
174 builtins : BUILTIN_COMMANDS,
175 name : this.options.name,
176 normal_root : this.options.option_root,
177 builtin_root : BUILTIN_OPTION_ROOT
178 };
179
180 // 'help' command need special treatment
181 if (
182 // cortex -h
183 ~index_h ||
184 // cortex --help
185 ~index_help ||
186 // root command will be help command
187 is_entry ||
188 // cortex --wrong-argument
189 !is_command
190 ) {
191 // 1 2 3
192 // cortex install -h
193 // cortex install --help
194 // -> cortex help
195 var command_for_help =
196 index_h !== 2 &&
197 index_help !== 2 &&
198 argv[2];
199
200 // Wrong usage
201 // cortex --abc -h
202 if (!this._is_command(command_for_help)) {
203 command_for_help = null;
204 }
205
206 command = 'help';
207
208 if (is_entry) {
209 this.emit('entry');
210 }
211
212 return callback(null, {
213 argv: argv,
214 command: command,
215 type: this._is_builtin(command)
216 ? 'builtin'
217 : 'normal',
218 options: mix({
219 command: command_for_help,
220 // if there's only root command, an `entrance` option will be added
221 entry: is_entry
222 }, help_extra_options)
223 });
224 }
225
226 // Normal & Builtin
227 ////////////////////////////////////////////////////////////////////////////////////
228 this._parse_argv(command, argv, function (err, result) {
229 if (err) {
230 return callback(err);
231 }
232
233 if (command === 'help') {
234 mix(result.options, help_extra_options);
235 }
236
237 callback(null, result);
238 });
239};
240
241
242Comfort.prototype._is_command = function(command) {
243 return command && command.indexOf('-') !== 0;
244};
245
246
247Comfort.prototype._is_builtin = function(command) {
248 return ~BUILTIN_COMMANDS.indexOf(command);
249};
250
251
252Comfort.prototype._is_normal = function(command) {
253 return ~this.commands.indexOf(command);
254};
255
256
257Comfort.prototype._command_not_found = function(command, callback) {
258 var name = this.options.name;
259 callback({
260 code: 'COMMAND_NOT_FOUND',
261 message: name + ': "' + command + '" is not a "' + name + '" command. See "' + name + ' --help".',
262 data: {
263 name: name,
264 command: command
265 }
266 });
267};
268
269
270// Parse the argv of a normal or builtin command
271Comfort.prototype._parse_argv = function(command, argv, callback) {
272 // builtin command is less
273 var is_builtin = this._is_builtin(command);
274 var option_root = is_builtin
275 ? BUILTIN_OPTION_ROOT
276 : this.options.option_root;
277
278 var type = is_builtin
279 ? 'builtin'
280 : 'normal';
281
282 var self = this;
283 this._get_option_rule(command, option_root, function (err, rule) {
284 if (err) {
285 return callback(err);
286 }
287
288 if (!rule) {
289 return callback(null, {
290 argv: argv,
291 command: command,
292 type: type,
293 options: {}
294 });
295 }
296
297 // parse argv
298 clean({
299 schema: rule.options,
300 shorthands: rule.shorthands,
301 offset: self.options.offset
302
303 }).parse(argv, function(err, results, details) {
304 callback(err, {
305 command: command,
306 options: results,
307 argv: argv,
308 type: type
309 });
310 });
311 });
312};
313
314
315Comfort.prototype._get_option_rule = function(command, root, callback) {
316 var file = node_path.join(root, command + '.js');
317 fs.exists(file, function (exists) {
318 if (!exists) {
319 return callback(null, null);
320 }
321
322 var rule;
323 try {
324 rule = require(file);
325 } catch(e) {
326 return callback({
327 code: 'FAIL_READ_OPTION',
328 message: 'Fails to read option file "' + file + '": ' + e.stack,
329 data: {
330 command: command,
331 file: file,
332 error: e
333 }
334 });
335 }
336
337 callback(null, rule);
338 });
339};
340
341
342Comfort.prototype._get_command = function(command, root, callback) {
343 var file = node_path.join(root, command + '.js');
344 fs.exists(file, function (exists) {
345 if (!exists) {
346 return this._command_not_found(command, callback);
347 }
348
349 var proto;
350 try {
351 proto = require(file);
352 } catch(e) {
353 return callback({
354 code: 'FAIL_READ_COMMAND',
355 message: 'Fails to read command file "' + file + '": ' + e.stack,
356 data: {
357 command: command,
358 file: file,
359 error: e
360 }
361 });
362 }
363
364 callback(null, proto);
365 });
366};
367
368
369// Run from argv
370Comfort.prototype.run = function(argv, callback) {
371 var self = this;
372 this.parse(argv, function (err, result) {
373 if (err) {
374 return callback(err);
375 }
376
377 var command = result.command;
378
379 if (result.type === 'plugin') {
380 return self.plugin(command, result.argv.slice(3), callback);
381 }
382
383 self.command(command, result.options, callback);
384 });
385};
386
387
388// Run a command with specified options
389Comfort.prototype.command = function(command, options, callback) {
390 this.commander(command, function (err, commander) {
391 if (err) {
392 return callback(err);
393 }
394
395 commander.run(options, callback);
396 });
397};
398
399
400// Try to run the given command from the `PATH`
401Comfort.prototype.plugin = function(command, args, callback) {
402 this.emit('plugin', {
403 name: name,
404 command: command,
405 args: args
406 });
407
408 var name = this.options.name;
409 var bin = name + '-' + command;
410 var paths = process.env.PATH.split(':').map(function (path) {
411 return node_path.resolve(path, bin);
412 });
413
414 var self = this;
415 this._try_files(paths, function (found) {
416 if (!found) {
417 return self._command_not_found(command, callback);
418 }
419
420 var plugin = spawn(found, args, {
421 stdio: 'inherit',
422 // `options.customFds` is now DEPRECATED.
423 // just for backward compatibility.
424 customFds: [0, 1, 2]
425 });
426
427 // for node <= 0.6, 'close' event often could not be triggered
428 plugin.on('exit', function(code) {
429 callback(code);
430 });
431 });
432};
433
434
435Comfort.prototype._try_files = function(files, callback) {
436 async.eachSeries(files, function (file, done) {
437 fs.exists(file, function (exists) {
438 if (!exists) {
439 return done(null);
440 }
441
442 fs.stat(file, function (err, stat) {
443 if (!err && stat.isFile()) {
444 return done(file);
445 }
446
447 done(null);
448 });
449 });
450 }, callback);
451};
452
453
454// @returns {Object|false}
455Comfort.prototype.commander = function(command, callback) {
456 // cache commander to improve performance
457 var commander = this._commander[command];
458 if (commander) {
459 return callback(null, commander);
460 }
461
462 var is_builtin = this._is_builtin(command);
463 var command_root = is_builtin
464 ? BUILTIN_COMMAND_ROOT
465 : this.options.command_root;
466
467 var self = this;
468 this._get_command(command, command_root, function (err, proto) {
469 if (err) {
470 return callback(err);
471 }
472
473 // There might be more than one comfort instances,
474 // so `Object.create` a new commander object to prevent reference pollution.
475 // Equivalent to prototype inheritance
476 var commander = self._commander[command] = Object.create(proto);
477
478 // Equivalent to `constructor.call(this)`
479 mix(commander, self._context);
480
481 callback(null, commander);
482 });
483};
484
485
486// # Design spec
487
488// ## method to run in cli (bin):
489// - .cli()
490
491// Should:
492// - driven by events
493
494// ## method used by users or utility methods:
495// - .run(),
496// - .command_exists(),
497// - .get_commander()
498
499// Should:
500// - driven by callbacks
501// - handle errors with node-favored async way
502
503// ****
504
505// # Events:
506// ## plugin
507// {
508// name: {string},
509// command: {string} plugin name,
510// args: {Array}
511// }
512
513// ## complete
514// {
515// name : {string}
516// command: {string} command name
517// err : {mixed}
518// data : []
519// }
520
521// run cli
522Comfort.prototype._cli = function(argv) {
523 argv = argv || process.argv;
524
525 var self = this;
526 this.run(argv, function (err) {
527 if (err) {
528 return self.emit('error', err);
529 }
530
531 self.emit('finish');
532 });
533};
534
535
536Comfort.prototype.cli = function(argv) {
537 if (this.pending) {
538 this.on('setup', function () {
539 this._cli(argv);
540 }.bind(this));
541 } else {
542 this._cli(argv);
543 }
544};