Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81164 views
1
2
/**
3
* Module dependencies.
4
*/
5
6
var EventEmitter = require('events').EventEmitter;
7
var spawn = require('child_process').spawn;
8
var path = require('path');
9
var dirname = path.dirname;
10
var basename = path.basename;
11
12
/**
13
* Expose the root command.
14
*/
15
16
exports = module.exports = new Command();
17
18
/**
19
* Expose `Command`.
20
*/
21
22
exports.Command = Command;
23
24
/**
25
* Expose `Option`.
26
*/
27
28
exports.Option = Option;
29
30
/**
31
* Initialize a new `Option` with the given `flags` and `description`.
32
*
33
* @param {String} flags
34
* @param {String} description
35
* @api public
36
*/
37
38
function Option(flags, description) {
39
this.flags = flags;
40
this.required = ~flags.indexOf('<');
41
this.optional = ~flags.indexOf('[');
42
this.bool = !~flags.indexOf('-no-');
43
flags = flags.split(/[ ,|]+/);
44
if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
45
this.long = flags.shift();
46
this.description = description || '';
47
}
48
49
/**
50
* Return option name.
51
*
52
* @return {String}
53
* @api private
54
*/
55
56
Option.prototype.name = function() {
57
return this.long
58
.replace('--', '')
59
.replace('no-', '');
60
};
61
62
/**
63
* Check if `arg` matches the short or long flag.
64
*
65
* @param {String} arg
66
* @return {Boolean}
67
* @api private
68
*/
69
70
Option.prototype.is = function(arg) {
71
return arg == this.short || arg == this.long;
72
};
73
74
/**
75
* Initialize a new `Command`.
76
*
77
* @param {String} name
78
* @api public
79
*/
80
81
function Command(name) {
82
this.commands = [];
83
this.options = [];
84
this._execs = [];
85
this._args = [];
86
this._name = name;
87
}
88
89
/**
90
* Inherit from `EventEmitter.prototype`.
91
*/
92
93
Command.prototype.__proto__ = EventEmitter.prototype;
94
95
/**
96
* Add command `name`.
97
*
98
* The `.action()` callback is invoked when the
99
* command `name` is specified via __ARGV__,
100
* and the remaining arguments are applied to the
101
* function for access.
102
*
103
* When the `name` is "*" an un-matched command
104
* will be passed as the first arg, followed by
105
* the rest of __ARGV__ remaining.
106
*
107
* Examples:
108
*
109
* program
110
* .version('0.0.1')
111
* .option('-C, --chdir <path>', 'change the working directory')
112
* .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
113
* .option('-T, --no-tests', 'ignore test hook')
114
*
115
* program
116
* .command('setup')
117
* .description('run remote setup commands')
118
* .action(function() {
119
* console.log('setup');
120
* });
121
*
122
* program
123
* .command('exec <cmd>')
124
* .description('run the given remote command')
125
* .action(function(cmd) {
126
* console.log('exec "%s"', cmd);
127
* });
128
*
129
* program
130
* .command('teardown <dir> [otherDirs...]')
131
* .description('run teardown commands')
132
* .action(function(dir, otherDirs) {
133
* console.log('dir "%s"', dir);
134
* if (otherDirs) {
135
* otherDirs.forEach(function (oDir) {
136
* console.log('dir "%s"', oDir);
137
* });
138
* }
139
* });
140
*
141
* program
142
* .command('*')
143
* .description('deploy the given env')
144
* .action(function(env) {
145
* console.log('deploying "%s"', env);
146
* });
147
*
148
* program.parse(process.argv);
149
*
150
* @param {String} name
151
* @param {String} [desc] for git-style sub-commands
152
* @return {Command} the new command
153
* @api public
154
*/
155
156
Command.prototype.command = function(name, desc) {
157
var args = name.split(/ +/);
158
var cmd = new Command(args.shift());
159
160
if (desc) {
161
cmd.description(desc);
162
this.executables = true;
163
this._execs[cmd._name] = true;
164
}
165
166
this.commands.push(cmd);
167
cmd.parseExpectedArgs(args);
168
cmd.parent = this;
169
170
if (desc) return this;
171
return cmd;
172
};
173
174
/**
175
* Add an implicit `help [cmd]` subcommand
176
* which invokes `--help` for the given command.
177
*
178
* @api private
179
*/
180
181
Command.prototype.addImplicitHelpCommand = function() {
182
this.command('help [cmd]', 'display help for [cmd]');
183
};
184
185
/**
186
* Parse expected `args`.
187
*
188
* For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
189
*
190
* @param {Array} args
191
* @return {Command} for chaining
192
* @api public
193
*/
194
195
Command.prototype.parseExpectedArgs = function(args) {
196
if (!args.length) return;
197
var self = this;
198
args.forEach(function(arg) {
199
var argDetails = {
200
required: false,
201
name: '',
202
variadic: false
203
};
204
205
switch (arg[0]) {
206
case '<':
207
argDetails.required = true;
208
argDetails.name = arg.slice(1, -1);
209
break;
210
case '[':
211
argDetails.name = arg.slice(1, -1);
212
break;
213
}
214
215
if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
216
argDetails.variadic = true;
217
argDetails.name = argDetails.name.slice(0, -3);
218
}
219
if (argDetails.name) {
220
self._args.push(argDetails);
221
}
222
});
223
return this;
224
};
225
226
/**
227
* Register callback `fn` for the command.
228
*
229
* Examples:
230
*
231
* program
232
* .command('help')
233
* .description('display verbose help')
234
* .action(function() {
235
* // output help here
236
* });
237
*
238
* @param {Function} fn
239
* @return {Command} for chaining
240
* @api public
241
*/
242
243
Command.prototype.action = function(fn) {
244
var self = this;
245
var listener = function(args, unknown) {
246
// Parse any so-far unknown options
247
args = args || [];
248
unknown = unknown || [];
249
250
var parsed = self.parseOptions(unknown);
251
252
// Output help if necessary
253
outputHelpIfNecessary(self, parsed.unknown);
254
255
// If there are still any unknown options, then we simply
256
// die, unless someone asked for help, in which case we give it
257
// to them, and then we die.
258
if (parsed.unknown.length > 0) {
259
self.unknownOption(parsed.unknown[0]);
260
}
261
262
// Leftover arguments need to be pushed back. Fixes issue #56
263
if (parsed.args.length) args = parsed.args.concat(args);
264
265
self._args.forEach(function(arg, i) {
266
if (arg.required && null == args[i]) {
267
self.missingArgument(arg.name);
268
} else if (arg.variadic) {
269
if (i !== self._args.length - 1) {
270
self.variadicArgNotLast(arg.name);
271
}
272
273
args[i] = args.splice(i);
274
}
275
});
276
277
// Always append ourselves to the end of the arguments,
278
// to make sure we match the number of arguments the user
279
// expects
280
if (self._args.length) {
281
args[self._args.length] = self;
282
} else {
283
args.push(self);
284
}
285
286
fn.apply(self, args);
287
};
288
this.parent.on(this._name, listener);
289
if (this._alias) this.parent.on(this._alias, listener);
290
return this;
291
};
292
293
/**
294
* Define option with `flags`, `description` and optional
295
* coercion `fn`.
296
*
297
* The `flags` string should contain both the short and long flags,
298
* separated by comma, a pipe or space. The following are all valid
299
* all will output this way when `--help` is used.
300
*
301
* "-p, --pepper"
302
* "-p|--pepper"
303
* "-p --pepper"
304
*
305
* Examples:
306
*
307
* // simple boolean defaulting to false
308
* program.option('-p, --pepper', 'add pepper');
309
*
310
* --pepper
311
* program.pepper
312
* // => Boolean
313
*
314
* // simple boolean defaulting to true
315
* program.option('-C, --no-cheese', 'remove cheese');
316
*
317
* program.cheese
318
* // => true
319
*
320
* --no-cheese
321
* program.cheese
322
* // => false
323
*
324
* // required argument
325
* program.option('-C, --chdir <path>', 'change the working directory');
326
*
327
* --chdir /tmp
328
* program.chdir
329
* // => "/tmp"
330
*
331
* // optional argument
332
* program.option('-c, --cheese [type]', 'add cheese [marble]');
333
*
334
* @param {String} flags
335
* @param {String} description
336
* @param {Function|Mixed} fn or default
337
* @param {Mixed} defaultValue
338
* @return {Command} for chaining
339
* @api public
340
*/
341
342
Command.prototype.option = function(flags, description, fn, defaultValue) {
343
var self = this
344
, option = new Option(flags, description)
345
, oname = option.name()
346
, name = camelcase(oname);
347
348
// default as 3rd arg
349
if (typeof fn != 'function') {
350
defaultValue = fn;
351
fn = null;
352
}
353
354
// preassign default value only for --no-*, [optional], or <required>
355
if (false == option.bool || option.optional || option.required) {
356
// when --no-* we make sure default is true
357
if (false == option.bool) defaultValue = true;
358
// preassign only if we have a default
359
if (undefined !== defaultValue) self[name] = defaultValue;
360
}
361
362
// register the option
363
this.options.push(option);
364
365
// when it's passed assign the value
366
// and conditionally invoke the callback
367
this.on(oname, function(val) {
368
// coercion
369
if (null !== val && fn) val = fn(val, undefined === self[name]
370
? defaultValue
371
: self[name]);
372
373
// unassigned or bool
374
if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
375
// if no value, bool true, and we have a default, then use it!
376
if (null == val) {
377
self[name] = option.bool
378
? defaultValue || true
379
: false;
380
} else {
381
self[name] = val;
382
}
383
} else if (null !== val) {
384
// reassign
385
self[name] = val;
386
}
387
});
388
389
return this;
390
};
391
392
/**
393
* Parse `argv`, settings options and invoking commands when defined.
394
*
395
* @param {Array} argv
396
* @return {Command} for chaining
397
* @api public
398
*/
399
400
Command.prototype.parse = function(argv) {
401
// implicit help
402
if (this.executables) this.addImplicitHelpCommand();
403
404
// store raw args
405
this.rawArgs = argv;
406
407
// guess name
408
this._name = this._name || basename(argv[1], '.js');
409
410
// process argv
411
var parsed = this.parseOptions(this.normalize(argv.slice(2)));
412
var args = this.args = parsed.args;
413
414
var result = this.parseArgs(this.args, parsed.unknown);
415
416
// executable sub-commands
417
var name = result.args[0];
418
if (this._execs[name] && typeof this._execs[name] != "function") {
419
return this.executeSubCommand(argv, args, parsed.unknown);
420
}
421
422
return result;
423
};
424
425
/**
426
* Execute a sub-command executable.
427
*
428
* @param {Array} argv
429
* @param {Array} args
430
* @param {Array} unknown
431
* @api private
432
*/
433
434
Command.prototype.executeSubCommand = function(argv, args, unknown) {
435
args = args.concat(unknown);
436
437
if (!args.length) this.help();
438
if ('help' == args[0] && 1 == args.length) this.help();
439
440
// <cmd> --help
441
if ('help' == args[0]) {
442
args[0] = args[1];
443
args[1] = '--help';
444
}
445
446
// executable
447
var dir = dirname(argv[1]);
448
var bin = basename(argv[1], '.js') + '-' + args[0];
449
450
// check for ./<bin> first
451
var local = path.join(dir, bin);
452
453
// run it
454
args = args.slice(1);
455
args.unshift(local);
456
var proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
457
proc.on('error', function(err) {
458
if (err.code == "ENOENT") {
459
console.error('\n %s(1) does not exist, try --help\n', bin);
460
} else if (err.code == "EACCES") {
461
console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
462
}
463
});
464
465
this.runningCommand = proc;
466
};
467
468
/**
469
* Normalize `args`, splitting joined short flags. For example
470
* the arg "-abc" is equivalent to "-a -b -c".
471
* This also normalizes equal sign and splits "--abc=def" into "--abc def".
472
*
473
* @param {Array} args
474
* @return {Array}
475
* @api private
476
*/
477
478
Command.prototype.normalize = function(args) {
479
var ret = []
480
, arg
481
, lastOpt
482
, index;
483
484
for (var i = 0, len = args.length; i < len; ++i) {
485
arg = args[i];
486
if (i > 0) {
487
lastOpt = this.optionFor(args[i-1]);
488
}
489
490
if (arg === '--') {
491
// Honor option terminator
492
ret = ret.concat(args.slice(i));
493
break;
494
} else if (lastOpt && lastOpt.required) {
495
ret.push(arg);
496
} else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
497
arg.slice(1).split('').forEach(function(c) {
498
ret.push('-' + c);
499
});
500
} else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
501
ret.push(arg.slice(0, index), arg.slice(index + 1));
502
} else {
503
ret.push(arg);
504
}
505
}
506
507
return ret;
508
};
509
510
/**
511
* Parse command `args`.
512
*
513
* When listener(s) are available those
514
* callbacks are invoked, otherwise the "*"
515
* event is emitted and those actions are invoked.
516
*
517
* @param {Array} args
518
* @return {Command} for chaining
519
* @api private
520
*/
521
522
Command.prototype.parseArgs = function(args, unknown) {
523
var name;
524
525
if (args.length) {
526
name = args[0];
527
if (this.listeners(name).length) {
528
this.emit(args.shift(), args, unknown);
529
} else {
530
this.emit('*', args);
531
}
532
} else {
533
outputHelpIfNecessary(this, unknown);
534
535
// If there were no args and we have unknown options,
536
// then they are extraneous and we need to error.
537
if (unknown.length > 0) {
538
this.unknownOption(unknown[0]);
539
}
540
}
541
542
return this;
543
};
544
545
/**
546
* Return an option matching `arg` if any.
547
*
548
* @param {String} arg
549
* @return {Option}
550
* @api private
551
*/
552
553
Command.prototype.optionFor = function(arg) {
554
for (var i = 0, len = this.options.length; i < len; ++i) {
555
if (this.options[i].is(arg)) {
556
return this.options[i];
557
}
558
}
559
};
560
561
/**
562
* Parse options from `argv` returning `argv`
563
* void of these options.
564
*
565
* @param {Array} argv
566
* @return {Array}
567
* @api public
568
*/
569
570
Command.prototype.parseOptions = function(argv) {
571
var args = []
572
, len = argv.length
573
, literal
574
, option
575
, arg;
576
577
var unknownOptions = [];
578
579
// parse options
580
for (var i = 0; i < len; ++i) {
581
arg = argv[i];
582
583
// literal args after --
584
if ('--' == arg) {
585
literal = true;
586
continue;
587
}
588
589
if (literal) {
590
args.push(arg);
591
continue;
592
}
593
594
// find matching Option
595
option = this.optionFor(arg);
596
597
// option is defined
598
if (option) {
599
// requires arg
600
if (option.required) {
601
arg = argv[++i];
602
if (null == arg) return this.optionMissingArgument(option);
603
this.emit(option.name(), arg);
604
// optional arg
605
} else if (option.optional) {
606
arg = argv[i+1];
607
if (null == arg || ('-' == arg[0] && '-' != arg)) {
608
arg = null;
609
} else {
610
++i;
611
}
612
this.emit(option.name(), arg);
613
// bool
614
} else {
615
this.emit(option.name());
616
}
617
continue;
618
}
619
620
// looks like an option
621
if (arg.length > 1 && '-' == arg[0]) {
622
unknownOptions.push(arg);
623
624
// If the next argument looks like it might be
625
// an argument for this option, we pass it on.
626
// If it isn't, then it'll simply be ignored
627
if (argv[i+1] && '-' != argv[i+1][0]) {
628
unknownOptions.push(argv[++i]);
629
}
630
continue;
631
}
632
633
// arg
634
args.push(arg);
635
}
636
637
return { args: args, unknown: unknownOptions };
638
};
639
640
/**
641
* Return an object containing options as key-value pairs
642
*
643
* @return {Object}
644
* @api public
645
*/
646
Command.prototype.opts = function() {
647
var result = {}
648
, len = this.options.length;
649
650
for (var i = 0 ; i < len; i++) {
651
var key = this.options[i].name();
652
result[key] = key === 'version' ? this._version : this[key];
653
}
654
return result;
655
};
656
657
/**
658
* Argument `name` is missing.
659
*
660
* @param {String} name
661
* @api private
662
*/
663
664
Command.prototype.missingArgument = function(name) {
665
console.error();
666
console.error(" error: missing required argument `%s'", name);
667
console.error();
668
process.exit(1);
669
};
670
671
/**
672
* `Option` is missing an argument, but received `flag` or nothing.
673
*
674
* @param {String} option
675
* @param {String} flag
676
* @api private
677
*/
678
679
Command.prototype.optionMissingArgument = function(option, flag) {
680
console.error();
681
if (flag) {
682
console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
683
} else {
684
console.error(" error: option `%s' argument missing", option.flags);
685
}
686
console.error();
687
process.exit(1);
688
};
689
690
/**
691
* Unknown option `flag`.
692
*
693
* @param {String} flag
694
* @api private
695
*/
696
697
Command.prototype.unknownOption = function(flag) {
698
console.error();
699
console.error(" error: unknown option `%s'", flag);
700
console.error();
701
process.exit(1);
702
};
703
704
/**
705
* Variadic argument with `name` is not the last argument as required.
706
*
707
* @param {String} name
708
* @api private
709
*/
710
711
Command.prototype.variadicArgNotLast = function(name) {
712
console.error();
713
console.error(" error: variadic arguments must be last `%s'", name);
714
console.error();
715
process.exit(1);
716
};
717
718
/**
719
* Set the program version to `str`.
720
*
721
* This method auto-registers the "-V, --version" flag
722
* which will print the version number when passed.
723
*
724
* @param {String} str
725
* @param {String} flags
726
* @return {Command} for chaining
727
* @api public
728
*/
729
730
Command.prototype.version = function(str, flags) {
731
if (0 == arguments.length) return this._version;
732
this._version = str;
733
flags = flags || '-V, --version';
734
this.option(flags, 'output the version number');
735
this.on('version', function() {
736
process.stdout.write(str + '\n');
737
process.exit(0);
738
});
739
return this;
740
};
741
742
/**
743
* Set the description to `str`.
744
*
745
* @param {String} str
746
* @return {String|Command}
747
* @api public
748
*/
749
750
Command.prototype.description = function(str) {
751
if (0 == arguments.length) return this._description;
752
this._description = str;
753
return this;
754
};
755
756
/**
757
* Set an alias for the command
758
*
759
* @param {String} alias
760
* @return {String|Command}
761
* @api public
762
*/
763
764
Command.prototype.alias = function(alias) {
765
if (0 == arguments.length) return this._alias;
766
this._alias = alias;
767
return this;
768
};
769
770
/**
771
* Set / get the command usage `str`.
772
*
773
* @param {String} str
774
* @return {String|Command}
775
* @api public
776
*/
777
778
Command.prototype.usage = function(str) {
779
var args = this._args.map(function(arg) {
780
return humanReadableArgName(arg);
781
});
782
783
var usage = '[options]'
784
+ (this.commands.length ? ' [command]' : '')
785
+ (this._args.length ? ' ' + args.join(' ') : '');
786
787
if (0 == arguments.length) return this._usage || usage;
788
this._usage = str;
789
790
return this;
791
};
792
793
/**
794
* Get the name of the command
795
*
796
* @param {String} name
797
* @return {String|Command}
798
* @api public
799
*/
800
801
Command.prototype.name = function(name) {
802
return this._name;
803
};
804
805
/**
806
* Return the largest option length.
807
*
808
* @return {Number}
809
* @api private
810
*/
811
812
Command.prototype.largestOptionLength = function() {
813
return this.options.reduce(function(max, option) {
814
return Math.max(max, option.flags.length);
815
}, 0);
816
};
817
818
/**
819
* Return help for options.
820
*
821
* @return {String}
822
* @api private
823
*/
824
825
Command.prototype.optionHelp = function() {
826
var width = this.largestOptionLength();
827
828
// Prepend the help information
829
return [pad('-h, --help', width) + ' ' + 'output usage information']
830
.concat(this.options.map(function(option) {
831
return pad(option.flags, width) + ' ' + option.description;
832
}))
833
.join('\n');
834
};
835
836
/**
837
* Return command help documentation.
838
*
839
* @return {String}
840
* @api private
841
*/
842
843
Command.prototype.commandHelp = function() {
844
if (!this.commands.length) return '';
845
846
var commands = this.commands.map(function(cmd) {
847
var args = cmd._args.map(function(arg) {
848
return humanReadableArgName(arg);
849
}).join(' ');
850
851
return [
852
cmd._name
853
+ (cmd._alias
854
? '|' + cmd._alias
855
: '')
856
+ (cmd.options.length
857
? ' [options]'
858
: '')
859
+ ' ' + args
860
, cmd.description()
861
];
862
});
863
864
var width = commands.reduce(function(max, command) {
865
return Math.max(max, command[0].length);
866
}, 0);
867
868
return [
869
''
870
, ' Commands:'
871
, ''
872
, commands.map(function(cmd) {
873
return pad(cmd[0], width) + ' ' + cmd[1];
874
}).join('\n').replace(/^/gm, ' ')
875
, ''
876
].join('\n');
877
};
878
879
/**
880
* Return program help documentation.
881
*
882
* @return {String}
883
* @api private
884
*/
885
886
Command.prototype.helpInformation = function() {
887
return [
888
''
889
, ' Usage: ' + this._name
890
+ (this._alias
891
? '|' + this._alias
892
: '')
893
+ ' ' + this.usage()
894
, '' + this.commandHelp()
895
, ' Options:'
896
, ''
897
, '' + this.optionHelp().replace(/^/gm, ' ')
898
, ''
899
, ''
900
].join('\n');
901
};
902
903
/**
904
* Output help information for this command
905
*
906
* @api public
907
*/
908
909
Command.prototype.outputHelp = function() {
910
process.stdout.write(this.helpInformation());
911
this.emit('--help');
912
};
913
914
/**
915
* Output help information and exit.
916
*
917
* @api public
918
*/
919
920
Command.prototype.help = function() {
921
this.outputHelp();
922
process.exit();
923
};
924
925
/**
926
* Camel-case the given `flag`
927
*
928
* @param {String} flag
929
* @return {String}
930
* @api private
931
*/
932
933
function camelcase(flag) {
934
return flag.split('-').reduce(function(str, word) {
935
return str + word[0].toUpperCase() + word.slice(1);
936
});
937
}
938
939
/**
940
* Pad `str` to `width`.
941
*
942
* @param {String} str
943
* @param {Number} width
944
* @return {String}
945
* @api private
946
*/
947
948
function pad(str, width) {
949
var len = Math.max(0, width - str.length);
950
return str + Array(len + 1).join(' ');
951
}
952
953
/**
954
* Output help information if necessary
955
*
956
* @param {Command} command to output help for
957
* @param {Array} array of options to search for -h or --help
958
* @api private
959
*/
960
961
function outputHelpIfNecessary(cmd, options) {
962
options = options || [];
963
for (var i = 0; i < options.length; i++) {
964
if (options[i] == '--help' || options[i] == '-h') {
965
cmd.outputHelp();
966
process.exit(0);
967
}
968
}
969
}
970
971
/**
972
* Takes an argument an returns its human readable equivalent for help usage.
973
*
974
* @param {Object} arg
975
* @return {String}
976
* @api private
977
*/
978
979
function humanReadableArgName(arg) {
980
var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
981
982
return arg.required
983
? '<' + nameOutput + '>'
984
: '[' + nameOutput + ']'
985
}
986
987