commander 8.1.0 → 8.2.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.
package/Readme.md CHANGED
@@ -93,7 +93,6 @@ import { Command } from 'commander';
93
93
  const program = new Command();
94
94
  ```
95
95
 
96
-
97
96
  ## Options
98
97
 
99
98
  Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space or vertical bar ('|').
@@ -308,13 +307,14 @@ program.version('0.0.1', '-v, --vers', 'output the current version');
308
307
  You can add most options using the `.option()` method, but there are some additional features available
309
308
  by constructing an `Option` explicitly for less common cases.
310
309
 
311
- Example file: [options-extra.js](./examples/options-extra.js)
310
+ Example files: [options-extra.js](./examples/options-extra.js), [options-env.js](./examples/options-env.js)
312
311
 
313
312
  ```js
314
313
  program
315
314
  .addOption(new Option('-s, --secret').hideHelp())
316
315
  .addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
317
- .addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']));
316
+ .addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']))
317
+ .addOption(new Option('-p, --port <number>', 'port number').env('PORT'));
318
318
  ```
319
319
 
320
320
  ```bash
@@ -324,10 +324,14 @@ Usage: help [options]
324
324
  Options:
325
325
  -t, --timeout <delay> timeout in seconds (default: one minute)
326
326
  -d, --drink <size> drink cup size (choices: "small", "medium", "large")
327
+ -p, --port <number> port number (env: PORT)
327
328
  -h, --help display help for command
328
329
 
329
330
  $ extra --drink huge
330
331
  error: option '-d, --drink <size>' argument 'huge' is invalid. Allowed choices are small, medium, large.
332
+
333
+ $ PORT=80 extra
334
+ Options: { timeout: 60, port: '80' }
331
335
  ```
332
336
 
333
337
  ### Custom option processing
@@ -688,6 +692,18 @@ error: unknown option '--unknown'
688
692
  (add --help for additional information)
689
693
  ```
690
694
 
695
+ You can also show suggestions after an error for an unknown command or option.
696
+
697
+ ```js
698
+ program.showSuggestionAfterError();
699
+ ```
700
+
701
+ ```sh
702
+ $ pizza --hepl
703
+ error: unknown option '--hepl'
704
+ (Did you mean --help?)
705
+ ```
706
+
691
707
  ### Display help from code
692
708
 
693
709
  `.help()`: display help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status.
@@ -903,7 +919,6 @@ You can modify this behaviour for custom applications. In addition, you can modi
903
919
 
904
920
  Example file: [configure-output.js](./examples/configure-output.js)
905
921
 
