commander 6.2.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/CHANGELOG.md +140 -105
  2. package/Readme.md +318 -195
  3. package/index.js +792 -473
  4. package/package.json +15 -12
  5. package/typings/index.d.ts +241 -30
package/index.js CHANGED
@@ -3,23 +3,359 @@
3
3
  */
4
4
 
5
5
  const EventEmitter = require('events').EventEmitter;
6
- const spawn = require('child_process').spawn;
6
+ const childProcess = require('child_process');
7
7
  const path = require('path');
8
8
  const fs = require('fs');
9
9
 
10
10
  // @ts-check
11
11
 
12
+ // Although this is a class, methods are static in style to allow override using subclass or just functions.
13
+ class Help {
14
+ constructor() {
15
+ this.helpWidth = undefined;
16
+ this.sortSubcommands = false;
17
+ this.sortOptions = false;
18
+ }
19
+
20
+ /**
21
+ * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
22
+ *
23
+ * @param {Command} cmd
24
+ * @returns {Command[]}
25
+ */
26
+
27
+ visibleCommands(cmd) {
28
+ const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
29
+ if (cmd._hasImplicitHelpCommand()) {
30
+ // Create a command matching the implicit help command.
31
+ const args = cmd._helpCommandnameAndArgs.split(/ +/);
32
+ const helpCommand = cmd.createCommand(args.shift())
33
+ .helpOption(false);
34
+ helpCommand.description(cmd._helpCommandDescription);
35
+ helpCommand._parseExpectedArgs(args);
36
+ visibleCommands.push(helpCommand);
37
+ }
38
+ if (this.sortSubcommands) {
39
+ visibleCommands.sort((a, b) => {
40
+ return a.name().localeCompare(b.name());
41
+ });
42
+ }
43
+ return visibleCommands;
44
+ }
45
+
46
+ /**
47
+ * Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one.
48
+ *
49
+ * @param {Command} cmd
50
+ * @returns {Option[]}
51
+ */
52
+
53
+ visibleOptions(cmd) {
54
+ const visibleOptions = cmd.options.filter((option) => !option.hidden);
55
+ // Implicit help
56
+ const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag);
57
+ const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag);
58
+ if (showShortHelpFlag || showLongHelpFlag) {
59
+ let helpOption;
60
+ if (!showShortHelpFlag) {
61
+ helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription);
62
+ } else if (!showLongHelpFlag) {
63
+ helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription);
64
+ } else {
65
+ helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
66
+ }
67
+ visibleOptions.push(helpOption);
68
+ }
69
+ if (this.sortOptions) {
70
+ const getSortKey = (option) => {
71
+ // WYSIWYG for order displayed in help with short before long, no special handling for negated.
72
+ return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
73
+ };
74
+ visibleOptions.sort((a, b) => {
75
+ return getSortKey(a).localeCompare(getSortKey(b));
76
+ });
77
+ }
78
+ return visibleOptions;
79
+ }
80
+
81
+ /**
82
+ * Get an array of the arguments which have descriptions.
83
+ *
84
+ * @param {Command} cmd
85
+ * @returns {{ term: string, description:string }[]}
86
+ */
87
+
88
+ visibleArguments(cmd) {
89
+ if (cmd._argsDescription && cmd._args.length) {
90
+ return cmd._args.map((argument) => {
91
+ return { term: argument.name, description: cmd._argsDescription[argument.name] || '' };
92
+ }, 0);
93
+ }
94
+ return [];
95
+ }
96
+
97
+ /**
98
+ * Get the command term to show in the list of subcommands.
99
+ *
100
+ * @param {Command} cmd
101
+ * @returns {string}
102
+ */
103
+
104
+ subcommandTerm(cmd) {
105
+ // Legacy. Ignores custom usage string, and nested commands.
106
+ const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' ');
107
+ return cmd._name +
108
+ (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
109
+ (cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
110
+ (args ? ' ' + args : '');
111
+ }
112
+
113
+ /**
114
+ * Get the option term to show in the list of options.
115
+ *
116
+ * @param {Option} option
117
+ * @returns {string}
118
+ */
119
+
120
+ optionTerm(option) {
121
+ return option.flags;
122
+ }
123
+
124
+ /**
125
+ * Get the longest command term length.
126
+ *
127
+ * @param {Command} cmd
128
+ * @param {Help} helper
129
+ * @returns {number}
130
+ */
131
+
132
+ longestSubcommandTermLength(cmd, helper) {
133
+ return helper.visibleCommands(cmd).reduce((max, command) => {
134
+ return Math.max(max, helper.subcommandTerm(command).length);
135
+ }, 0);
136
+ };
137
+
138
+ /**
139
+ * Get the longest option term length.
140
+ *
141
+ * @param {Command} cmd
142
+ * @param {Help} helper
143
+ * @returns {number}
144
+ */
145
+
146
+ longestOptionTermLength(cmd, helper) {
147
+ return helper.visibleOptions(cmd).reduce((max, option) => {
148
+ return Math.max(max, helper.optionTerm(option).length);
149
+ }, 0);
150
+ };
151
+
152
+ /**
153
+ * Get the longest argument term length.
154
+ *
155
+ * @param {Command} cmd
156
+ * @param {Help} helper
157
+ * @returns {number}
158
+ */
159
+
160
+ longestArgumentTermLength(cmd, helper) {
161
+ return helper.visibleArguments(cmd).reduce((max, argument) => {
162
+ return Math.max(max, argument.term.length);
163
+ }, 0);
164
+ };
165
+
166
+ /**
167
+ * Get the command usage to be displayed at the top of the built-in help.
168
+ *
169
+ * @param {Command} cmd
170
+ * @returns {string}
171
+ */
172
+
173
+ commandUsage(cmd) {
174
+ // Usage
175
+ let cmdName = cmd._name;
176
+ if (cmd._aliases[0]) {
177
+ cmdName = cmdName + '|' + cmd._aliases[0];
178
+ }
179
+ let parentCmdNames = '';
180
+ for (let parentCmd = cmd.parent; parentCmd; parentCmd = parentCmd.parent) {
181
+ parentCmdNames = parentCmd.name() + ' ' + parentCmdNames;
182
+ }
183
+ return parentCmdNames + cmdName + ' ' + cmd.usage();
184
+ }
185
+
186
+ /**
187
+ * Get the description for the command.
188
+ *
189
+ * @param {Command} cmd
190
+ * @returns {string}
191
+ */
192
+
193
+ commandDescription(cmd) {
194
+ // @ts-ignore: overloaded return type
195
+ return cmd.description();
196
+ }
197
+
198
+ /**
199
+ * Get the command description to show in the list of subcommands.
200
+ *
201
+ * @param {Command} cmd
202
+ * @returns {string}
203
+ */
204
+
205
+ subcommandDescription(cmd) {
206
+ // @ts-ignore: overloaded return type
207
+ return cmd.description();
208
+ }
209
+
210
+ /**
211
+ * Get the option description to show in the list of options.
212
+ *
213
+ * @param {Option} option
214
+ * @return {string}
215
+ */
216
+
217
+ optionDescription(option) {
218
+ if (option.negate) {
219
+ return option.description;
220
+ }
221
+ const extraInfo = [];
222
+ if (option.argChoices) {
223
+ extraInfo.push(
224
+ // use stringify to match the display of the default value
225
+ `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
226
+ }
227
+ if (option.defaultValue !== undefined) {
228
+ extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
229
+ }
230
+ if (extraInfo.length > 0) {
231
+ return `${option.description} (${extraInfo.join(', ')})`;
232
+ }
233
+ return option.description;
234
+ };
235
+
236
+ /**
237
+ * Generate the built-in help text.
238
+ *
239
+ * @param {Command} cmd
240
+ * @param {Help} helper
241
+ * @returns {string}
242
+ */
243
+
244
+ formatHelp(cmd, helper) {
245
+ const termWidth = helper.padWidth(cmd, helper);
246
+ const helpWidth = helper.helpWidth || 80;
247
+ const itemIndentWidth = 2;
248
+ const itemSeparatorWidth = 2; // between term and description
249
+ function formatItem(term, description) {
250
+ if (description) {
251
+ const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
252
+ return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
253
+ }
254
+ return term;
255
+ };
256
+ function formatList(textArray) {
257
+ return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
258
+ }
259
+
260
+ // Usage
261
+ let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
262
+
263
+ // Description
264
+ const commandDescription = helper.commandDescription(cmd);
265
+ if (commandDescription.length > 0) {
266
+ output = output.concat([commandDescription, '']);
267
+ }
268
+
269
+ // Arguments
270
+ const argumentList = helper.visibleArguments(cmd).map((argument) => {
271
+ return formatItem(argument.term, argument.description);
272
+ });
273
+ if (argumentList.length > 0) {
274
+ output = output.concat(['Arguments:', formatList(argumentList), '']);
275
+ }
276
+
277
+ // Options
278
+ const optionList = helper.visibleOptions(cmd).map((option) => {
279
+ return formatItem(helper.optionTerm(option), helper.optionDescription(option));
280
+ });
281
+ if (optionList.length > 0) {
282
+ output = output.concat(['Options:', formatList(optionList), '']);
283
+ }
284
+
285
+ // Commands
286
+ const commandList = helper.visibleCommands(cmd).map((cmd) => {
287
+ return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
288
+ });
289
+ if (commandList.length > 0) {
290
+ output = output.concat(['Commands:', formatList(commandList), '']);
291
+ }
292
+
293
+ return output.join('\n');
294
+ }
295
+
296
+ /**
297
+ * Calculate the pad width from the maximum term length.
298
+ *
299
+ * @param {Command} cmd
300
+ * @param {Help} helper
301
+ * @returns {number}
302
+ */
303
+
304
+ padWidth(cmd, helper) {
305
+ return Math.max(
306
+ helper.longestOptionTermLength(cmd, helper),
307
+ helper.longestSubcommandTermLength(cmd, helper),
308
+ helper.longestArgumentTermLength(cmd, helper)
309
+ );
310
+ };
311
+
312
+ /**
313
+ * Wrap the given string to width characters per line, with lines after the first indented.
314
+ * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.
315
+ *
316
+ * @param {string} str
317
+ * @param {number} width
318
+ * @param {number} indent
319
+ * @param {number} [minColumnWidth=40]
320
+ * @return {string}
321
+ *
322
+ */
323
+
324
+ wrap(str, width, indent, minColumnWidth = 40) {
325
+ // Detect manually wrapped and indented strings by searching for line breaks
326
+ // followed by multiple spaces/tabs.
327
+ if (str.match(/[\n]\s+/)) return str;
328
+ // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line).
329
+ const columnWidth = width - indent;
330
+ if (columnWidth < minColumnWidth) return str;
331
+
332
+ const leadingStr = str.substr(0, indent);
333
+ const columnText = str.substr(indent);
334
+
335
+ const indentString = ' '.repeat(indent);
336
+ const regex = new RegExp('.{1,' + (columnWidth - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g');
337
+ const lines = columnText.match(regex) || [];
338
+ return leadingStr + lines.map((line, i) => {
339
+ if (line.slice(-1) === '\n') {
340
+ line = line.slice(0, line.length - 1);
341
+ }
342
+ return ((i > 0) ? indentString : '') + line.trimRight();
343
+ }).join('\n');
344
+ }
345
+ }
346
+
12
347
  class Option {
13
348
  /**
14
349
  * Initialize a new `Option` with the given `flags` and `description`.
15
350
  *
16
351
  * @param {string} flags
17
- * @param {string} description
18
- * @api public
352
+ * @param {string} [description]
19
353
  */
20
354
 
21
355
  constructor(flags, description) {
22
356
  this.flags = flags;
357
+ this.description = description || '';
358
+
23
359
  this.required = flags.includes('<'); // A value must be supplied when the option is specified.
24
360
  this.optional = flags.includes('['); // A value is optional when the option is specified.
25
361
  // variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
@@ -32,15 +368,85 @@ class Option {
32
368
  if (this.long) {
33
369
  this.negate = this.long.startsWith('--no-');
34
370
  }
35
- this.description = description || '';
36
371
  this.defaultValue = undefined;
372
+ this.defaultValueDescription = undefined;
373
+ this.parseArg = undefined;
374
+ this.hidden = false;
375
+ this.argChoices = undefined;
37
376
  }
38
377
 
378
+ /**
379
+ * Set the default value, and optionally supply the description to be displayed in the help.
380
+ *
381
+ * @param {any} value
382
+ * @param {string} [description]
383
+ * @return {Option}
384
+ */
385
+
386
+ default(value, description) {
387
+ this.defaultValue = value;
388
+ this.defaultValueDescription = description;
389
+ return this;
390
+ };
391
+
392
+ /**
393
+ * Set the custom handler for processing CLI option arguments into option values.
394
+ *
395
+ * @param {Function} [fn]
396
+ * @return {Option}
397
+ */
398
+
399
+ argParser(fn) {
400
+ this.parseArg = fn;
401
+ return this;
402
+ };
403
+
404
+ /**
405
+ * Whether the option is mandatory and must have a value after parsing.
406
+ *
407
+ * @param {boolean} [mandatory=true]
408
+ * @return {Option}
409
+ */
410
+
411
+ makeOptionMandatory(mandatory = true) {
412
+ this.mandatory = !!mandatory;
413
+ return this;
414
+ };
415
+
416
+ /**
417
+ * Hide option in help.
418
+ *
419
+ * @param {boolean} [hide=true]
420
+ * @return {Option}
421
+ */
422
+
423
+ hideHelp(hide = true) {
424
+ this.hidden = !!hide;
425
+ return this;
426
+ };
427
+
428
+ /**
429
+ * Only allow option value to be one of choices.
430
+ *
431
+ * @param {string[]} values
432
+ * @return {Option}
433
+ */
434
+
435
+ choices(values) {
436
+ this.argChoices = values;
437
+ this.parseArg = (arg) => {
438
+ if (!values.includes(arg)) {
439
+ throw new InvalidOptionArgumentError(`Allowed choices are ${values.join(', ')}.`);
440
+ }
441
+ return arg;
442
+ };
443
+ return this;
444
+ };
445
+
39
446
  /**
40
447
  * Return option name.
41
448
  *
42
449
  * @return {string}
43
- * @api private
44
450
  */
45
451
 
46
452
  name() {
@@ -98,12 +504,29 @@ class CommanderError extends Error {
98
504
  }
99
505
  }
100
506
 
507
+ /**
508
+ * InvalidOptionArgumentError class
509
+ * @class
510
+ */
511
+ class InvalidOptionArgumentError extends CommanderError {
512
+ /**
513
+ * Constructs the InvalidOptionArgumentError class
514
+ * @param {string} [message] explanation of why argument is invalid
515
+ * @constructor
516
+ */
517
+ constructor(message) {
518
+ super(1, 'commander.invalidOptionArgument', message);
519
+ // properly capture stack trace in Node.js
520
+ Error.captureStackTrace(this, this.constructor);
521
+ this.name = this.constructor.name;
522
+ }
523
+ }
524
+
101
525
  class Command extends EventEmitter {
102
526
  /**
103
527
  * Initialize a new `Command`.
104
528
  *
105
529
  * @param {string} [name]
106
- * @api public
107
530
  */
108
531
 
109
532
  constructor(name) {
@@ -112,14 +535,13 @@ class Command extends EventEmitter {
112
535
  this.options = [];
113
536
  this.parent = null;
114
537
  this._allowUnknownOption = false;
538
+ this._allowExcessArguments = true;
115
539
  this._args = [];
116
540
  this.rawArgs = null;
117
541
  this._scriptPath = null;
118
542
  this._name = name || '';
119
543
  this._optionValues = {};
120
- this._storeOptionsAsProperties = true; // backwards compatible by default
121
- this._storeOptionsAsPropertiesCalled = false;
122
- this._passCommandToAction = true; // backwards compatible by default
544
+ this._storeOptionsAsProperties = false;
123
545
  this._actionResults = [];
124
546
  this._actionHandler = null;
125
547
  this._executableHandler = false;
@@ -128,6 +550,19 @@ class Command extends EventEmitter {
128
550
  this._exitCallback = null;
129
551
  this._aliases = [];
130
552
  this._combineFlagAndOptionalValue = true;
553
+ this._description = '';
554
+ this._argsDescription = undefined;
555
+ this._enablePositionalOptions = false;
556
+ this._passThroughOptions = false;
557
+
558
+ // see .configureOutput() for docs
559
+ this._outputConfiguration = {
560
+ writeOut: (str) => process.stdout.write(str),
561
+ writeErr: (str) => process.stderr.write(str),
562
+ getOutHelpWidth: () => process.stdout.isTTY ? process.stdout.columns : undefined,
563
+ getErrHelpWidth: () => process.stderr.isTTY ? process.stderr.columns : undefined,
564
+ outputError: (str, write) => write(str)
565
+ };
131
566
 
132
567
  this._hidden = false;
133
568
  this._hasHelpOption = true;
@@ -135,10 +570,11 @@ class Command extends EventEmitter {
135
570
  this._helpDescription = 'display help for command';
136
571
  this._helpShortFlag = '-h';
137
572
  this._helpLongFlag = '--help';
138
- this._hasImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false
573
+ this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false
139
574
  this._helpCommandName = 'help';
140
575
  this._helpCommandnameAndArgs = 'help [command]';
141
576
  this._helpCommandDescription = 'display help for command';
577
+ this._helpConfiguration = {};
142
578
  }
143
579
 
144
580
  /**
@@ -165,7 +601,6 @@ class Command extends EventEmitter {
165
601
  * @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable)
166
602
  * @param {Object} [execOpts] - configuration options (for executable)
167
603
  * @return {Command} returns new command for action handler, or `this` for executable command
168
- * @api public
169
604
  */
170
605
 
171
606
  command(nameAndArgs, actionOptsOrExecDesc, execOpts) {
@@ -185,7 +620,9 @@ class Command extends EventEmitter {
185
620
  }
186
621
  if (opts.isDefault) this._defaultCommandName = cmd._name;
187
622
 
188
- cmd._hidden = !!(opts.noHelp || opts.hidden);
623
+ cmd._outputConfiguration = this._outputConfiguration;
624
+
625
+ cmd._hidden = !!(opts.noHelp || opts.hidden); // noHelp is deprecated old name for hidden
189
626
  cmd._hasHelpOption = this._hasHelpOption;
190
627
  cmd._helpFlags = this._helpFlags;
191
628
  cmd._helpDescription = this._helpDescription;
@@ -194,10 +631,12 @@ class Command extends EventEmitter {
194
631
  cmd._helpCommandName = this._helpCommandName;
195
632
  cmd._helpCommandnameAndArgs = this._helpCommandnameAndArgs;
196
633
  cmd._helpCommandDescription = this._helpCommandDescription;
634
+ cmd._helpConfiguration = this._helpConfiguration;
197
635
  cmd._exitCallback = this._exitCallback;
198
636
  cmd._storeOptionsAsProperties = this._storeOptionsAsProperties;
199
- cmd._passCommandToAction = this._passCommandToAction;
200
637
  cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue;
638
+ cmd._allowExcessArguments = this._allowExcessArguments;
639
+ cmd._enablePositionalOptions = this._enablePositionalOptions;
201
640
 
202
641
  cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
203
642
  this.commands.push(cmd);
@@ -216,13 +655,64 @@ class Command extends EventEmitter {
216
655
  *
217
656
  * @param {string} [name]
218
657
  * @return {Command} new command
219
- * @api public
220
658
  */
221
659
 
222
660
  createCommand(name) {
223
661
  return new Command(name);
224
662
  };
225
663
 
664
+ /**
665
+ * You can customise the help with a subclass of Help by overriding createHelp,
666
+ * or by overriding Help properties using configureHelp().
667
+ *
668
+ * @return {Help}
669
+ */
670
+
671
+ createHelp() {
672
+ return Object.assign(new Help(), this.configureHelp());
673
+ };
674
+
675
+ /**
676
+ * You can customise the help by overriding Help properties using configureHelp(),
677
+ * or with a subclass of Help by overriding createHelp().
678
+ *
679
+ * @param {Object} [configuration] - configuration options
680
+ * @return {Command|Object} `this` command for chaining, or stored configuration
681
+ */
682
+
683
+ configureHelp(configuration) {
684
+ if (configuration === undefined) return this._helpConfiguration;
685
+
686
+ this._helpConfiguration = configuration;
687
+ return this;
688
+ }
689
+
690
+ /**
691
+ * The default output goes to stdout and stderr. You can customise this for special
692
+ * applications. You can also customise the display of errors by overriding outputError.
693
+ *
694
+ * The configuration properties are all functions:
695
+ *
696
+ * // functions to change where being written, stdout and stderr
697
+ * writeOut(str)
698
+ * writeErr(str)
699
+ * // matching functions to specify width for wrapping help
700
+ * getOutHelpWidth()
701
+ * getErrHelpWidth()
702
+ * // functions based on what is being written out
703
+ * outputError(str, write) // used for displaying errors, and not used for displaying help
704
+ *
705
+ * @param {Object} [configuration] - configuration options
706
+ * @return {Command|Object} `this` command for chaining, or stored configuration
707
+ */
708
+
709
+ configureOutput(configuration) {
710
+ if (configuration === undefined) return this._outputConfiguration;
711
+
712
+ Object.assign(this._outputConfiguration, configuration);
713
+ return this;
714
+ }
715
+
226
716
  /**
227
717
  * Add a prepared subcommand.
228
718
  *
@@ -231,7 +721,6 @@ class Command extends EventEmitter {
231
721
  * @param {Command} cmd - new subcommand
232
722
  * @param {Object} [opts] - configuration options
233
723
  * @return {Command} `this` command for chaining
234
- * @api public
235
724
  */
236
725
 
237
726
  addCommand(cmd, opts) {
@@ -260,8 +749,6 @@ class Command extends EventEmitter {
260
749
 
261
750
  /**
262
751
  * Define argument syntax for the command.
263
- *
264
- * @api public
265
752
  */
266
753
 
267
754
  arguments(desc) {
@@ -276,14 +763,13 @@ class Command extends EventEmitter {
276
763
  * addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details
277
764
  *
278
765
  * @return {Command} `this` command for chaining
279
- * @api public
280
766
  */
281
767
 
282
768
  addHelpCommand(enableOrNameAndArgs, description) {
283
769
  if (enableOrNameAndArgs === false) {
284
- this._hasImplicitHelpCommand = false;
770
+ this._addImplicitHelpCommand = false;
285
771
  } else {
286
- this._hasImplicitHelpCommand = true;
772
+ this._addImplicitHelpCommand = true;
287
773
  if (typeof enableOrNameAndArgs === 'string') {
288
774
  this._helpCommandName = enableOrNameAndArgs.split(' ')[0];
289
775
  this._helpCommandnameAndArgs = enableOrNameAndArgs;
@@ -298,11 +784,11 @@ class Command extends EventEmitter {
298
784
  * @api private
299
785
  */
300
786
 
301
- _lazyHasImplicitHelpCommand() {
302
- if (this._hasImplicitHelpCommand === undefined) {
303
- this._hasImplicitHelpCommand = this.commands.length && !this._actionHandler && !this._findCommand('help');
787
+ _hasImplicitHelpCommand() {
788
+ if (this._addImplicitHelpCommand === undefined) {
789
+ return this.commands.length && !this._actionHandler && !this._findCommand('help');
304
790
  }
305
- return this._hasImplicitHelpCommand;
791
+ return this._addImplicitHelpCommand;
306
792
  };
307
793
 
308
794
  /**
@@ -355,7 +841,6 @@ class Command extends EventEmitter {
355
841
  *
356
842
  * @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing
357
843
  * @return {Command} `this` command for chaining
358
- * @api public
359
844
  */
360
845
 
361
846
  exitOverride(fn) {
@@ -405,7 +890,6 @@ class Command extends EventEmitter {
405
890
  *
406
891
  * @param {Function} fn
407
892
  * @return {Command} `this` command for chaining
408
- * @api public
409
893
  */
410
894
 
411
895
  action(fn) {
@@ -413,15 +897,12 @@ class Command extends EventEmitter {
413
897
  // The .action callback takes an extra parameter which is the command or options.
414
898
  const expectedArgsCount = this._args.length;
415
899
  const actionArgs = args.slice(0, expectedArgsCount);
416
- if (this._passCommandToAction) {
417
- actionArgs[expectedArgsCount] = this;
900
+ if (this._storeOptionsAsProperties) {
901
+ actionArgs[expectedArgsCount] = this; // backwards compatible "options"
418
902
  } else {
419
903
  actionArgs[expectedArgsCount] = this.opts();
420
904
  }
421
- // Add the extra arguments so available too.
422
- if (args.length > expectedArgsCount) {
423
- actionArgs.push(args.slice(expectedArgsCount));
424
- }
905
+ actionArgs.push(this);
425
906
 
426
907
  const actionResult = fn.apply(this, actionArgs);
427
908
  // Remember result in case it is async. Assume parseAsync getting called on root.
@@ -436,83 +917,31 @@ class Command extends EventEmitter {
436
917
  };
437
918
 
438
919
  /**
439
- * Internal routine to check whether there is a clash storing option value with a Command property.
920
+ * Factory routine to create a new unattached option.
440
921
  *
441
- * @param {Option} option
442
- * @api private
922
+ * See .option() for creating an attached option, which uses this routine to
923
+ * create the option. You can override createOption to return a custom option.
924
+ *
925
+ * @param {string} flags
926
+ * @param {string} [description]
927
+ * @return {Option} new option
443
928
  */
444
929
 
445
- _checkForOptionNameClash(option) {
446
- if (!this._storeOptionsAsProperties || this._storeOptionsAsPropertiesCalled) {
447
- // Storing options safely, or user has been explicit and up to them.
448
- return;
449
- }
450
- // User may override help, and hard to tell if worth warning.
451
- if (option.name() === 'help') {
452
- return;
453
- }
454
-
455
- const commandProperty = this._getOptionValue(option.attributeName());
456
- if (commandProperty === undefined) {
457
- // no clash
458
- return;
459
- }
460
-
461
- let foundClash = true;
462
- if (option.negate) {
463
- // It is ok if define foo before --no-foo.
464
- const positiveLongFlag = option.long.replace(/^--no-/, '--');
465
- foundClash = !this._findOption(positiveLongFlag);
466
- } else if (option.long) {
467
- const negativeLongFlag = option.long.replace(/^--/, '--no-');
468
- foundClash = !this._findOption(negativeLongFlag);
469
- }
470
-
471
- if (foundClash) {
472
- throw new Error(`option '${option.name()}' clashes with existing property '${option.attributeName()}' on Command
473
- - call storeOptionsAsProperties(false) to store option values safely,
474
- - or call storeOptionsAsProperties(true) to suppress this check,
475
- - or change option name
476
-
477
- Read more on https://git.io/JJc0W`);
478
- }
930
+ createOption(flags, description) {
931
+ return new Option(flags, description);
479
932
  };
480
933
 
481
934
  /**
482
- * Internal implementation shared by .option() and .requiredOption()
935
+ * Add an option.
483
936
  *
484
- * @param {Object} config
485
- * @param {string} flags
486
- * @param {string} description
487
- * @param {Function|*} [fn] - custom option processing function or default value
488
- * @param {*} [defaultValue]
937
+ * @param {Option} option
489
938
  * @return {Command} `this` command for chaining
490
- * @api private
491
939
  */
492
-
493
- _optionEx(config, flags, description, fn, defaultValue) {
494
- const option = new Option(flags, description);
940
+ addOption(option) {
495
941
  const oname = option.name();
496
942
  const name = option.attributeName();
497
- option.mandatory = !!config.mandatory;
498
-
499
- this._checkForOptionNameClash(option);
500
-
501
- // default as 3rd arg
502
- if (typeof fn !== 'function') {
503
- if (fn instanceof RegExp) {
504
- // This is a bit simplistic (especially no error messages), and probably better handled by caller using custom option processing.
505
- // No longer documented in README, but still present for backwards compatibility.
506
- const regex = fn;
507
- fn = (val, def) => {
508
- const m = regex.exec(val);
509
- return m ? m[0] : def;
510
- };
511
- } else {
512
- defaultValue = fn;
513
- fn = null;
514
- }
515
- }
943
+
944
+ let defaultValue = option.defaultValue;
516
945
 
517
946
  // preassign default value for --no-*, [optional], <required>, or plain flag if boolean value
518
947
  if (option.negate || option.optional || option.required || typeof defaultValue === 'boolean') {
@@ -524,7 +953,6 @@ Read more on https://git.io/JJc0W`);
524
953
  // preassign only if we have a default
525
954
  if (defaultValue !== undefined) {
526
955
  this._setOptionValue(name, defaultValue);
527
- option.defaultValue = defaultValue;
528
956
  }
529
957
  }
530
958
 
@@ -537,8 +965,16 @@ Read more on https://git.io/JJc0W`);
537
965
  const oldValue = this._getOptionValue(name);
538
966
 
539
967
  // custom processing
540
- if (val !== null && fn) {
541
- val = fn(val, oldValue === undefined ? defaultValue : oldValue);
968
+ if (val !== null && option.parseArg) {
969
+ try {
970
+ val = option.parseArg(val, oldValue === undefined ? defaultValue : oldValue);
971
+ } catch (err) {
972
+ if (err.code === 'commander.invalidOptionArgument') {
973
+ const message = `error: option '${option.flags}' argument '${val}' is invalid. ${err.message}`;
974
+ this._displayError(err.exitCode, err.code, message);
975
+ }
976
+ throw err;
977
+ }
542
978
  } else if (val !== null && option.variadic) {
543
979
  if (oldValue === defaultValue || !Array.isArray(oldValue)) {
544
980
  val = [val];
@@ -564,13 +1000,38 @@ Read more on https://git.io/JJc0W`);
564
1000
  });
565
1001
 
566
1002
  return this;
567
- };
1003
+ }
1004
+
1005
+ /**
1006
+ * Internal implementation shared by .option() and .requiredOption()
1007
+ *
1008
+ * @api private
1009
+ */
1010
+ _optionEx(config, flags, description, fn, defaultValue) {
1011
+ const option = this.createOption(flags, description);
1012
+ option.makeOptionMandatory(!!config.mandatory);
1013
+ if (typeof fn === 'function') {
1014
+ option.default(defaultValue).argParser(fn);
1015
+ } else if (fn instanceof RegExp) {
1016
+ // deprecated
1017
+ const regex = fn;
1018
+ fn = (val, def) => {
1019
+ const m = regex.exec(val);
1020
+ return m ? m[0] : def;
1021
+ };
1022
+ option.default(defaultValue).argParser(fn);
1023
+ } else {
1024
+ option.default(fn);
1025
+ }
1026
+
1027
+ return this.addOption(option);
1028
+ }
568
1029
 
569
1030
  /**
570
1031
  * Define option with `flags`, `description` and optional
571
1032
  * coercion `fn`.
572
1033
  *
573
- * The `flags` string should contain both the short and long flags,
1034
+ * The `flags` string contains the short and/or long flags,
574
1035
  * separated by comma, a pipe or space. The following are all valid
575
1036
  * all will output this way when `--help` is used.
576
1037
  *
@@ -611,11 +1072,10 @@ Read more on https://git.io/JJc0W`);
611
1072
  * program.option('-c, --cheese [type]', 'add cheese [marble]');
612
1073
  *
613
1074
  * @param {string} flags
614
- * @param {string} description
1075
+ * @param {string} [description]
615
1076
  * @param {Function|*} [fn] - custom option processing function or default value
616
1077
  * @param {*} [defaultValue]
617
1078
  * @return {Command} `this` command for chaining
618
- * @api public
619
1079
  */
620
1080
 
621
1081
  option(flags, description, fn, defaultValue) {
@@ -626,14 +1086,13 @@ Read more on https://git.io/JJc0W`);
626
1086
  * Add a required option which must have a value after parsing. This usually means
627
1087
  * the option must be specified on the command line. (Otherwise the same as .option().)
628
1088
  *
629
- * The `flags` string should contain both the short and long flags, separated by comma, a pipe or space.
1089
+ * The `flags` string contains the short and/or long flags, separated by comma, a pipe or space.
630
1090
  *
631
1091
  * @param {string} flags
632
- * @param {string} description
1092
+ * @param {string} [description]
633
1093
  * @param {Function|*} [fn] - custom option processing function or default value
634
1094
  * @param {*} [defaultValue]
635
1095
  * @return {Command} `this` command for chaining
636
- * @api public
637
1096
  */
638
1097
 
639
1098
  requiredOption(flags, description, fn, defaultValue) {
@@ -649,55 +1108,77 @@ Read more on https://git.io/JJc0W`);
649
1108
  * .combineFlagAndOptionalValue(true) // `-f80` is treated like `--flag=80`, this is the default behaviour
650
1109
  * .combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b`
651
1110
  *
652
- * @param {Boolean} [arg] - if `true` or omitted, an optional value can be specified directly after the flag.
653
- * @api public
1111
+ * @param {Boolean} [combine=true] - if `true` or omitted, an optional value can be specified directly after the flag.
654
1112
  */
655
- combineFlagAndOptionalValue(arg) {
656
- this._combineFlagAndOptionalValue = (arg === undefined) || arg;
1113
+ combineFlagAndOptionalValue(combine = true) {
1114
+ this._combineFlagAndOptionalValue = !!combine;
657
1115
  return this;
658
1116
  };
659
1117
 
660
1118
  /**
661
1119
  * Allow unknown options on the command line.
662
1120
  *
663
- * @param {Boolean} [arg] - if `true` or omitted, no error will be thrown
1121
+ * @param {Boolean} [allowUnknown=true] - if `true` or omitted, no error will be thrown
664
1122
  * for unknown options.
665
- * @api public
666
1123
  */
667
- allowUnknownOption(arg) {
668
- this._allowUnknownOption = (arg === undefined) || arg;
1124
+ allowUnknownOption(allowUnknown = true) {
1125
+ this._allowUnknownOption = !!allowUnknown;
669
1126
  return this;
670
1127
  };
671
1128
 
672
1129
  /**
673
- * Whether to store option values as properties on command object,
674
- * or store separately (specify false). In both cases the option values can be accessed using .opts().
675
- *
676
- * @param {boolean} value
677
- * @return {Command} `this` command for chaining
678
- * @api public
679
- */
1130
+ * Allow excess command-arguments on the command line. Pass false to make excess arguments an error.
1131
+ *
1132
+ * @param {Boolean} [allowExcess=true] - if `true` or omitted, no error will be thrown
1133
+ * for excess arguments.
1134
+ */
1135
+ allowExcessArguments(allowExcess = true) {
1136
+ this._allowExcessArguments = !!allowExcess;
1137
+ return this;
1138
+ };
680
1139
 
681
- storeOptionsAsProperties(value) {
682
- this._storeOptionsAsPropertiesCalled = true;
683
- this._storeOptionsAsProperties = (value === undefined) || value;
684
- if (this.options.length) {
685
- throw new Error('call .storeOptionsAsProperties() before adding options');
1140
+ /**
1141
+ * Enable positional options. Positional means global options are specified before subcommands which lets
1142
+ * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions.
1143
+ * The default behaviour is non-positional and global options may appear anywhere on the command line.
1144
+ *
1145
+ * @param {Boolean} [positional=true]
1146
+ */
1147
+ enablePositionalOptions(positional = true) {
1148
+ this._enablePositionalOptions = !!positional;
1149
+ return this;
1150
+ };
1151
+
1152
+ /**
1153
+ * Pass through options that come after command-arguments rather than treat them as command-options,
1154
+ * so actual command-options come before command-arguments. Turning this on for a subcommand requires
1155
+ * positional options to have been enabled on the program (parent commands).
1156
+ * The default behaviour is non-positional and options may appear before or after command-arguments.
1157
+ *
1158
+ * @param {Boolean} [passThrough=true]
1159
+ * for unknown options.
1160
+ */
1161
+ passThroughOptions(passThrough = true) {
1162
+ this._passThroughOptions = !!passThrough;
1163
+ if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) {
1164
+ throw new Error('passThroughOptions can not be used without turning on enablePositionOptions for parent command(s)');
686
1165
  }
687
1166
  return this;
688
1167
  };
689
1168
 
690
1169
  /**
691
- * Whether to pass command to action handler,
692
- * or just the options (specify false).
1170
+ * Whether to store option values as properties on command object,
1171
+ * or store separately (specify false). In both cases the option values can be accessed using .opts().
693
1172
  *
694
- * @param {boolean} value
1173
+ * @param {boolean} [storeAsProperties=true]
695
1174
  * @return {Command} `this` command for chaining
696
- * @api public
697
1175
  */
698
1176
 
699
- passCommandToAction(value) {
700
- this._passCommandToAction = (value === undefined) || value;
1177
+ storeOptionsAsProperties(storeAsProperties = true) {
1178
+ this._storeOptionsAsProperties = !!storeAsProperties;
1179
+ if (this.options.length) {
1180
+ throw new Error('call .storeOptionsAsProperties() before adding options');
1181
+ }
701
1182
  return this;
702
1183
  };
703
1184
 
@@ -748,7 +1229,6 @@ Read more on https://git.io/JJc0W`);
748
1229
  * @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron
749
1230
  * @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
750
1231
  * @return {Command} `this` command for chaining
751
- * @api public
752
1232
  */
753
1233
 
754
1234
  parse(argv, parseOptions) {
@@ -760,7 +1240,7 @@ Read more on https://git.io/JJc0W`);
760
1240
  // Default to using process.argv
761
1241
  if (argv === undefined) {
762
1242
  argv = process.argv;
763
- // @ts-ignore
1243
+ // @ts-ignore: unknown property
764
1244
  if (process.versions && process.versions.electron) {
765
1245
  parseOptions.from = 'electron';
766
1246
  }
@@ -776,7 +1256,7 @@ Read more on https://git.io/JJc0W`);
776
1256
  userArgs = argv.slice(2);
777
1257
  break;
778
1258
  case 'electron':
779
- // @ts-ignore
1259
+ // @ts-ignore: unknown property
780
1260
  if (process.defaultApp) {
781
1261
  this._scriptPath = argv[1];
782
1262
  userArgs = argv.slice(2);
@@ -790,7 +1270,9 @@ Read more on https://git.io/JJc0W`);
790
1270
  default:
791
1271
  throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`);
792
1272
  }
1273
+ // @ts-ignore: unknown property
793
1274
  if (!this._scriptPath && process.mainModule) {
1275
+ // @ts-ignore: unknown property
794
1276
  this._scriptPath = process.mainModule.filename;
795
1277
  }
796
1278
 
@@ -821,7 +1303,6 @@ Read more on https://git.io/JJc0W`);
821
1303
  * @param {Object} [parseOptions]
822
1304
  * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron'
823
1305
  * @return {Promise}
824
- * @api public
825
1306
  */
826
1307
 
827
1308
  parseAsync(argv, parseOptions) {
@@ -846,7 +1327,9 @@ Read more on https://git.io/JJc0W`);
846
1327
  // Want the entry script as the reference for command name and directory for searching for other files.
847
1328
  let scriptPath = this._scriptPath;
848
1329
  // Fallback in case not set, due to how Command created or called.
1330
+ // @ts-ignore: unknown property
849
1331
  if (!scriptPath && process.mainModule) {
1332
+ // @ts-ignore: unknown property
850
1333
  scriptPath = process.mainModule.filename;
851
1334
  }
852
1335
 
@@ -885,15 +1368,15 @@ Read more on https://git.io/JJc0W`);
885
1368
  // add executable arguments to spawn
886
1369
  args = incrementNodeInspectorPort(process.execArgv).concat(args);
887
1370
 
888
- proc = spawn(process.argv[0], args, { stdio: 'inherit' });
1371
+ proc = childProcess.spawn(process.argv[0], args, { stdio: 'inherit' });
889
1372
  } else {
890
- proc = spawn(bin, args, { stdio: 'inherit' });
1373
+ proc = childProcess.spawn(bin, args, { stdio: 'inherit' });
891
1374
  }
892
1375
  } else {
893
1376
  args.unshift(bin);
894
1377
  // add executable arguments to spawn
895
1378
  args = incrementNodeInspectorPort(process.execArgv).concat(args);
896
- proc = spawn(process.execPath, args, { stdio: 'inherit' });
1379
+ proc = childProcess.spawn(process.execPath, args, { stdio: 'inherit' });
897
1380
  }
898
1381
 
899
1382
  const signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP'];
@@ -945,7 +1428,7 @@ Read more on https://git.io/JJc0W`);
945
1428
  */
946
1429
  _dispatchSubcommand(commandName, operands, unknown) {
947
1430
  const subCommand = this._findCommand(commandName);
948
- if (!subCommand) this._helpAndError();
1431
+ if (!subCommand) this.help({ error: true });
949
1432
 
950
1433
  if (subCommand._executableHandler) {
951
1434
  this._executeSubCommand(subCommand, operands.concat(unknown));
@@ -968,7 +1451,7 @@ Read more on https://git.io/JJc0W`);
968
1451
 
969
1452
  if (operands && this._findCommand(operands[0])) {
970
1453
  this._dispatchSubcommand(operands[0], operands.slice(1), unknown);
971
- } else if (this._lazyHasImplicitHelpCommand() && operands[0] === this._helpCommandName) {
1454
+ } else if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) {
972
1455
  if (operands.length === 1) {
973
1456
  this.help();
974
1457
  } else {
@@ -980,7 +1463,7 @@ Read more on https://git.io/JJc0W`);
980
1463
  } else {
981
1464
  if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) {
982
1465
  // probably missing subcommand and no handler, user needs help
983
- this._helpAndError();
1466
+ this.help({ error: true });
984
1467
  }
985
1468
 
986
1469
  outputHelpIfRequested(this, parsed.unknown);
@@ -989,20 +1472,28 @@ Read more on https://git.io/JJc0W`);
989
1472
  this.unknownOption(parsed.unknown[0]);
990
1473
  }
991
1474
 
1475
+ const commandEvent = `command:${this.name()}`;
992
1476
  if (this._actionHandler) {
1477
+ // Check expected arguments and collect variadic together.
993
1478
  const args = this.args.slice();
994
1479
  this._args.forEach((arg, i) => {
995
1480
  if (arg.required && args[i] == null) {
996
1481
  this.missingArgument(arg.name);
997
1482
  } else if (arg.variadic) {
998
1483
  args[i] = args.splice(i);
1484
+ args.length = Math.min(i + 1, args.length);
999
1485
  }
1000
1486
  });
1487
+ if (args.length > this._args.length) {
1488
+ this._excessArguments(args);
1489
+ }
1001
1490
 
1002
1491
  this._actionHandler(args);
1003
- this.emit('command:' + this.name(), operands, unknown);
1492
+ if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy
1493
+ } else if (this.parent && this.parent.listenerCount(commandEvent)) {
1494
+ this.parent.emit(commandEvent, operands, unknown); // legacy
1004
1495
  } else if (operands.length) {
1005
- if (this._findCommand('*')) {
1496
+ if (this._findCommand('*')) { // legacy
1006
1497
  this._dispatchSubcommand('*', operands, unknown);
1007
1498
  } else if (this.listenerCount('command:*')) {
1008
1499
  this.emit('command:*', operands, unknown);
@@ -1011,7 +1502,7 @@ Read more on https://git.io/JJc0W`);
1011
1502
  }
1012
1503
  } else if (this.commands.length) {
1013
1504
  // This command has subcommands and nothing hooked up at this level, so display help.
1014
- this._helpAndError();
1505
+ this.help({ error: true });
1015
1506
  } else {
1016
1507
  // fall through for caller to handle after calling .parse()
1017
1508
  }
@@ -1072,7 +1563,6 @@ Read more on https://git.io/JJc0W`);
1072
1563
  *
1073
1564
  * @param {String[]} argv
1074
1565
  * @return {{operands: String[], unknown: String[]}}
1075
- * @api public
1076
1566
  */
1077
1567
 
1078
1568
  parseOptions(argv) {
@@ -1152,11 +1642,38 @@ Read more on https://git.io/JJc0W`);
1152
1642
  }
1153
1643
  }
1154
1644
 
1155
- // looks like an option but unknown, unknowns from here
1156
- if (arg.length > 1 && arg[0] === '-') {
1645
+ // Not a recognised option by this command.
1646
+ // Might be a command-argument, or subcommand option, or unknown option, or help command or option.
1647
+
1648
+ // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands.
1649
+ if (maybeOption(arg)) {
1157
1650
  dest = unknown;
1158
1651
  }
1159
1652
 
1653
+ // If using positionalOptions, stop processing our options at subcommand.
1654
+ if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) {
1655
+ if (this._findCommand(arg)) {
1656
+ operands.push(arg);
1657
+ if (args.length > 0) unknown.push(...args);
1658
+ break;
1659
+ } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) {
1660
+ operands.push(arg);
1661
+ if (args.length > 0) operands.push(...args);
1662
+ break;
1663
+ } else if (this._defaultCommandName) {
1664
+ unknown.push(arg);
1665
+ if (args.length > 0) unknown.push(...args);
1666
+ break;
1667
+ }
1668
+ }
1669
+
1670
+ // If using passThroughOptions, stop processing options at first command-argument.
1671
+ if (this._passThroughOptions) {
1672
+ dest.push(arg);
1673
+ if (args.length > 0) dest.push(...args);
1674
+ break;
1675
+ }
1676
+
1160
1677
  // add arg
1161
1678
  dest.push(arg);
1162
1679
  }
@@ -1168,7 +1685,6 @@ Read more on https://git.io/JJc0W`);
1168
1685
  * Return an object containing options as key-value pairs
1169
1686
  *
1170
1687
  * @return {Object}
1171
- * @api public
1172
1688
  */
1173
1689
  opts() {
1174
1690
  if (this._storeOptionsAsProperties) {
@@ -1186,6 +1702,16 @@ Read more on https://git.io/JJc0W`);
1186
1702
  return this._optionValues;
1187
1703
  };
1188
1704
 
1705
+ /**
1706
+ * Internal bottleneck for handling of parsing errors.
1707
+ *
1708
+ * @api private
1709
+ */
1710
+ _displayError(exitCode, code, message) {
1711
+ this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr);
1712
+ this._exit(exitCode, code, message);
1713
+ }
1714
+
1189
1715
  /**
1190
1716
  * Argument `name` is missing.
1191
1717
  *
@@ -1195,27 +1721,19 @@ Read more on https://git.io/JJc0W`);
1195
1721
 
1196
1722
  missingArgument(name) {
1197
1723
  const message = `error: missing required argument '${name}'`;
1198
- console.error(message);
1199
- this._exit(1, 'commander.missingArgument', message);
1724
+ this._displayError(1, 'commander.missingArgument', message);
1200
1725
  };
1201
1726
 
1202
1727
  /**
1203
- * `Option` is missing an argument, but received `flag` or nothing.
1728
+ * `Option` is missing an argument.
1204
1729
  *
1205
1730
  * @param {Option} option
1206
- * @param {string} [flag]
1207
1731
  * @api private
1208
1732
  */
1209
1733
 
1210
- optionMissingArgument(option, flag) {
1211
- let message;
1212
- if (flag) {
1213
- message = `error: option '${option.flags}' argument missing, got '${flag}'`;
1214
- } else {
1215
- message = `error: option '${option.flags}' argument missing`;
1216
- }
1217
- console.error(message);
1218
- this._exit(1, 'commander.optionMissingArgument', message);
1734
+ optionMissingArgument(option) {
1735
+ const message = `error: option '${option.flags}' argument missing`;
1736
+ this._displayError(1, 'commander.optionMissingArgument', message);
1219
1737
  };
1220
1738
 
1221
1739
  /**
@@ -1227,8 +1745,7 @@ Read more on https://git.io/JJc0W`);
1227
1745
 
1228
1746
  missingMandatoryOptionValue(option) {
1229
1747
  const message = `error: required option '${option.flags}' not specified`;
1230
- console.error(message);
1231
- this._exit(1, 'commander.missingMandatoryOptionValue', message);
1748
+ this._displayError(1, 'commander.missingMandatoryOptionValue', message);
1232
1749
  };
1233
1750
 
1234
1751
  /**
@@ -1241,8 +1758,24 @@ Read more on https://git.io/JJc0W`);
1241
1758
  unknownOption(flag) {
1242
1759
  if (this._allowUnknownOption) return;
1243
1760
  const message = `error: unknown option '${flag}'`;
1244
- console.error(message);
1245
- this._exit(1, 'commander.unknownOption', message);
1761
+ this._displayError(1, 'commander.unknownOption', message);
1762
+ };
1763
+
1764
+ /**
1765
+ * Excess arguments, more than expected.
1766
+ *
1767
+ * @param {string[]} receivedArgs
1768
+ * @api private
1769
+ */
1770
+
1771
+ _excessArguments(receivedArgs) {
1772
+ if (this._allowExcessArguments) return;
1773
+
1774
+ const expected = this._args.length;
1775
+ const s = (expected === 1) ? '' : 's';
1776
+ const forSubcommand = this.parent ? ` for '${this.name()}'` : '';
1777
+ const message = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`;
1778
+ this._displayError(1, 'commander.excessArguments', message);
1246
1779
  };
1247
1780
 
1248
1781
  /**
@@ -1259,8 +1792,7 @@ Read more on https://git.io/JJc0W`);
1259
1792
  const fullCommand = partCommands.join(' ');
1260
1793
  const message = `error: unknown command '${this.args[0]}'.` +
1261
1794
  (this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : '');
1262
- console.error(message);
1263
- this._exit(1, 'commander.unknownCommand', message);
1795
+ this._displayError(1, 'commander.unknownCommand', message);
1264
1796
  };
1265
1797
 
1266
1798
  /**
@@ -1275,7 +1807,6 @@ Read more on https://git.io/JJc0W`);
1275
1807
  * @param {string} [flags]
1276
1808
  * @param {string} [description]
1277
1809
  * @return {this | string} `this` command for chaining, or version string if no arguments
1278
- * @api public
1279
1810
  */
1280
1811
 
1281
1812
  version(str, flags, description) {
@@ -1283,11 +1814,11 @@ Read more on https://git.io/JJc0W`);
1283
1814
  this._version = str;
1284
1815
  flags = flags || '-V, --version';
1285
1816
  description = description || 'output the version number';
1286
- const versionOption = new Option(flags, description);
1817
+ const versionOption = this.createOption(flags, description);
1287
1818
  this._versionOptionName = versionOption.attributeName();
1288
1819
  this.options.push(versionOption);
1289
1820
  this.on('option:' + versionOption.name(), () => {
1290
- process.stdout.write(str + '\n');
1821
+ this._outputConfiguration.writeOut(`${str}\n`);
1291
1822
  this._exit(0, 'commander.version', str);
1292
1823
  });
1293
1824
  return this;
@@ -1296,12 +1827,10 @@ Read more on https://git.io/JJc0W`);
1296
1827
  /**
1297
1828
  * Set the description to `str`.
1298
1829
  *
1299
- * @param {string} str
1830
+ * @param {string} [str]
1300
1831
  * @param {Object} [argsDescription]
1301
1832
  * @return {string|Command}
1302
- * @api public
1303
1833
  */
1304
-
1305
1834
  description(str, argsDescription) {
1306
1835
  if (str === undefined && argsDescription === undefined) return this._description;
1307
1836
  this._description = str;
@@ -1316,7 +1845,6 @@ Read more on https://git.io/JJc0W`);
1316
1845
  *
1317
1846
  * @param {string} [alias]
1318
1847
  * @return {string|Command}
1319
- * @api public
1320
1848
  */
1321
1849
 
1322
1850
  alias(alias) {
@@ -1341,7 +1869,6 @@ Read more on https://git.io/JJc0W`);
1341
1869
  *
1342
1870
  * @param {string[]} [aliases]
1343
1871
  * @return {string[]|Command}
1344
- * @api public
1345
1872
  */
1346
1873
 
1347
1874
  aliases(aliases) {
@@ -1357,7 +1884,6 @@ Read more on https://git.io/JJc0W`);
1357
1884
  *
1358
1885
  * @param {string} [str]
1359
1886
  * @return {String|Command}
1360
- * @api public
1361
1887
  */
1362
1888
 
1363
1889
  usage(str) {
@@ -1382,8 +1908,7 @@ Read more on https://git.io/JJc0W`);
1382
1908
  * Get or set the name of the command
1383
1909
  *
1384
1910
  * @param {string} [str]
1385
- * @return {String|Command}
1386
- * @api public
1911
+ * @return {string|Command}
1387
1912
  */
1388
1913
 
1389
1914
  name(str) {
@@ -1393,250 +1918,76 @@ Read more on https://git.io/JJc0W`);
1393
1918
  };
1394
1919
 
1395
1920
  /**
1396
- * Return prepared commands.
1921
+ * Return program help documentation.
1397
1922
  *
1398
- * @return {Array}
1399
- * @api private
1923
+ * @param {{ error: boolean }} [contextOptions] - pass {error:true} to wrap for stderr instead of stdout
1924
+ * @return {string}
1400
1925
  */
1401
1926
 
1402
- prepareCommands() {
1403
- const commandDetails = this.commands.filter((cmd) => {
1404
- return !cmd._hidden;
1405
- }).map((cmd) => {
1406
- const args = cmd._args.map((arg) => {
1407
- return humanReadableArgName(arg);
1408
- }).join(' ');
1409
-
1410
- return [
1411
- cmd._name +
1412
- (cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
1413
- (cmd.options.length ? ' [options]' : '') +
1414
- (args ? ' ' + args : ''),
1415
- cmd._description
1416
- ];
1417
- });
1418
-
1419
- if (this._lazyHasImplicitHelpCommand()) {
1420
- commandDetails.push([this._helpCommandnameAndArgs, this._helpCommandDescription]);
1927
+ helpInformation(contextOptions) {
1928
+ const helper = this.createHelp();
1929
+ if (helper.helpWidth === undefined) {
1930
+ helper.helpWidth = (contextOptions && contextOptions.error) ? this._outputConfiguration.getErrHelpWidth() : this._outputConfiguration.getOutHelpWidth();
1421
1931
  }
1422
- return commandDetails;
1932
+ return helper.formatHelp(this, helper);
1423
1933
  };
1424
1934
 
1425
1935
  /**
1426
- * Return the largest command length.
1427
- *
1428
- * @return {number}
1429
1936
  * @api private
1430
1937
  */
1431
1938
 
1432
- largestCommandLength() {
1433
- const commands = this.prepareCommands();
1434
- return commands.reduce((max, command) => {
1435
- return Math.max(max, command[0].length);
1436
- }, 0);
1437
- };
1438
-
1439
- /**
1440
- * Return the largest option length.
1441
- *
1442
- * @return {number}
1443
- * @api private
1444
- */
1445
-
1446
- largestOptionLength() {
1447
- const options = [].slice.call(this.options);
1448
- options.push({
1449
- flags: this._helpFlags
1450
- });
1451
-
1452
- return options.reduce((max, option) => {
1453
- return Math.max(max, option.flags.length);
1454
- }, 0);
1455
- };
1939
+ _getHelpContext(contextOptions) {
1940
+ contextOptions = contextOptions || {};
1941
+ const context = { error: !!contextOptions.error };
1942
+ let write;
1943
+ if (context.error) {
1944
+ write = (arg) => this._outputConfiguration.writeErr(arg);
1945
+ } else {
1946
+ write = (arg) => this._outputConfiguration.writeOut(arg);
1947
+ }
1948
+ context.write = contextOptions.write || write;
1949
+ context.command = this;
1950
+ return context;
1951
+ }
1456
1952
 
1457
1953
  /**
1458
- * Return the largest arg length.
1954
+ * Output help information for this command.
1459
1955
  *
1460
- * @return {number}
1461
- * @api private
1462
- */
1463
-
1464
- largestArgLength() {
1465
- return this._args.reduce((max, arg) => {
1466
- return Math.max(max, arg.name.length);
1467
- }, 0);
1468
- };
1469
-
1470
- /**
1471
- * Return the pad width.
1956
+ * Outputs built-in help, and custom text added using `.addHelpText()`.
1472
1957
  *
1473
- * @return {number}
1474
- * @api private
1958
+ * @param {{ error: boolean } | Function} [contextOptions] - pass {error:true} to write to stderr instead of stdout
1475
1959
  */
1476
1960
 
1477
- padWidth() {
1478
- let width = this.largestOptionLength();
1479
- if (this._argsDescription && this._args.length) {
1480
- if (this.largestArgLength() > width) {
1481
- width = this.largestArgLength();
1482
- }
1483
- }
1484
-
1485
- if (this.commands && this.commands.length) {
1486
- if (this.largestCommandLength() > width) {
1487
- width = this.largestCommandLength();
1488
- }
1961
+ outputHelp(contextOptions) {
1962
+ let deprecatedCallback;
1963
+ if (typeof contextOptions === 'function') {
1964
+ deprecatedCallback = contextOptions;
1965
+ contextOptions = undefined;
1489
1966
  }
1967
+ const context = this._getHelpContext(contextOptions);
1490
1968
 
1491
- return width;
1492
- };
1493
-
1494
- /**
1495
- * Return help for options.
1496
- *
1497
- * @return {string}
1498
- * @api private
1499
- */
1500
-
1501
- optionHelp() {
1502
- const width = this.padWidth();
1503
- const columns = process.stdout.columns || 80;
1504
- const descriptionWidth = columns - width - 4;
1505
- function padOptionDetails(flags, description) {
1506
- return pad(flags, width) + ' ' + optionalWrap(description, descriptionWidth, width + 2);
1507
- };
1508
-
1509
- // Explicit options (including version)
1510
- const help = this.options.map((option) => {
1511
- const fullDesc = option.description +
1512
- ((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : '');
1513
- return padOptionDetails(option.flags, fullDesc);
1514
- });
1515
-
1516
- // Implicit help
1517
- const showShortHelpFlag = this._hasHelpOption && this._helpShortFlag && !this._findOption(this._helpShortFlag);
1518
- const showLongHelpFlag = this._hasHelpOption && !this._findOption(this._helpLongFlag);
1519
- if (showShortHelpFlag || showLongHelpFlag) {
1520
- let helpFlags = this._helpFlags;
1521
- if (!showShortHelpFlag) {
1522
- helpFlags = this._helpLongFlag;
1523
- } else if (!showLongHelpFlag) {
1524
- helpFlags = this._helpShortFlag;
1525
- }
1526
- help.push(padOptionDetails(helpFlags, this._helpDescription));
1969
+ const groupListeners = [];
1970
+ let command = this;
1971
+ while (command) {
1972
+ groupListeners.push(command); // ordered from current command to root
1973
+ command = command.parent;
1527
1974
  }
1528
1975
 
1529
- return help.join('\n');
1530
- };
1531
-
1532
- /**
1533
- * Return command help documentation.
1534
- *
1535
- * @return {string}
1536
- * @api private
1537
- */
1538
-
1539
- commandHelp() {
1540
- if (!this.commands.length && !this._lazyHasImplicitHelpCommand()) return '';
1541
-
1542
- const commands = this.prepareCommands();
1543
- const width = this.padWidth();
1976
+ groupListeners.slice().reverse().forEach(command => command.emit('beforeAllHelp', context));
1977
+ this.emit('beforeHelp', context);
1544
1978
 
1545
- const columns = process.stdout.columns || 80;
1546
- const descriptionWidth = columns - width - 4;
1547
-
1548
- return [
1549
- 'Commands:',
1550
- commands.map((cmd) => {
1551
- const desc = cmd[1] ? ' ' + cmd[1] : '';
1552
- return (desc ? pad(cmd[0], width) : cmd[0]) + optionalWrap(desc, descriptionWidth, width + 2);
1553
- }).join('\n').replace(/^/gm, ' '),
1554
- ''
1555
- ].join('\n');
1556
- };
1557
-
1558
- /**
1559
- * Return program help documentation.
1560
- *
1561
- * @return {string}
1562
- * @api public
1563
- */
1564
-
1565
- helpInformation() {
1566
- let desc = [];
1567
- if (this._description) {
1568
- desc = [
1569
- this._description,
1570
- ''
1571
- ];
1572
-
1573
- const argsDescription = this._argsDescription;
1574
- if (argsDescription && this._args.length) {
1575
- const width = this.padWidth();
1576
- const columns = process.stdout.columns || 80;
1577
- const descriptionWidth = columns - width - 5;
1578
- desc.push('Arguments:');
1579
- this._args.forEach((arg) => {
1580
- desc.push(' ' + pad(arg.name, width) + ' ' + wrap(argsDescription[arg.name] || '', descriptionWidth, width + 4));
1581
- });
1582
- desc.push('');
1979
+ let helpInformation = this.helpInformation(context);
1980
+ if (deprecatedCallback) {
1981
+ helpInformation = deprecatedCallback(helpInformation);
1982
+ if (typeof helpInformation !== 'string' && !Buffer.isBuffer(helpInformation)) {
1983
+ throw new Error('outputHelp callback must return a string or a Buffer');
1583
1984
  }
1584
1985
  }
1986
+ context.write(helpInformation);
1585
1987
 
1586
- let cmdName = this._name;
1587
- if (this._aliases[0]) {
1588
- cmdName = cmdName + '|' + this._aliases[0];
1589
- }
1590
- let parentCmdNames = '';
1591
- for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {
1592
- parentCmdNames = parentCmd.name() + ' ' + parentCmdNames;
1593
- }
1594
- const usage = [
1595
- 'Usage: ' + parentCmdNames + cmdName + ' ' + this.usage(),
1596
- ''
1597
- ];
1598
-
1599
- let cmds = [];
1600
- const commandHelp = this.commandHelp();
1601
- if (commandHelp) cmds = [commandHelp];
1602
-
1603
- let options = [];
1604
- if (this._hasHelpOption || this.options.length > 0) {
1605
- options = [
1606
- 'Options:',
1607
- '' + this.optionHelp().replace(/^/gm, ' '),
1608
- ''
1609
- ];
1610
- }
1611
-
1612
- return usage
1613
- .concat(desc)
1614
- .concat(options)
1615
- .concat(cmds)
1616
- .join('\n');
1617
- };
1618
-
1619
- /**
1620
- * Output help information for this command.
1621
- *
1622
- * When listener(s) are available for the helpLongFlag
1623
- * those callbacks are invoked.
1624
- *
1625
- * @api public
1626
- */
1627
-
1628
- outputHelp(cb) {
1629
- if (!cb) {
1630
- cb = (passthru) => {
1631
- return passthru;
1632
- };
1633
- }
1634
- const cbOutput = cb(this.helpInformation());
1635
- if (typeof cbOutput !== 'string' && !Buffer.isBuffer(cbOutput)) {
1636
- throw new Error('outputHelp callback must return a string or a Buffer');
1637
- }
1638
- process.stdout.write(cbOutput);
1639
- this.emit(this._helpLongFlag);
1988
+ this.emit(this._helpLongFlag); // deprecated
1989
+ this.emit('afterHelp', context);
1990
+ groupListeners.forEach(command => command.emit('afterAllHelp', context));
1640
1991
  };
1641
1992
 
1642
1993
  /**
@@ -1647,7 +1998,6 @@ Read more on https://git.io/JJc0W`);
1647
1998
  * @param {string | boolean} [flags]
1648
1999
  * @param {string} [description]
1649
2000
  * @return {Command} `this` command for chaining
1650
- * @api public
1651
2001
  */
1652
2002
 
1653
2003
  helpOption(flags, description) {
@@ -1668,28 +2018,52 @@ Read more on https://git.io/JJc0W`);
1668
2018
  /**
1669
2019
  * Output help information and exit.
1670
2020
  *
1671
- * @param {Function} [cb]
1672
- * @api public
2021
+ * Outputs built-in help, and custom text added using `.addHelpText()`.
2022
+ *
2023
+ * @param {{ error: boolean }} [contextOptions] - pass {error:true} to write to stderr instead of stdout
1673
2024
  */
1674
2025
 
1675
- help(cb) {
1676
- this.outputHelp(cb);
1677
- // exitCode: preserving original behaviour which was calling process.exit()
2026
+ help(contextOptions) {
2027
+ this.outputHelp(contextOptions);
2028
+ let exitCode = process.exitCode || 0;
2029
+ if (exitCode === 0 && contextOptions && typeof contextOptions !== 'function' && contextOptions.error) {
2030
+ exitCode = 1;
2031
+ }
1678
2032
  // message: do not have all displayed text available so only passing placeholder.
1679
- this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)');
2033
+ this._exit(exitCode, 'commander.help', '(outputHelp)');
1680
2034
  };
1681
2035
 
1682
2036
  /**
1683
- * Output help information and exit. Display for error situations.
2037
+ * Add additional text to be displayed with the built-in help.
1684
2038
  *
1685
- * @api private
2039
+ * Position is 'before' or 'after' to affect just this command,
2040
+ * and 'beforeAll' or 'afterAll' to affect this command and all its subcommands.
2041
+ *
2042
+ * @param {string} position - before or after built-in help
2043
+ * @param {string | Function} text - string to add, or a function returning a string
2044
+ * @return {Command} `this` command for chaining
1686
2045
  */
1687
-
1688
- _helpAndError() {
1689
- this.outputHelp();
1690
- // message: do not have all displayed text available so only passing placeholder.
1691
- this._exit(1, 'commander.help', '(outputHelp)');
1692
- };
2046
+ addHelpText(position, text) {
2047
+ const allowedValues = ['beforeAll', 'before', 'after', 'afterAll'];
2048
+ if (!allowedValues.includes(position)) {
2049
+ throw new Error(`Unexpected value for position to addHelpText.
2050
+ Expecting one of '${allowedValues.join("', '")}'`);
2051
+ }
2052
+ const helpEvent = `${position}Help`;
2053
+ this.on(helpEvent, (context) => {
2054
+ let helpStr;
2055
+ if (typeof text === 'function') {
2056
+ helpStr = text({ error: context.error, command: context.command });
2057
+ } else {
2058
+ helpStr = text;
2059
+ }
2060
+ // Ignore falsy value when nothing to output.
2061
+ if (helpStr) {
2062
+ context.write(`${helpStr}\n`);
2063
+ }
2064
+ });
2065
+ return this;
2066
+ }
1693
2067
  };
1694
2068
 
1695
2069
  /**
@@ -1706,6 +2080,8 @@ exports.program = exports; // More explicit access to global command.
1706
2080
  exports.Command = Command;
1707
2081
  exports.Option = Option;
1708
2082
  exports.CommanderError = CommanderError;
2083
+ exports.InvalidOptionArgumentError = InvalidOptionArgumentError;
2084
+ exports.Help = Help;
1709
2085
 
1710
2086
  /**
1711
2087
  * Camel-case the given `flag`
@@ -1721,63 +2097,6 @@ function camelcase(flag) {
1721
2097
  });
1722
2098
  }
1723
2099
 
1724
- /**
1725
- * Pad `str` to `width`.
1726
- *
1727
- * @param {string} str
1728
- * @param {number} width
1729
- * @return {string}
1730
- * @api private
1731
- */
1732
-
1733
- function pad(str, width) {
1734
- const len = Math.max(0, width - str.length);
1735
- return str + Array(len + 1).join(' ');
1736
- }
1737
-
1738
- /**
1739
- * Wraps the given string with line breaks at the specified width while breaking
1740
- * words and indenting every but the first line on the left.
1741
- *
1742
- * @param {string} str
1743
- * @param {number} width
1744
- * @param {number} indent
1745
- * @return {string}
1746
- * @api private
1747
- */
1748
- function wrap(str, width, indent) {
1749
- const regex = new RegExp('.{1,' + (width - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g');
1750
- const lines = str.match(regex) || [];
1751
- return lines.map((line, i) => {
1752
- if (line.slice(-1) === '\n') {
1753
- line = line.slice(0, line.length - 1);
1754
- }
1755
- return ((i > 0 && indent) ? Array(indent + 1).join(' ') : '') + line.trimRight();
1756
- }).join('\n');
1757
- }
1758
-
1759
- /**
1760
- * Optionally wrap the given str to a max width of width characters per line
1761
- * while indenting with indent spaces. Do not wrap if insufficient width or
1762
- * string is manually formatted.
1763
- *
1764
- * @param {string} str
1765
- * @param {number} width
1766
- * @param {number} indent
1767
- * @return {string}
1768
- * @api private
1769
- */
1770
- function optionalWrap(str, width, indent) {
1771
- // Detect manually wrapped and indented strings by searching for line breaks
1772
- // followed by multiple spaces/tabs.
1773
- if (str.match(/[\n]\s+/)) return str;
1774
- // Do not wrap to narrow columns (or can end up with a word per line).
1775
- const minWidth = 40;
1776
- if (width < minWidth) return str;
1777
-
1778
- return wrap(str, width, indent);
1779
- }
1780
-
1781
2100
  /**
1782
2101
  * Output help information if help flags specified
1783
2102
  *