commander 13.1.0 → 14.0.1

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
@@ -38,6 +38,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
38
38
  - [.description and .summary](#description-and-summary)
39
39
  - [.helpOption(flags, description)](#helpoptionflags-description)
40
40
  - [.helpCommand()](#helpcommand)
41
+ - [Help Groups](#help-groups)
41
42
  - [More configuration](#more-configuration-2)
42
43
  - [Custom event listeners](#custom-event-listeners)
43
44
  - [Bits and pieces](#bits-and-pieces)
@@ -334,7 +335,8 @@ add cheese type mozzarella
334
335
  ```
335
336
 
336
337
  Options with an optional option-argument are not greedy and will ignore arguments starting with a dash.
337
- So `id` behaves as a boolean option for `--id -5`, but you can use a combined form if needed like `--id=-5`.
338
+ So `id` behaves as a boolean option for `--id -ABCD`, but you can use a combined form if needed like `--id=-ABCD`.
339
+ Negative numbers are special and are accepted as an option-argument.
338
340
 
339
341
  For information about possible ambiguous cases, see [options taking varying arguments](./docs/options-in-depth.md).
340
342
 
@@ -926,6 +928,14 @@ program.helpCommand('assist [command]', 'show assistance');
926
928
 
927
929
  (Or use `.addHelpCommand()` to add a command you construct yourself.)
928
930
 
931
+ ### Help Groups
932
+
933
+ The help by default lists options under the the heading `Options:` and commands under `Commands:`. You can create your own groups
934
+ with different headings. The high-level way is to set the desired group heading while adding the options and commands,
935
+ using `.optionsGroup()` and `.commandsGroup()`. The low-level way is using `.helpGroup()` on an individual `Option` or `Command`
936
+
937
+ Example file: [help-groups.js](./examples/help-groups.js)
938
+
929
939
  ### More configuration
930
940
 
931
941
  The built-in help is formatted using the Help class.
@@ -933,7 +943,7 @@ You can configure the help by modifying data properties and methods using `.conf
933
943
 
934
944
  Simple properties include `sortSubcommands`, `sortOptions`, and `showGlobalOptions`. You can add color using the style methods like `styleTitle()`.
935
945
 
936
- For more detail and examples of changing the displayed text, color, and layout see (./docs/help-in-depth.md)
946
+ For more detail and examples of changing the displayed text, color, and layout see [help in depth](./docs/help-in-depth.md).
937
947
 
938
948
  ## Custom event listeners
939
949
 
@@ -999,8 +1009,8 @@ program arg --port=80
999
1009
 
1000
1010
  By default, the option processing shows an error for an unknown option. To have an unknown option treated as an ordinary command-argument and continue looking for options, use `.allowUnknownOption()`. This lets you mix known and unknown options.
1001
1011
 
1002
- By default, the argument processing does not display an error for more command-arguments than expected.
1003
- To display an error for excess arguments, use`.allowExcessArguments(false)`.
1012
+ By default, the argument processing displays an error for more command-arguments than expected.
1013
+ To suppress the error for excess arguments, use`.allowExcessArguments()`.
1004
1014
 
1005
1015
  ### Legacy options as properties
1006
1016
 
@@ -1053,7 +1063,7 @@ customise the new subcommand (example file [custom-command-class.js](./examples/
1053
1063
  You can enable `--harmony` option in two ways:
1054
1064
 
1055
1065
  - Use `#! /usr/bin/env node --harmony` in the subcommands scripts. (Note Windows does not support this pattern.)
1056
- - Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning subcommand process.
1066
+ - Use the `--harmony` option when calling the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning subcommand processes.
1057
1067
 
1058
1068
  ### Debugging stand-alone executable subcommands
1059
1069
 
@@ -1137,7 +1147,7 @@ There is more information available about:
1137
1147
 
1138
1148
  ## Support
1139
1149
 
1140
- The current version of Commander is fully supported on Long Term Support versions of Node.js, and requires at least v18.
1150
+ The current version of Commander is fully supported on Long Term Support versions of Node.js, and requires at least v20.
1141
1151
  (For older versions of Node.js, use an older version of Commander.)
1142
1152
 
1143
1153
  The main forum for free and community support is the project [Issues](https://github.com/tj/commander.js/issues) on GitHub.
package/lib/argument.js CHANGED
@@ -33,7 +33,7 @@ class Argument {
33
33
  break;
34
34
  }
35
35
 
36
- if (this._name.length > 3 && this._name.slice(-3) === '...') {
36
+ if (this._name.endsWith('...')) {
37
37
  this.variadic = true;
38
38
  this._name = this._name.slice(0, -3);
39
39
  }
@@ -53,12 +53,13 @@ class Argument {
53
53
  * @package
54
54
  */
55
55
 
56
- _concatValue(value, previous) {
56
+ _collectValue(value, previous) {
57
57
  if (previous === this.defaultValue || !Array.isArray(previous)) {
58
58
  return [value];
59
59
  }
60
60
 
61
- return previous.concat(value);
61
+ previous.push(value);
62
+ return previous;
62
63
  }
63
64
 
64
65
  /**
@@ -103,7 +104,7 @@ class Argument {
103
104
  );
104
105
  }
105
106
  if (this.variadic) {
106
- return this._concatValue(arg, previous);
107
+ return this._collectValue(arg, previous);
107
108
  }
108
109
  return arg;
109
110
  };
package/lib/command.js CHANGED
@@ -80,6 +80,12 @@ class Command extends EventEmitter {
80
80
  /** @type {Command} */
81
81
  this._helpCommand = undefined; // lazy initialised, inherited
82
82
  this._helpConfiguration = {};
83
+ /** @type {string | undefined} */
84
+ this._helpGroupHeading = undefined; // soft initialised when added to parent
85
+ /** @type {string | undefined} */
86
+ this._defaultCommandGroup = undefined;
87
+ /** @type {string | undefined} */
88
+ this._defaultOptionGroup = undefined;
83
89
  }
84
90
 
85
91
  /**
@@ -239,7 +245,10 @@ class Command extends EventEmitter {
239
245
  configureOutput(configuration) {
240
246
  if (configuration === undefined) return this._outputConfiguration;
241
247
 
242
- Object.assign(this._outputConfiguration, configuration);
248
+ this._outputConfiguration = {
249
+ ...this._outputConfiguration,
250
+ ...configuration,
251
+ };
243
252
  return this;
244
253
  }
245
254
 
@@ -320,16 +329,16 @@ class Command extends EventEmitter {
320
329
  *
321
330
  * @param {string} name
322
331
  * @param {string} [description]
323
- * @param {(Function|*)} [fn] - custom argument processing function
332
+ * @param {(Function|*)} [parseArg] - custom argument processing function or default value
324
333
  * @param {*} [defaultValue]
325
334
  * @return {Command} `this` command for chaining
326
335
  */
327
- argument(name, description, fn, defaultValue) {
336
+ argument(name, description, parseArg, defaultValue) {
328
337
  const argument = this.createArgument(name, description);
329
- if (typeof fn === 'function') {
330
- argument.default(defaultValue).argParser(fn);
338
+ if (typeof parseArg === 'function') {
339
+ argument.default(defaultValue).argParser(parseArg);
331
340
  } else {
332
- argument.default(fn);
341
+ argument.default(parseArg);
333
342
  }
334
343
  this.addArgument(argument);
335
344
  return this;
@@ -365,7 +374,7 @@ class Command extends EventEmitter {
365
374
  */
366
375
  addArgument(argument) {
367
376
  const previousArgument = this.registeredArguments.slice(-1)[0];
368
- if (previousArgument && previousArgument.variadic) {
377
+ if (previousArgument?.variadic) {
369
378
  throw new Error(
370
379
  `only the last argument can be variadic '${previousArgument.name()}'`,
371
380
  );
@@ -400,11 +409,15 @@ class Command extends EventEmitter {
400
409
  helpCommand(enableOrNameAndArgs, description) {
401
410
  if (typeof enableOrNameAndArgs === 'boolean') {
402
411
  this._addImplicitHelpCommand = enableOrNameAndArgs;
412
+ if (enableOrNameAndArgs && this._defaultCommandGroup) {
413
+ // make the command to store the group
414
+ this._initCommandGroup(this._getHelpCommand());
415
+ }
403
416
  return this;
404
417
  }
405
418
 
406
- enableOrNameAndArgs = enableOrNameAndArgs ?? 'help [command]';
407
- const [, helpName, helpArgs] = enableOrNameAndArgs.match(/([^ ]+) *(.*)/);
419
+ const nameAndArgs = enableOrNameAndArgs ?? 'help [command]';
420
+ const [, helpName, helpArgs] = nameAndArgs.match(/([^ ]+) *(.*)/);
408
421
  const helpDescription = description ?? 'display help for command';
409
422
 
410
423
  const helpCommand = this.createCommand(helpName);
@@ -414,6 +427,8 @@ class Command extends EventEmitter {
414
427
 
415
428
  this._addImplicitHelpCommand = true;
416
429
  this._helpCommand = helpCommand;
430
+ // init group unless lazy create
431
+ if (enableOrNameAndArgs || description) this._initCommandGroup(helpCommand);
417
432
 
418
433
  return this;
419
434
  }
@@ -435,6 +450,7 @@ class Command extends EventEmitter {
435
450
 
436
451
  this._addImplicitHelpCommand = true;
437
452
  this._helpCommand = helpCommand;
453
+ this._initCommandGroup(helpCommand);
438
454
  return this;
439
455
  }
440
456
 
@@ -613,6 +629,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
613
629
  - already used by option '${matchingOption.flags}'`);
614
630
  }
615
631
 
632
+ this._initOptionGroup(option);
616
633
  this.options.push(option);
617
634
  }
618
635
 
@@ -640,6 +657,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
640
657
  );
641
658
  }
642
659
 
660
+ this._initCommandGroup(command);
643
661
  this.commands.push(command);
644
662
  }
645
663
 
@@ -683,7 +701,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
683
701
  if (val !== null && option.parseArg) {
684
702
  val = this._callParseArg(option, val, oldValue, invalidValueMessage);
685
703
  } else if (val !== null && option.variadic) {
686
- val = option._concatValue(val, oldValue);
704
+ val = option._collectValue(val, oldValue);
687
705
  }
688
706
 
689
707
  // Fill-in appropriate missing values. Long winded but easy to follow.
@@ -1462,7 +1480,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1462
1480
 
1463
1481
  _chainOrCall(promise, fn) {
1464
1482
  // thenable
1465
- if (promise && promise.then && typeof promise.then === 'function') {
1483
+ if (promise?.then && typeof promise.then === 'function') {
1466
1484
  // already have a promise, chain callback
1467
1485
  return promise.then(() => fn());
1468
1486
  }
@@ -1593,7 +1611,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1593
1611
  promiseChain = this._chainOrCallHooks(promiseChain, 'postAction');
1594
1612
  return promiseChain;
1595
1613
  }
1596
- if (this.parent && this.parent.listenerCount(commandEvent)) {
1614
+ if (this.parent?.listenerCount(commandEvent)) {
1597
1615
  checkForUnknownOptions();
1598
1616
  this._processArguments();
1599
1617
  this.parent.emit(commandEvent, operands, unknown); // legacy
@@ -1723,33 +1741,49 @@ Expecting one of '${allowedValues.join("', '")}'`);
1723
1741
  * sub --unknown uuu op => [sub], [--unknown uuu op]
1724
1742
  * sub -- --unknown uuu op => [sub --unknown uuu op], []
1725
1743
  *
1726
- * @param {string[]} argv
1744
+ * @param {string[]} args
1727
1745
  * @return {{operands: string[], unknown: string[]}}
1728
1746
  */
1729
1747
 
1730
- parseOptions(argv) {
1748
+ parseOptions(args) {
1731
1749
  const operands = []; // operands, not options or values
1732
1750
  const unknown = []; // first unknown option and remaining unknown args
1733
1751
  let dest = operands;
1734
- const args = argv.slice();
1735
1752
 
1736
1753
  function maybeOption(arg) {
1737
1754
  return arg.length > 1 && arg[0] === '-';
1738
1755
  }
1739
1756
 
1757
+ const negativeNumberArg = (arg) => {
1758
+ // return false if not a negative number
1759
+ if (!/^-\d*\.?\d+(e[+-]?\d+)?$/.test(arg)) return false;
1760
+ // negative number is ok unless digit used as an option in command hierarchy
1761
+ return !this._getCommandAndAncestors().some((cmd) =>
1762
+ cmd.options
1763
+ .map((opt) => opt.short)
1764
+ .some((short) => /^-\d$/.test(short)),
1765
+ );
1766
+ };
1767
+
1740
1768
  // parse options
1741
1769
  let activeVariadicOption = null;
1742
- while (args.length) {
1743
- const arg = args.shift();
1770
+ let activeGroup = null; // working through group of short options, like -abc
1771
+ let i = 0;
1772
+ while (i < args.length || activeGroup) {
1773
+ const arg = activeGroup ?? args[i++];
1774
+ activeGroup = null;
1744
1775
 
1745
1776
  // literal
1746
1777
  if (arg === '--') {
1747
1778
  if (dest === unknown) dest.push(arg);
1748
- dest.push(...args);
1779
+ dest.push(...args.slice(i));
1749
1780
  break;
1750
1781
  }
1751
1782
 
1752
- if (activeVariadicOption && !maybeOption(arg)) {
1783
+ if (
1784
+ activeVariadicOption &&
1785
+ (!maybeOption(arg) || negativeNumberArg(arg))
1786
+ ) {
1753
1787
  this.emit(`option:${activeVariadicOption.name()}`, arg);
1754
1788
  continue;
1755
1789
  }
@@ -1760,14 +1794,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1760
1794
  // recognised option, call listener to assign value with possible custom processing
1761
1795
  if (option) {
1762
1796
  if (option.required) {
1763
- const value = args.shift();
1797
+ const value = args[i++];
1764
1798
  if (value === undefined) this.optionMissingArgument(option);
1765
1799
  this.emit(`option:${option.name()}`, value);
1766
1800
  } else if (option.optional) {
1767
1801
  let value = null;
1768
1802
  // historical behaviour is optional value is following arg unless an option
1769
- if (args.length > 0 && !maybeOption(args[0])) {
1770
- value = args.shift();
1803
+ if (
1804
+ i < args.length &&
1805
+ (!maybeOption(args[i]) || negativeNumberArg(args[i]))
1806
+ ) {
1807
+ value = args[i++];
1771
1808
  }
1772
1809
  this.emit(`option:${option.name()}`, value);
1773
1810
  } else {
@@ -1790,9 +1827,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1790
1827
  // option with value following in same argument
1791
1828
  this.emit(`option:${option.name()}`, arg.slice(2));
1792
1829
  } else {
1793
- // boolean option, emit and put back remainder of arg for further processing
1830
+ // boolean option
1794
1831
  this.emit(`option:${option.name()}`);
1795
- args.unshift(`-${arg.slice(2)}`);
1832
+ // remove the processed option and keep processing group
1833
+ activeGroup = `-${arg.slice(2)}`;
1796
1834
  }
1797
1835
  continue;
1798
1836
  }
@@ -1812,7 +1850,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
1812
1850
  // Might be a command-argument, or subcommand option, or unknown option, or help command or option.
1813
1851
 
1814
1852
  // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands.
1815
- if (maybeOption(arg)) {
1853
+ // A negative number in a leaf command is not an unknown option.
1854
+ if (
1855
+ dest === operands &&
1856
+ maybeOption(arg) &&
1857
+ !(this.commands.length === 0 && negativeNumberArg(arg))
1858
+ ) {
1816
1859
  dest = unknown;
1817
1860
  }
1818
1861
 
@@ -1824,26 +1867,23 @@ Expecting one of '${allowedValues.join("', '")}'`);
1824
1867
  ) {
1825
1868
  if (this._findCommand(arg)) {
1826
1869
  operands.push(arg);
1827
- if (args.length > 0) unknown.push(...args);
1870
+ unknown.push(...args.slice(i));
1828
1871
  break;
1829
1872
  } else if (
1830
1873
  this._getHelpCommand() &&
1831
1874
  arg === this._getHelpCommand().name()
1832
1875
  ) {
1833
- operands.push(arg);
1834
- if (args.length > 0) operands.push(...args);
1876
+ operands.push(arg, ...args.slice(i));
1835
1877
  break;
1836
1878
  } else if (this._defaultCommandName) {
1837
- unknown.push(arg);
1838
- if (args.length > 0) unknown.push(...args);
1879
+ unknown.push(arg, ...args.slice(i));
1839
1880
  break;
1840
1881
  }
1841
1882
  }
1842
1883
 
1843
1884
  // If using passThroughOptions, stop processing options at first command-argument.
1844
1885
  if (this._passThroughOptions) {
1845
- dest.push(arg);
1846
- if (args.length > 0) dest.push(...args);
1886
+ dest.push(arg, ...args.slice(i));
1847
1887
  break;
1848
1888
  }
1849
1889
 
@@ -2294,6 +2334,75 @@ Expecting one of '${allowedValues.join("', '")}'`);
2294
2334
  return this;
2295
2335
  }
2296
2336
 
2337
+ /**
2338
+ * Set/get the help group heading for this subcommand in parent command's help.
2339
+ *
2340
+ * @param {string} [heading]
2341
+ * @return {Command | string}
2342
+ */
2343
+
2344
+ helpGroup(heading) {
2345
+ if (heading === undefined) return this._helpGroupHeading ?? '';
2346
+ this._helpGroupHeading = heading;
2347
+ return this;
2348
+ }
2349
+
2350
+ /**
2351
+ * Set/get the default help group heading for subcommands added to this command.
2352
+ * (This does not override a group set directly on the subcommand using .helpGroup().)
2353
+ *
2354
+ * @example
2355
+ * program.commandsGroup('Development Commands:);
2356
+ * program.command('watch')...
2357
+ * program.command('lint')...
2358
+ * ...
2359
+ *
2360
+ * @param {string} [heading]
2361
+ * @returns {Command | string}
2362
+ */
2363
+ commandsGroup(heading) {
2364
+ if (heading === undefined) return this._defaultCommandGroup ?? '';
2365
+ this._defaultCommandGroup = heading;
2366
+ return this;
2367
+ }
2368
+
2369
+ /**
2370
+ * Set/get the default help group heading for options added to this command.
2371
+ * (This does not override a group set directly on the option using .helpGroup().)
2372
+ *
2373
+ * @example
2374
+ * program
2375
+ * .optionsGroup('Development Options:')
2376
+ * .option('-d, --debug', 'output extra debugging')
2377
+ * .option('-p, --profile', 'output profiling information')
2378
+ *
2379
+ * @param {string} [heading]
2380
+ * @returns {Command | string}
2381
+ */
2382
+ optionsGroup(heading) {
2383
+ if (heading === undefined) return this._defaultOptionGroup ?? '';
2384
+ this._defaultOptionGroup = heading;
2385
+ return this;
2386
+ }
2387
+
2388
+ /**
2389
+ * @param {Option} option
2390
+ * @private
2391
+ */
2392
+ _initOptionGroup(option) {
2393
+ if (this._defaultOptionGroup && !option.helpGroupHeading)
2394
+ option.helpGroup(this._defaultOptionGroup);
2395
+ }
2396
+
2397
+ /**
2398
+ * @param {Command} cmd
2399
+ * @private
2400
+ */
2401
+ _initCommandGroup(cmd) {
2402
+ if (this._defaultCommandGroup && !cmd.helpGroup())
2403
+ cmd.helpGroup(this._defaultCommandGroup);
2404
+ }
2405
+
2297
2406
  /**
2298
2407
  * Set the name of the command from script filename, such as process.argv[1],
2299
2408
  * or require.main.filename, or __filename.
@@ -2448,12 +2557,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
2448
2557
  */
2449
2558
 
2450
2559
  helpOption(flags, description) {
2451
- // Support disabling built-in help option.
2560
+ // Support enabling/disabling built-in help option.
2452
2561
  if (typeof flags === 'boolean') {
2453
- // true is not an expected value. Do something sensible but no unit-test.
2454
- // istanbul ignore if
2455
2562
  if (flags) {
2456
- this._helpOption = this._helpOption ?? undefined; // preserve existing option
2563
+ if (this._helpOption === null) this._helpOption = undefined; // reenable
2564
+ if (this._defaultOptionGroup) {
2565
+ // make the option to store the group
2566
+ this._initOptionGroup(this._getHelpOption());
2567
+ }
2457
2568
  } else {
2458
2569
  this._helpOption = null; // disable
2459
2570
  }
@@ -2461,9 +2572,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
2461
2572
  }
2462
2573
 
2463
2574
  // Customise flags and description.
2464
- flags = flags ?? '-h, --help';
2465
- description = description ?? 'display help for command';
2466
- this._helpOption = this.createOption(flags, description);
2575
+ this._helpOption = this.createOption(
2576
+ flags ?? '-h, --help',
2577
+ description ?? 'display help for command',
2578
+ );
2579
+ // init group unless lazy create
2580
+ if (flags || description) this._initOptionGroup(this._helpOption);
2467
2581
 
2468
2582
  return this;
2469
2583
  }
@@ -2492,6 +2606,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2492
2606
  */
2493
2607
  addHelpOption(option) {
2494
2608
  this._helpOption = option;
2609
+ this._initOptionGroup(option);
2495
2610
  return this;
2496
2611
  }
2497
2612
 
package/lib/help.js CHANGED
@@ -352,7 +352,11 @@ class Help {
352
352
  extraInfo.push(`env: ${option.envVar}`);
353
353
  }
354
354
  if (extraInfo.length > 0) {
355
- return `${option.description} (${extraInfo.join(', ')})`;
355
+ const extraDescription = `(${extraInfo.join(', ')})`;
356
+ if (option.description) {
357
+ return `${option.description} ${extraDescription}`;
358
+ }
359
+ return extraDescription;
356
360
  }
357
361
 
358
362
  return option.description;
@@ -388,6 +392,46 @@ class Help {
388
392
  return argument.description;
389
393
  }
390
394
 
395
+ /**
396
+ * Format a list of items, given a heading and an array of formatted items.
397
+ *
398
+ * @param {string} heading
399
+ * @param {string[]} items
400
+ * @param {Help} helper
401
+ * @returns string[]
402
+ */
403
+ formatItemList(heading, items, helper) {
404
+ if (items.length === 0) return [];
405
+
406
+ return [helper.styleTitle(heading), ...items, ''];
407
+ }
408
+
409
+ /**
410
+ * Group items by their help group heading.
411
+ *
412
+ * @param {Command[] | Option[]} unsortedItems
413
+ * @param {Command[] | Option[]} visibleItems
414
+ * @param {Function} getGroup
415
+ * @returns {Map<string, Command[] | Option[]>}
416
+ */
417
+ groupItems(unsortedItems, visibleItems, getGroup) {
418
+ const result = new Map();
419
+ // Add groups in order of appearance in unsortedItems.
420
+ unsortedItems.forEach((item) => {
421
+ const group = getGroup(item);
422
+ if (!result.has(group)) result.set(group, []);
423
+ });
424
+ // Add items in order of appearance in visibleItems.
425
+ visibleItems.forEach((item) => {
426
+ const group = getGroup(item);
427
+ if (!result.has(group)) {
428
+ result.set(group, []);
429
+ }
430
+ result.get(group).push(item);
431
+ });
432
+ return result;
433
+ }
434
+
391
435
  /**
392
436
  * Generate the built-in help text.
393
437
  *
@@ -429,28 +473,25 @@ class Help {
429
473
  helper.styleArgumentDescription(helper.argumentDescription(argument)),
430
474
  );
431
475
  });
432
- if (argumentList.length > 0) {
433
- output = output.concat([
434
- helper.styleTitle('Arguments:'),
435
- ...argumentList,
436
- '',
437
- ]);
438
- }
476
+ output = output.concat(
477
+ this.formatItemList('Arguments:', argumentList, helper),
478
+ );
439
479
 
440
480
  // Options
441
- const optionList = helper.visibleOptions(cmd).map((option) => {
442
- return callFormatItem(
443
- helper.styleOptionTerm(helper.optionTerm(option)),
444
- helper.styleOptionDescription(helper.optionDescription(option)),
445
- );
481
+ const optionGroups = this.groupItems(
482
+ cmd.options,
483
+ helper.visibleOptions(cmd),
484
+ (option) => option.helpGroupHeading ?? 'Options:',
485
+ );
486
+ optionGroups.forEach((options, group) => {
487
+ const optionList = options.map((option) => {
488
+ return callFormatItem(
489
+ helper.styleOptionTerm(helper.optionTerm(option)),
490
+ helper.styleOptionDescription(helper.optionDescription(option)),
491
+ );
492
+ });
493
+ output = output.concat(this.formatItemList(group, optionList, helper));
446
494
  });
447
- if (optionList.length > 0) {
448
- output = output.concat([
449
- helper.styleTitle('Options:'),
450
- ...optionList,
451
- '',
452
- ]);
453
- }
454
495
 
455
496
  if (helper.showGlobalOptions) {
456
497
  const globalOptionList = helper
@@ -461,29 +502,26 @@ class Help {
461
502
  helper.styleOptionDescription(helper.optionDescription(option)),
462
503
  );
463
504
  });
464
- if (globalOptionList.length > 0) {
465
- output = output.concat([
466
- helper.styleTitle('Global Options:'),
467
- ...globalOptionList,
468
- '',
469
- ]);
470
- }
505
+ output = output.concat(
506
+ this.formatItemList('Global Options:', globalOptionList, helper),
507
+ );
471
508
  }
472
509
 
473
510
  // Commands
474
- const commandList = helper.visibleCommands(cmd).map((cmd) => {
475
- return callFormatItem(
476
- helper.styleSubcommandTerm(helper.subcommandTerm(cmd)),
477
- helper.styleSubcommandDescription(helper.subcommandDescription(cmd)),
478
- );
511
+ const commandGroups = this.groupItems(
512
+ cmd.commands,
513
+ helper.visibleCommands(cmd),
514
+ (sub) => sub.helpGroup() || 'Commands:',
515
+ );
516
+ commandGroups.forEach((commands, group) => {
517
+ const commandList = commands.map((sub) => {
518
+ return callFormatItem(
519
+ helper.styleSubcommandTerm(helper.subcommandTerm(sub)),
520
+ helper.styleSubcommandDescription(helper.subcommandDescription(sub)),
521
+ );
522
+ });
523
+ output = output.concat(this.formatItemList(group, commandList, helper));
479
524
  });
480
- if (commandList.length > 0) {
481
- output = output.concat([
482
- helper.styleTitle('Commands:'),
483
- ...commandList,
484
- '',
485
- ]);
486
- }
487
525
 
488
526
  return output.join('\n');
489
527
  }
package/lib/option.js CHANGED
@@ -33,6 +33,7 @@ class Option {
33
33
  this.argChoices = undefined;
34
34
  this.conflictsWith = [];
35
35
  this.implied = undefined;
36
+ this.helpGroupHeading = undefined; // soft initialised when option added to command
36
37
  }
37
38
 
38
39
  /**
@@ -161,12 +162,13 @@ class Option {
161
162
  * @package
162
163
  */
163
164
 
164
- _concatValue(value, previous) {
165
+ _collectValue(value, previous) {
165
166
  if (previous === this.defaultValue || !Array.isArray(previous)) {
166
167
  return [value];
167
168
  }
168
169
 
169
- return previous.concat(value);
170
+ previous.push(value);
171
+ return previous;
170
172
  }
171
173
 
172
174
  /**
@@ -185,7 +187,7 @@ class Option {
185
187
  );
186
188
  }
187
189
  if (this.variadic) {
188
- return this._concatValue(arg, previous);
190
+ return this._collectValue(arg, previous);
189
191
  }
190
192
  return arg;
191
193
  };
@@ -219,6 +221,17 @@ class Option {
219
221
  return camelcase(this.name());
220
222
  }
221
223
 
224
+ /**
225
+ * Set the help group heading.
226
+ *
227
+ * @param {string} heading
228
+ * @return {Option}
229
+ */
230
+ helpGroup(heading) {
231
+ this.helpGroupHeading = heading;
232
+ return this;
233
+ }
234
+
222
235
  /**
223
236
  * Check if `arg` matches the short or long flag.
224
237
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commander",
3
- "version": "13.1.0",
3
+ "version": "14.0.1",
4
4
  "description": "the complete solution for node.js command-line programs",
5
5
  "keywords": [
6
6
  "commander",
@@ -61,22 +61,22 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^9.4.0",
64
- "@types/jest": "^29.2.4",
64
+ "@types/jest": "^30.0.0",
65
65
  "@types/node": "^22.7.4",
66
66
  "eslint": "^9.17.0",
67
- "eslint-config-prettier": "^9.1.0",
68
- "eslint-plugin-jest": "^28.3.0",
69
- "globals": "^15.7.0",
70
- "jest": "^29.3.1",
67
+ "eslint-config-prettier": "^10.0.1",
68
+ "eslint-plugin-jest": "^29.0.1",
69
+ "globals": "^16.0.0",
70
+ "jest": "^30.0.3",
71
71
  "prettier": "^3.2.5",
72
72
  "ts-jest": "^29.0.3",
73
- "tsd": "^0.31.0",
73
+ "tsd": "^0.33.0",
74
74
  "typescript": "^5.0.4",
75
75
  "typescript-eslint": "^8.12.2"
76
76
  },
77
77
  "types": "typings/index.d.ts",
78
78
  "engines": {
79
- "node": ">=18"
79
+ "node": ">=20"
80
80
  },
81
81
  "support": true
82
82
  }
@@ -51,6 +51,7 @@ export class Argument {
51
51
  variadic: boolean;
52
52
  defaultValue?: any;
53
53
  defaultValueDescription?: string;
54
+ parseArg?: <T>(value: string, previous: T) => T;
54
55
  argChoices?: string[];
55
56
 
56
57
  /**
@@ -109,6 +110,7 @@ export class Option {
109
110
  parseArg?: <T>(value: string, previous: T) => T;
110
111
  hidden: boolean;
111
112
  argChoices?: string[];
113
+ helpGroupHeading?: string;
112
114
 
113
115
  constructor(flags: string, description?: string);
114
116
 
@@ -192,6 +194,11 @@ export class Option {
192
194
  */
193
195
  attributeName(): string;
194
196
 
197
+ /**
198
+ * Set the help group heading.
199
+ */
200
+ helpGroup(heading: string): this;
201
+
195
202
  /**
196
203
  * Return whether a boolean option.
197
204
  *
@@ -313,6 +320,20 @@ export class Help {
313
320
  helper: Help,
314
321
  ): string;
315
322
 
323
+ /**
324
+ * Format a list of items, given a heading and an array of formatted items.
325
+ */
326
+ formatItemList(heading: string, items: string[], helper: Help): string[];
327
+
328
+ /**
329
+ * Group items by their help group heading.
330
+ */
331
+ groupItems<T extends Command | Option>(
332
+ unsortedItems: T[],
333
+ visibleItems: T[],
334
+ getGroup: (item: T) => string,
335
+ ): Map<string, T[]>;
336
+
316
337
  /** Generate the built-in help text. */
317
338
  formatHelp(cmd: Command, helper: Help): string;
318
339
  }
@@ -466,7 +487,7 @@ export class Command {
466
487
  argument<T>(
467
488
  flags: string,
468
489
  description: string,
469
- fn: (value: string, previous: T) => T,
490
+ parseArg: (value: string, previous: T) => T,
470
491
  defaultValue?: T,
471
492
  ): this;
472
493
  argument(name: string, description?: string, defaultValue?: unknown): this;
@@ -968,6 +989,53 @@ export class Command {
968
989
  */
969
990
  executableDir(): string | null;
970
991
 
992
+ /**
993
+ * Set the help group heading for this subcommand in parent command's help.
994
+ *
995
+ * @returns `this` command for chaining
996
+ */
997
+ helpGroup(heading: string): this;
998
+ /**
999
+ * Get the help group heading for this subcommand in parent command's help.
1000
+ */
1001
+ helpGroup(): string;
1002
+
1003
+ /**
1004
+ * Set the default help group heading for subcommands added to this command.
1005
+ * (This does not override a group set directly on the subcommand using .helpGroup().)
1006
+ *
1007
+ * @example
1008
+ * program.commandsGroup('Development Commands:);
1009
+ * program.command('watch')...
1010
+ * program.command('lint')...
1011
+ * ...
1012
+ *
1013
+ * @returns `this` command for chaining
1014
+ */
1015
+ commandsGroup(heading: string): this;
1016
+ /**
1017
+ * Get the default help group heading for subcommands added to this command.
1018
+ */
1019
+ commandsGroup(): string;
1020
+
1021
+ /**
1022
+ * Set the default help group heading for options added to this command.
1023
+ * (This does not override a group set directly on the option using .helpGroup().)
1024
+ *
1025
+ * @example
1026
+ * program
1027
+ * .optionsGroup('Development Options:')
1028
+ * .option('-d, --debug', 'output extra debugging')
1029
+ * .option('-p, --profile', 'output profiling information')
1030
+ *
1031
+ * @returns `this` command for chaining
1032
+ */
1033
+ optionsGroup(heading: string): this;
1034
+ /**
1035
+ * Get the default help group heading for options added to this command.
1036
+ */
1037
+ optionsGroup(): string;
1038
+
971
1039
  /**
972
1040
  * Output help information for this command.
973
1041
  *