906
-
907
922
  ```js
908
923
  function errorColor(str) {
909
924
  // Add ANSI escape codes to display text in red.
package/lib/command.js CHANGED
@@ -7,6 +7,7 @@ const { Argument, humanReadableArgName } = require('./argument.js');
7
7
  const { CommanderError } = require('./error.js');
8
8
  const { Help } = require('./help.js');
9
9
  const { Option, splitOptionFlags } = require('./option.js');
10
+ const { suggestSimilar } = require('./suggestSimilar');
10
11
 
11
12
  // @ts-check
12
13
 
@@ -35,6 +36,7 @@ class Command extends EventEmitter {
35
36
  this._scriptPath = null;
36
37
  this._name = name || '';
37
38
  this._optionValues = {};
39
+ this._optionValueSources = {}; // default < env < cli
38
40
  this._storeOptionsAsProperties = false;
39
41
  this._actionHandler = null;
40
42
  this._executableHandler = false;
@@ -50,6 +52,7 @@ class Command extends EventEmitter {
50
52
  this._lifeCycleHooks = {}; // a hash of arrays
51
53
  /** @type {boolean | string} */
52
54
  this._showHelpAfterError = false;
55
+ this._showSuggestionAfterError = false;
53
56
 
54
57
  // see .configureOutput() for docs
55
58
  this._outputConfiguration = {
@@ -98,6 +101,7 @@ class Command extends EventEmitter {
98
101
  this._allowExcessArguments = sourceCommand._allowExcessArguments;
99
102
  this._enablePositionalOptions = sourceCommand._enablePositionalOptions;
100
103
  this._showHelpAfterError = sourceCommand._showHelpAfterError;
104
+ this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError;
101
105
 
102
106
  return this;
103
107
  }
@@ -232,6 +236,17 @@ class Command extends EventEmitter {
232
236
  return this;
233
237
  }
234
238
 
239
+ /**
240
+ * Display suggestion of similar commands for unknown commands, or options for unknown options.
241
+ *
242
+ * @param {boolean} [displaySuggestion]
243
+ * @return {Command} `this` command for chaining
244
+ */
245
+ showSuggestionAfterError(displaySuggestion = true) {
246
+ this._showSuggestionAfterError = !!displaySuggestion;
247
+ return this;
248
+ }
249
+
235
250
  /**
236
251
  * Add a prepared subcommand.
237
252
  *
@@ -512,16 +527,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
512
527
  }
513
528
  // preassign only if we have a default
514
529
  if (defaultValue !== undefined) {
515
- this.setOptionValue(name, defaultValue);
530
+ this._setOptionValueWithSource(name, defaultValue, 'default');
516
531
  }
517
532
  }
518
533
 
519
534
  // register the option
520
535
  this.options.push(option);
521
536
 
522
- // when it's passed assign the value
523
- // and conditionally invoke the callback
524
- this.on('option:' + oname, (val) => {
537
+ // handler for cli and env supplied values
538
+ const handleOptionValue = (val, invalidValueMessage, valueSource) => {
539
+ // Note: using closure to access lots of lexical scoped variables.
525
540
  const oldValue = this.getOptionValue(name);
526
541
 
527
542
  // custom processing
@@ -530,7 +545,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
530
545
  val = option.parseArg(val, oldValue === undefined ? defaultValue : oldValue);
531
546
  } catch (err) {
532
547
  if (err.code === 'commander.invalidArgument') {
533
- const message = `error: option '${option.flags}' argument '${val}' is invalid. ${err.message}`;
548
+ const message = `${invalidValueMessage} ${err.message}`;
534
549
  this._displayError(err.exitCode, err.code, message);
535
550
  }
536
551
  throw err;
@@ -543,18 +558,28 @@ Expecting one of '${allowedValues.join("', '")}'`);
543
558
  if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') {
544
559
  // if no value, negate false, and we have a default, then use it!
545
560
  if (val == null) {
546
- this.setOptionValue(name, option.negate
547
- ? false
548
- : defaultValue || true);
561
+ this._setOptionValueWithSource(name, option.negate ? false : defaultValue || true, valueSource);
549
562
  } else {
550
- this.setOptionValue(name, val);
563
+ this._setOptionValueWithSource(name, val, valueSource);
551
564
  }
552
565
  } else if (val !== null) {
553
566
  // reassign
554
- this.setOptionValue(name, option.negate ? false : val);
567
+ this._setOptionValueWithSource(name, option.negate ? false : val, valueSource);
555
568
  }
569
+ };
570
+
571
+ this.on('option:' + oname, (val) => {
572
+ const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`;
573
+ handleOptionValue(val, invalidValueMessage, 'cli');
556
574
  });
557
575
 
576
+ if (option.envVar) {
577
+ this.on('optionEnv:' + oname, (val) => {
578
+ const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`;
579
+ handleOptionValue(val, invalidValueMessage, 'env');
580
+ });
581
+ }
582
+
558
583
  return this;
559
584
  }
560
585
 
@@ -767,6 +792,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
767
792
  return this;
768
793
  };
769
794
 
795
+ /**
796
+ * @api private
797
+ */
798
+ _setOptionValueWithSource(key, value, source) {
799
+ this.setOptionValue(key, value);
800
+ this._optionValueSources[key] = source;
801
+ }
802
+
770
803
  /**
771
804
  * Get user arguments implied or explicit arguments.
772
805
  * Side-effects: set _scriptPath if args included application, and use that to set implicit command name.
@@ -1131,6 +1164,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1131
1164
 
1132
1165
  _parseCommand(operands, unknown) {
1133
1166
  const parsed = this.parseOptions(unknown);
1167
+ this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env
1134
1168
  operands = operands.concat(parsed.operands);
1135
1169
  unknown = parsed.unknown;
1136
1170
  this.args = operands.concat(unknown);
@@ -1193,6 +1227,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1193
1227
  this._processArguments();
1194
1228
  }
1195
1229
  } else if (this.commands.length) {
1230
+ checkForUnknownOptions();
1196
1231
  // This command has subcommands and nothing hooked up at this level, so display help (and exit).
1197
1232
  this.help({ error: true });
1198
1233
  } else {
@@ -1411,6 +1446,30 @@ Expecting one of '${allowedValues.join("', '")}'`);
1411
1446
  this._exit(exitCode, code, message);
1412
1447
  }
1413
1448
 
1449
+ /**
1450
+ * Apply any option related environment variables, if option does
1451
+ * not have a value from cli or client code.
1452
+ *
1453
+ * @api private
1454
+ */
1455
+ _parseOptionsEnv() {
1456
+ this.options.forEach((option) => {
1457
+ if (option.envVar && option.envVar in process.env) {
1458
+ const optionKey = option.attributeName();
1459
+ // env is second lowest priority source, above default
1460
+ if (this.getOptionValue(optionKey) === undefined || this._optionValueSources[optionKey] === 'default') {
1461
+ if (option.required || option.optional) { // option can take a value
1462
+ // keep very simple, optional always takes value
1463
+ this.emit(`optionEnv:${option.name()}`, process.env[option.envVar]);
1464
+ } else { // boolean
1465
+ // keep very simple, only care that envVar defined and not the value
1466
+ this.emit(`optionEnv:${option.name()}`);
1467
+ }
1468
+ }
1469
+ }
1470
+ });
1471
+ }
1472
+
1414
1473
  /**
1415
1474
  * Argument `name` is missing.
1416
1475
  *
@@ -1456,7 +1515,23 @@ Expecting one of '${allowedValues.join("', '")}'`);
1456
1515
 
1457
1516
  unknownOption(flag) {
1458
1517
  if (this._allowUnknownOption) return;
1459
- const message = `error: unknown option '${flag}'`;
1518
+ let suggestion = '';
1519
+
1520
+ if (flag.startsWith('--') && this._showSuggestionAfterError) {
1521
+ // Looping to pick up the global options too
1522
+ let candidateFlags = [];
1523
+ let command = this;
1524
+ do {
1525
+ const moreFlags = command.createHelp().visibleOptions(command)
1526
+ .filter(option => option.long)
1527
+ .map(option => option.long);
1528
+ candidateFlags = candidateFlags.concat(moreFlags);
1529
+ command = command.parent;
1530
+ } while (command && !command._enablePositionalOptions);
1531
+ suggestion = suggestSimilar(flag, candidateFlags);
1532
+ }
1533
+
1534
+ const message = `error: unknown option '${flag}'${suggestion}`;
1460
1535
  this._displayError(1, 'commander.unknownOption', message);
1461
1536
  };
1462
1537
 
@@ -1484,7 +1559,20 @@ Expecting one of '${allowedValues.join("', '")}'`);
1484
1559
  */
1485
1560
 
1486
1561
  unknownCommand() {
1487
- const message = `error: unknown command '${this.args[0]}'`;
1562
+ const unknownName = this.args[0];
1563
+ let suggestion = '';
1564
+
1565
+ if (this._showSuggestionAfterError) {
1566
+ const candidateNames = [];
1567
+ this.createHelp().visibleCommands(this).forEach((command) => {
1568
+ candidateNames.push(command.name());
1569
+ // just visible alias
1570
+ if (command.alias()) candidateNames.push(command.alias());
1571
+ });
1572
+ suggestion = suggestSimilar(unknownName, candidateNames);
1573
+ }
1574
+
1575
+ const message = `error: unknown command '${unknownName}'${suggestion}`;
1488
1576
  this._displayError(1, 'commander.unknownCommand', message);
1489
1577
  };
1490
1578
 
package/lib/help.js CHANGED
@@ -234,21 +234,24 @@ class Help {
234
234
  */
235
235
 
236
236
  optionDescription(option) {
237
- if (option.negate) {
238
- return option.description;
239
- }
240
237
  const extraInfo = [];
241
- if (option.argChoices) {
238
+ // Some of these do not make sense for negated boolean and suppress for backwards compatibility.
239
+
240
+ if (option.argChoices && !option.negate) {
242
241
  extraInfo.push(
243
242
  // use stringify to match the display of the default value
244
243
  `choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
245
244
  }
246
- if (option.defaultValue !== undefined) {
245
+ if (option.defaultValue !== undefined && !option.negate) {
247
246
  extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
248
247
  }
248
+ if (option.envVar !== undefined) {
249
+ extraInfo.push(`env: ${option.envVar}`);
250
+ }
249
251
  if (extraInfo.length > 0) {
250
252
  return `${option.description} (${extraInfo.join(', ')})`;
251
253
  }
254
+
252
255
  return option.description;
253
256
  };
254
257
 
package/lib/option.js CHANGED
@@ -28,6 +28,7 @@ class Option {
28
28
  }
29
29
  this.defaultValue = undefined;
30
30
  this.defaultValueDescription = undefined;
31
+ this.envVar = undefined;
31
32
  this.parseArg = undefined;
32
33
  this.hidden = false;
33
34
  this.argChoices = undefined;
@@ -47,6 +48,19 @@ class Option {
47
48
  return this;
48
49
  };
49
50
 
51
+ /**
52
+ * Set environment variable to check for option value.
53
+ * Priority order of option values is default < env < cli
54
+ *
55
+ * @param {string} name
56
+ * @return {Option}
57
+ */
58
+
59
+ env(name) {
60
+ this.envVar = name;
61
+ return this;
62
+ };
63
+
50
64
  /**
51
65
  * Set the custom handler for processing CLI option arguments into option values.
52
66
  *
@@ -0,0 +1,100 @@
1
+ const maxDistance = 3;
2
+
3
+ function editDistance(a, b) {
4
+ // https://en.wikipedia.org/wiki/Damerau–Levenshtein_distance
5
+ // Calculating optimal string alignment distance, no substring is edited more than once.
6
+ // (Simple implementation.)
7
+
8
+ // Quick early exit, return worst case.
9
+ if (Math.abs(a.length - b.length) > maxDistance) return Math.max(a.length, b.length);
10
+
11
+ // distance between prefix substrings of a and b
12
+ const d = [];
13
+
14
+ // pure deletions turn a into empty string
15
+ for (let i = 0; i <= a.length; i++) {
16
+ d[i] = [i];
17
+ }
18
+ // pure insertions turn empty string into b
19
+ for (let j = 0; j <= b.length; j++) {
20
+ d[0][j] = j;
21
+ }
22
+
23
+ // fill matrix
24
+ for (let j = 1; j <= b.length; j++) {
25
+ for (let i = 1; i <= a.length; i++) {
26
+ let cost = 1;
27
+ if (a[i - 1] === b[j - 1]) {
28
+ cost = 0;
29
+ } else {
30
+ cost = 1;
31
+ }
32
+ d[i][j] = Math.min(
33
+ d[i - 1][j] + 1, // deletion
34
+ d[i][j - 1] + 1, // insertion
35
+ d[i - 1][j - 1] + cost // substitution
36
+ );
37
+ // transposition
38
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
39
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
40
+ }
41
+ }
42
+ }
43
+
44
+ return d[a.length][b.length];
45
+ }
46
+
47
+ /**
48
+ * Find close matches, restricted to same number of edits.
49
+ *
50
+ * @param {string} word
51
+ * @param {string[]} candidates
52
+ * @returns {string}
53
+ */
54
+
55
+ function suggestSimilar(word, candidates) {
56
+ if (!candidates || candidates.length === 0) return '';
57
+ // remove possible duplicates
58
+ candidates = Array.from(new Set(candidates));
59
+
60
+ const searchingOptions = word.startsWith('--');
61
+ if (searchingOptions) {
62
+ word = word.slice(2);
63
+ candidates = candidates.map(candidate => candidate.slice(2));
64
+ }
65
+
66
+ let similar = [];
67
+ let bestDistance = maxDistance;
68
+ const minSimilarity = 0.4;
69
+ candidates.forEach((candidate) => {
70
+ if (candidate.length <= 1) return; // no one character guesses
71
+
72
+ const distance = editDistance(word, candidate);
73
+ const length = Math.max(word.length, candidate.length);
74
+ const similarity = (length - distance) / length;
75
+ if (similarity > minSimilarity) {
76
+ if (distance < bestDistance) {
77
+ // better edit distance, throw away previous worse matches
78
+ bestDistance = distance;
79
+ similar = [candidate];
80
+ } else if (distance === bestDistance) {
81
+ similar.push(candidate);
82
+ }
83
+ }
84
+ });
85
+
86
+ similar.sort((a, b) => a.localeCompare(b));
87
+ if (searchingOptions) {
88
+ similar = similar.map(candidate => `--${candidate}`);
89
+ }
90
+
91
+ if (similar.length > 1) {
92
+ return `\n(Did you mean one of ${similar.join(', ')}?)`;
93
+ }
94
+ if (similar.length === 1) {
95
+ return `\n(Did you mean ${similar[0]}?)`;
96
+ }
97
+ return '';
98
+ }
99
+
100
+ exports.suggestSimilar = suggestSimilar;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commander",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "the complete solution for node.js command-line programs",
5
5
  "keywords": [
6
6
  "commander",
@@ -44,29 +44,29 @@ export class Argument {
44
44
  constructor(arg: string, description?: string);
45
45
 
46
46
  /**
47
- * Return argument name.
48
- */
47
+ * Return argument name.
48
+ */
49
49
  name(): string;
50
50
 
51
51
  /**
52
52
  * Set the default value, and optionally supply the description to be displayed in the help.
53
53
  */
54
- default(value: unknown, description?: string): this;
54
+ default(value: unknown, description?: string): this;
55
55
 
56
56
  /**
57
57
  * Set the custom handler for processing CLI command arguments into argument values.
58
58
  */
59
- argParser<T>(fn: (value: string, previous: T) => T): this;
59
+ argParser<T>(fn: (value: string, previous: T) => T): this;
60
60
 
61
61
  /**
62
62
  * Only allow argument value to be one of choices.
63
63
  */
64
- choices(values: string[]): this;
64
+ choices(values: string[]): this;
65
65
 
66
66
  /**
67
67
  * Make option-argument required.
68
68
  */
69
- argRequired(): this;
69
+ argRequired(): this;
70
70
 
71
71
  /**
72
72
  * Make option-argument optional.
@@ -99,6 +99,12 @@ export class Option {
99
99
  */
100
100
  default(value: unknown, description?: string): this;
101
101
 
102
+ /**
103
+ * Set environment variable to check for option value.
104
+ * Priority order of option values is default < env < cli
105
+ */
106
+ env(name: string): this;
107
+
102
108
  /**
103
109
  * Calculate the full description, including defaultValue etc.
104
110
  */
@@ -119,12 +125,6 @@ export class Option {
119
125
  */
120
126
  hideHelp(hide?: boolean): this;
121
127
 
122
- /**
123
- * Validation of option argument failed.
124
- * Intended for use from custom argument processing functions.
125
- */
126
- argumentRejected(messsage: string): never;
127
-
128
128
  /**
129
129
  * Only allow option value to be one of choices.
130
130
  */
@@ -406,7 +406,7 @@ export class Command {
406
406
  *
407
407
  * (Used internally when adding a command using `.command()` so subcommands inherit parent settings.)
408
408
  */
409
- copyInheritedSettings(sourceCommand: Command): this;
409
+ copyInheritedSettings(sourceCommand: Command): this;
410
410
 
411
411
  /**
412
412
  * Display the help or a custom message after an error occurs.
@@ -414,6 +414,11 @@ export class Command {
414
414
  showHelpAfterError(displayHelp?: boolean | string): this;
415
415
 
416
416
  /**
417
+ * Display suggestion of similar commands for unknown commands, or options for unknown options.
418
+ */
419
+ showSuggestionAfterError(displaySuggestion?: boolean): this;
420
+
421
+ /**
417
422
  * Register callback `fn` for the command.
418
423
  *
419
424
  * @example